Skip to content

Commit

Permalink
add loadoptions to configure BaseEndpoint (#2837)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucix-aws authored Oct 15, 2024
1 parent 8ed647a commit 3cc4661
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .changelog/024e7efafa274001b8677eb90bd32260.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": "024e7efa-fa27-4001-b867-7eb90bd32260",
"type": "feature",
"description": "Adds the LoadOptions hook `WithBaseEndpoint` for setting global endpoint override in-code.",
"modules": [
"config"
]
}
33 changes: 33 additions & 0 deletions config/load_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ type LoadOptions struct {
S3DisableExpressAuth *bool

AccountIDEndpointMode aws.AccountIDEndpointMode

// Service endpoint override. This value is not necessarily final and is
// passed to the service's EndpointResolverV2 for further delegation.
BaseEndpoint string
}

func (o LoadOptions) getDefaultsMode(ctx context.Context) (aws.DefaultsMode, bool, error) {
Expand Down Expand Up @@ -284,6 +288,19 @@ func (o LoadOptions) getAccountIDEndpointMode(ctx context.Context) (aws.AccountI
return o.AccountIDEndpointMode, len(o.AccountIDEndpointMode) > 0, nil
}

func (o LoadOptions) getBaseEndpoint(context.Context) (string, bool, error) {
return o.BaseEndpoint, o.BaseEndpoint != "", nil
}

// GetServiceBaseEndpoint satisfies (internal/configsources).ServiceBaseEndpointProvider.
//
// The sdkID value is unused because LoadOptions only supports setting a GLOBAL
// endpoint override. In-code, per-service endpoint overrides are performed via
// functional options in service client space.
func (o LoadOptions) GetServiceBaseEndpoint(context.Context, string) (string, bool, error) {
return o.BaseEndpoint, o.BaseEndpoint != "", nil
}

// WithRegion is a helper function to construct functional options
// that sets Region on config's LoadOptions. Setting the region to
// an empty string, will result in the region value being ignored.
Expand Down Expand Up @@ -1139,3 +1156,19 @@ func WithS3DisableExpressAuth(v bool) LoadOptionsFunc {
return nil
}
}

// WithBaseEndpoint is a helper function to construct functional options that
// sets BaseEndpoint on config's LoadOptions. Empty values have no effect, and
// subsequent calls to this API override previous ones.
//
// This is an in-code setting, therefore, any value set using this hook takes
// precedence over and will override ALL environment and shared config
// directives that set endpoint URLs. Functional options on service clients
// have higher specificity, and functional options that modify the value of
// BaseEndpoint on a client will take precedence over this setting.
func WithBaseEndpoint(v string) LoadOptionsFunc {
return func(o *LoadOptions) error {
o.BaseEndpoint = v
return nil
}
}
190 changes: 190 additions & 0 deletions service/internal/integrationtest/s3/endpoint_url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
//go:build integration
// +build integration

package s3

import (
"context"
"os"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
)

// From the SEP:
// Service-specific endpoint configuration MUST be resolved with an endpoint URL provider chain with the following precedence:
// - The value provided through code to an AWS SDK or tool via a command line
// parameter or a client or configuration constructor; for example the
// --endpoint-url command line parameter or the endpoint_url parameter
// provided to the Python SDK client.
// - The value provided by a service-specific environment variable.
// - The value provided by the global endpoint environment variable
// (AWS_ENDPOINT_URL).
// - The value provided by a service-specific parameter from a services
// definition section referenced in a profile in the shared configuration
// file.
// - The value provided by the global parameter from a profile in the shared
// configuration file.
// - The value resolved through the methods provided by the SDK or tool when
// no explicit endpoint URL is provided.

func TestInteg_EndpointURL(t *testing.T) {
for name, tt := range map[string]struct {
Env map[string]string
SharedConfig string
LoadOpts []func(*config.LoadOptions) error
ClientOpts []func(*s3.Options)
Expect string
}{
"no values": {
SharedConfig: `
[default]
`,
Expect: "",
},

"precedence 0: in-code, set via s3.Options": {
Env: map[string]string{
"AWS_ENDPOINT_URL": "https://global-env.com",
"AWS_ENDPOINT_URL_S3": "https://service-env.com",
},
SharedConfig: `
[default]
endpoint_url = https://global-cfg.com
services = service_cfg
[services service_cfg]
s3 =
endpoint_url = https://service-cfg.com
`,
LoadOpts: []func(*config.LoadOptions) error{
config.WithBaseEndpoint("https://loadopts.com"),
},
ClientOpts: []func(*s3.Options){
func(o *s3.Options) {
o.BaseEndpoint = aws.String("https://clientopts.com")
},
},
Expect: "https://clientopts.com",
},

"precedence 0: in-code, set via config.LoadOptions": {
Env: map[string]string{
"AWS_ENDPOINT_URL": "https://global-env.com",
"AWS_ENDPOINT_URL_S3": "https://service-env.com",
},
SharedConfig: `
[default]
endpoint_url = https://global-cfg.com
services = service_cfg
[services service_cfg]
s3 =
endpoint_url = https://service-cfg.com
`,
LoadOpts: []func(*config.LoadOptions) error{
config.WithBaseEndpoint("https://loadopts.com"),
},
Expect: "https://loadopts.com",
},

"precedence 1: service env": {
Env: map[string]string{
"AWS_ENDPOINT_URL": "https://global-env.com",
"AWS_ENDPOINT_URL_S3": "https://service-env.com",
},
SharedConfig: `
[default]
endpoint_url = https://global-cfg.com
services = service_cfg
[services service_cfg]
s3 =
endpoint_url = https://service-cfg.com
`,
Expect: "https://service-env.com",
},

"precedence 2: global env": {
Env: map[string]string{
"AWS_ENDPOINT_URL": "https://global-env.com",
},
SharedConfig: `
[default]
endpoint_url = https://global-cfg.com
services = service_cfg
[services service_cfg]
s3 =
endpoint_url = https://service-cfg.com
`,
Expect: "https://global-env.com",
},

"precedence 3: service cfg": {
SharedConfig: `
[default]
endpoint_url = https://global-cfg.com
services = service_cfg
[services service_cfg]
s3 =
endpoint_url = https://service-cfg.com
`,
Expect: "https://service-cfg.com",
},

"precedence 4: global cfg": {
SharedConfig: `
[default]
endpoint_url = https://global-cfg.com
`,
Expect: "https://global-cfg.com",
},
} {
t.Run(name, func(t *testing.T) {
reset, err := mockEnvironment(tt.Env, tt.SharedConfig)
if err != nil {
t.Fatalf("mock environment: %v", err)
}
defer reset()

loadopts := append(tt.LoadOpts,
config.WithSharedConfigFiles([]string{"test_shared_config"}))
cfg, err := config.LoadDefaultConfig(context.Background(), loadopts...)
if err != nil {
t.Fatalf("load config: %v", err)
}

svc := s3.NewFromConfig(cfg, tt.ClientOpts...)
actual := aws.ToString(svc.Options().BaseEndpoint)
if tt.Expect != actual {
t.Errorf("expect endpoint: %q != %q", tt.Expect, actual)
}
})
}
}

func mockEnvironment(env map[string]string, sharedCfg string) (func(), error) {
for k, v := range env {
os.Setenv(k, v)
}
f, err := os.Create("test_shared_config")
if err != nil {
return nil, err
}
if _, err := f.Write([]byte(sharedCfg)); err != nil {
return nil, err
}

return func() {
for k := range env {
os.Unsetenv(k)
}
if err := os.Remove("test_shared_config"); err != nil {
panic(err)
}
}, nil
}

0 comments on commit 3cc4661

Please sign in to comment.