Skip to content

Commit

Permalink
feat: added dynamic bucket region
Browse files Browse the repository at this point in the history
  • Loading branch information
pregnor committed Apr 26, 2021
1 parent e208d4e commit 35d06fe
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 52 deletions.
2 changes: 1 addition & 1 deletion cmd/helms3/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func (act deleteAction) Run(ctx context.Context) error {
return err
}

sess, err := awsutil.Session()
sess, err := awsutil.Session(awsutil.DynamicBucketRegion(repoEntry.URL()))
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/helms3/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func (act initAction) Run(ctx context.Context) error {
return errors.WithMessage(err, "get index reader")
}

sess, err := awsutil.Session()
sess, err := awsutil.Session(awsutil.DynamicBucketRegion(act.uri))
if err != nil {
return err
}
Expand Down
5 changes: 4 additions & 1 deletion cmd/helms3/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ type proxyCmd struct {
const indexYaml = "index.yaml"

func (act proxyCmd) Run(ctx context.Context) error {
sess, err := awsutil.Session(awsutil.AssumeRoleTokenProvider(awsutil.StderrTokenProvider))
sess, err := awsutil.Session(
awsutil.AssumeRoleTokenProvider(awsutil.StderrTokenProvider),
awsutil.DynamicBucketRegion(act.uri),
)
if err != nil {
return err
}
Expand Down
12 changes: 6 additions & 6 deletions cmd/helms3/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ func (act pushAction) Run(ctx context.Context) error {
return ErrForceAndIgnoreIfExists
}

sess, err := awsutil.Session()
repoEntry, err := helmutil.LookupRepoEntry(act.repoName)
if err != nil {
return err
}

sess, err := awsutil.Session(awsutil.DynamicBucketRegion(repoEntry.URL()))
if err != nil {
return err
}
Expand All @@ -70,11 +75,6 @@ func (act pushAction) Run(ctx context.Context) error {
return err
}

repoEntry, err := helmutil.LookupRepoEntry(act.repoName)
if err != nil {
return err
}

if cachedIndex, err := helmutil.LoadIndex(repoEntry.CacheFile()); err == nil {
// if cached index exists, check if the same chart version exists in it.
if cachedIndex.Has(chart.Name(), chart.Version()) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/helms3/reindex.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (act reindexAction) Run(ctx context.Context) error {
return err
}

sess, err := awsutil.Session()
sess, err := awsutil.Session(awsutil.DynamicBucketRegion(repoEntry.URL()))
if err != nil {
return err
}
Expand Down
42 changes: 0 additions & 42 deletions go.sum

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions internal/awsutil/session.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package awsutil

import (
"net/url"
"os"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
)

const (
Expand All @@ -30,6 +33,67 @@ func AssumeRoleTokenProvider(provider func() (string, error)) SessionOption {
}
}

// DynamicBucketRegion is an option for determining the Helm S3 bucket's AWS
// region dynamically thus allowing the mixed use of buckets residing in
// different regions without requiring manual updates on the HELM_S3_REGION,
// AWS_REGION, or AWS_DEFAULT_REGION environment variables.
//
// This HEAD bucket solution works with all kinds of S3 URIs containing
// the bucket name in the host part.
//
// The basic idea behind the HEAD bucket solution and the "official
// confirmation" this behavior is expected and supported came from a comment on
// the AWS SDK Go repository:
// https://github.com/aws/aws-sdk-go/issues/720#issuecomment-243891223
func DynamicBucketRegion(s3URL string) SessionOption {
return func(options *session.Options) {
parsedS3URL, err := url.Parse(s3URL)
if err != nil {
return
}

// Note: The dummy credentials are required in case no other credential
// provider is found, but even if the HEAD bucket request fails and
// returns a non-200 status code indicating no access to the bucket, the
// actual bucket region is returned in a response header.
//
// Note: A signing region **MUST** be configured, otherwise the signed
// request fails. The configured region itself is irrelevant, the
// endpoint officially works and returns the bucket region in a response
// header regardless of whether the signing region matches the bucket's
// region.
//
// Note: The default S3 endpoint **MUST** be configured to avoid making
// the request region specific thus avoiding regional redirect responses
// (301 Permanently moved) on HEAD bucket. This setting is only required
// because any other region than "us-east-1" would configure a
// region-specific endpoint as well, so it's more safe to explicitly
// configure the default endpoint.
//
// Source:
// https://github.com/aws/aws-sdk-go/issues/720#issuecomment-243891223
configuration := aws.NewConfig().
WithCredentials(credentials.NewStaticCredentials("dummy", "dummy", "")).
WithRegion("us-east-1").
WithEndpoint("s3.amazonaws.com")
session := session.Must(session.NewSession())
s3Client := s3.New(session, configuration)

bucketRegionHeader := "X-Amz-Bucket-Region"
input := &s3.HeadBucketInput{
Bucket: aws.String(parsedS3URL.Host),
}
request, _ := s3Client.HeadBucketRequest(input)
_ = request.Send()
if request.HTTPResponse == nil ||
len(request.HTTPResponse.Header[bucketRegionHeader]) == 0 {
return
}

options.Config.Region = aws.String(request.HTTPResponse.Header[bucketRegionHeader][0])
}
}

// Session returns an AWS session as described http://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html
func Session(opts ...SessionOption) (*session.Session, error) {
disableSSL := false
Expand Down
51 changes: 51 additions & 0 deletions internal/awsutil/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,59 @@ package awsutil
import (
"os"
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/stretchr/testify/require"
)

func TestDynamicBucketRegion(t *testing.T) { // nolint:paralleltest // Note: forcing sequential run because of log output manipulation.
defaultSession, err := Session()
require.NoError(t, err)
defaultRegion := aws.StringValue(defaultSession.Config.Region)

testCases := []struct {
caseDescription string
expectedBucketRegion string
inputS3URL string
}{
{
caseDescription: "existing S3 bucket URL with host only (no key) -> success",
expectedBucketRegion: "eu-central-1",
inputS3URL: "s3://eu-test-bucket",
},
{
caseDescription: "existing S3 bucket URL with key -> success",
expectedBucketRegion: "ap-southeast-2",
inputS3URL: "s3://cn-test-bucket/charts/chart-0.1.2.tgz",
},
{
caseDescription: "invalid URL -> failing URI parsing, no effect (default region)",
expectedBucketRegion: defaultRegion,
inputS3URL: "://not/a/URL",
},
{
caseDescription: "invalid S3 URL -> failing request, no effect (default region)",
expectedBucketRegion: defaultRegion,
inputS3URL: "",
},
{
caseDescription: "not existing S3 URL -> no region header, no effect (default region)",
expectedBucketRegion: defaultRegion,
inputS3URL: "s3://not-an-s3-bucket-url",
},
}

for _, testCase := range testCases { // nolint:paralleltest // Note: forcing sequential run because of log output manipulation.
testCase := testCase

t.Run(testCase.caseDescription, func(t *testing.T) {
actualSession, err := Session(DynamicBucketRegion(testCase.inputS3URL))
require.NoError(t, err)
require.Equal(t, testCase.expectedBucketRegion, aws.StringValue(actualSession.Config.Region))
})
}
}

func TestSessionWithCustomEndpoint(t *testing.T) {
os.Setenv("AWS_ENDPOINT", "foobar:1234")
os.Setenv("AWS_DISABLE_SSL", "true")
Expand Down

0 comments on commit 35d06fe

Please sign in to comment.