Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring Config loading, replacing the need for a Session. #4

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions aws/credentials/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,16 @@ type Value struct {
SessionToken string

// Provider used to get credentials
// TODO this should be named: Source
ProviderName string
}

// Valid returns if the credentials are valid and can be used to sign
// a AWS v4 request.
func (v Value) Valid() bool {
return len(v.AccessKeyID) > 0 && len(v.SecretAccessKey) > 0
}

// A Provider is the interface for any component which will provide credentials
// Value. A provider is required to manage its own Expired state, and what to
// be expired means.
Expand Down
13 changes: 7 additions & 6 deletions aws/credentials/static_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ var (
// A StaticProvider is a set of credentials which are set programmatically,
// and will never expire.
type StaticProvider struct {
Value
Value Value
}

// NewStaticCredentials returns a pointer to a new Credentials object
Expand All @@ -38,20 +38,21 @@ func NewStaticCredentialsFromCreds(creds Value) *Credentials {
}

// Retrieve returns the credentials or error if the credentials are invalid.
func (s *StaticProvider) Retrieve() (Value, error) {
if s.AccessKeyID == "" || s.SecretAccessKey == "" {
func (s StaticProvider) Retrieve() (Value, error) {
v := s.Value
if v.AccessKeyID == "" || v.SecretAccessKey == "" {
return Value{ProviderName: StaticProviderName}, ErrStaticCredentialsEmpty
}

if len(s.Value.ProviderName) == 0 {
s.Value.ProviderName = StaticProviderName
if len(v.ProviderName) == 0 {
v.ProviderName = StaticProviderName
}
return s.Value, nil
}

// IsExpired returns if the credentials are expired.
//
// For StaticProvider, the credentials never expired.
func (s *StaticProvider) IsExpired() bool {
func (s StaticProvider) IsExpired() bool {
return false
}
21 changes: 21 additions & 0 deletions aws/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
// instead. This package is useful when you need to reset the defaults
// of a session or service client to the SDK defaults before setting
// additional parameters.
//
// TODO rename to "default"
package defaults

import (
"fmt"
"log"
"net/http"
"net/url"
"os"
Expand Down Expand Up @@ -43,6 +46,24 @@ func Get() Defaults {
}
}

// Logger returns a Logger which will write log messages to stdout, and
// use same formatting runes as the stdlib log.Logger
func Logger() aws.Logger {
return &defaultLogger{
logger: log.New(os.Stdout, "", log.LstdFlags),
}
}

// A defaultLogger provides a minimalistic logger satisfying the Logger interface.
type defaultLogger struct {
logger *log.Logger
}

// Log logs the parameters to the stdlib logger. See log.Println.
func (l defaultLogger) Log(args ...interface{}) {
l.logger.Println(args...)
}

// Config returns the default configuration without credentials.
// To retrieve a config with credentials also included use
// `defaults.Get().Config` instead.
Expand Down
148 changes: 148 additions & 0 deletions aws/external/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package external

import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/credentials"
)

// A Config represents a generic configuration value or set of values. This type
// will be used by the AWSConfigResolvers to extract
//
// General the Config type will use type assertion against the Provider interfaces
// to extract specific data from the Config.
type Config interface{}

// A ConfigLoader is used to load external configuration data and returns it as
// a generic Config type.
//
// The loader should return an error if it fails to load the external configuration
// or the configuration data is malformed, or required components missing.
type ConfigLoader func(Configs) (Config, error)

// An AWSConfigResolver will extract configuration data from the Configs slice
// using the provider interfaces to extract specific functionality. The extracted
// configuration values will be written to the AWS Config value.
//
// The resolver should return an error if it it fails to extract the data, the
// data is malformed, or incomplete.
type AWSConfigResolver func(cfg *aws.Config, configs Configs) error

// Configs is a slice of Config values. These values will be used by the
// AWSConfigResolvers to extract external configuration values to populate the
// AWS Config type.
//
// Use AppendFromLoaders to add additional external Config values that are
// loaded from external sources.
//
// Use ResolveAWSConfig after external Config values have been added or loaded
// to extract the loaded configuration values into the AWS Config.
type Configs []Config

// AppendFromLoaders iterates over the slice of loaders passed in calling each
// loader function in order. The external config value returned by the loader
// will be added to the returned Configs slice.
//
// If a loader returns an error this method will stop iterating and return
// that error.
func (cs Configs) AppendFromLoaders(loaders []ConfigLoader) (Configs, error) {
for _, fn := range loaders {
cfg, err := fn(cs)
if err != nil {
return nil, err
}

cs = append(cs, cfg)
}

return cs, nil
}

// ResolveAWSConfig returns a AWS configuration populated with values by calling
// the resolvers slice passed in. Each resolver is called in order. Any resolver
// may overwrite the AWs Configuration value of a previous resolver.
//
// If an resolver returns an error this method will return that error, and stop
// iterating over the resolvers.
func (cs Configs) ResolveAWSConfig(resolvers []AWSConfigResolver) (aws.Config, error) {
var cfg aws.Config

for _, fn := range resolvers {
if err := fn(&cfg, cs); err != nil {
// TODO provide better error?
return aws.Config{}, err
}
}

return cfg, nil
}

// DefaultConfigLoaders are a slice of functions that will read external configuration
// sources for configuration values. These values are read by the AWSConfigResolvers
// using interfaces to extract specific information from the external configuration.
var DefaultConfigLoaders = []ConfigLoader{
LoadEnvConfig,
LoadSharedConfig,
}

// DefaultAWSConfigResolvers are a slice of functions that will resolve external
// configuration values into AWS configuration values.
//
// This will setup the AWS configuration's Region,
var DefaultAWSConfigResolvers = []AWSConfigResolver{
ResolveDefaultAWSConfig,
ResolveCustomCABundle,
ResolveRegion,
ResolveCredentialsValue,
ResolveEndpointCredentials,
ResolveAssumeRoleCredentials,
ResolveFallbackEC2Credentials,
}

// LoadDefaultAWSConfig reads the SDK's default external configurations, and
// populates an AWS Config with the values from the external configurations.
//
// An optional variadic set of additional Config values can be provided as input
// that will be prepended to the Configs slice. Use this to add custom configuration.
// The custom configurations must satisfy the respective providers for their data
// or the custom data will be ignored by the resolvers and config loaders.
//
// cfg, err := external.LoadDefaultAWSConfig(
// WithSharedConfigProfile("test-profile"),
// )
// if err != nil {
// panic(fmt.Sprintf("failed loading config, %v", err))
// }
//
//
// The default configuration sources are:
// * Environment Variables
// * Shared Configuration and Shared Credentials files.
func LoadDefaultAWSConfig(configs ...Config) (aws.Config, error) {
var cfgs Configs
cfgs = append(cfgs, configs...)

cfgs, err := cfgs.AppendFromLoaders(DefaultConfigLoaders)
if err != nil {
return aws.Config{}, err
}

return cfgs.ResolveAWSConfig(DefaultAWSConfigResolvers)
}

// WithRegion provides wrapping of a region string to satisfy the RegionProvider
// interface.
type WithRegion string

// GetRegion returns the region string.
func (v WithRegion) GetRegion() (string, error) {
return string(v), nil
}

// WithCredentialsValue provides wrapping of a credentials Value to satisfy the
// CredentialsValueProvider interface.
type WithCredentialsValue credentials.Value

// GetCredentialsValue returns the credentials value.
func (v WithCredentialsValue) GetCredentialsValue() (credentials.Value, error) {
return credentials.Value(v), nil
}
104 changes: 104 additions & 0 deletions aws/external/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package external

import (
"reflect"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/credentials"
)

func TestConfigs_SharedConfigOptions(t *testing.T) {
_, err := Configs{
StaticSharedConfigProfile("profile-name"),
StaticSharedConfigFiles([]string{"creds-file"}),
}.AppendFromLoaders([]ConfigLoader{
func(configs Configs) (Config, error) {
var profile string
var files []string
var err error

for _, cfg := range configs {
if p, ok := cfg.(SharedConfigProfileProvider); ok {
profile, err = p.GetSharedConfigProfile()
if err != nil {
return nil, err
}
}
if p, ok := cfg.(SharedConfigFilesProvider); ok {
files, err = p.GetSharedConfigFiles()
if err != nil {
return nil, err
}
}

}

if e, a := "profile-name", profile; e != a {
t.Errorf("expect %v profile, got %v", e, a)
}
if e, a := []string{"creds-file"}, files; !reflect.DeepEqual(e, a) {
t.Errorf("expect %v files, got %v", e, a)
}

return nil, nil
},
})

if err != nil {
t.Fatalf("expect no error, got %v", err)
}
}

func TestConfigs_AppendFromLoaders(t *testing.T) {
expectCfg := WithRegion("mock-region")

cfgs, err := Configs{}.AppendFromLoaders([]ConfigLoader{
func(configs Configs) (Config, error) {
if e, a := 0, len(configs); e != a {
t.Errorf("expect %v configs, got %v", e, a)
}
return expectCfg, nil
},
})

if err != nil {
t.Fatalf("expect no error, got %v", err)
}

if e, a := 1, len(cfgs); e != a {
t.Errorf("expect %v configs, got %v", e, a)
}

if e, a := expectCfg, cfgs[0]; e != a {
t.Errorf("expect %v config, got %v", e, a)
}
}

func TestConfigs_ResolveAWSConfig(t *testing.T) {
cfg, err := Configs{
WithRegion("mock-region"),
WithCredentialsValue(credentials.Value{
AccessKeyID: "AKID", SecretAccessKey: "SECRET",
ProviderName: "provider",
}),
}.ResolveAWSConfig([]AWSConfigResolver{
ResolveRegion,
ResolveCredentialsValue,
})
if err != nil {
t.Fatalf("expect no error, got %v", err)
}

if e, a := "mock-region", aws.StringValue(cfg.Region); e != a {
t.Errorf("expect %v region, got %v", e, a)
}

creds, err := cfg.Credentials.Get()
if err != nil {
t.Fatalf("expect no error, got %v", err)
}
if e, a := "provider", creds.ProviderName; e != a {
t.Errorf("expect %v provider, got %v", e, a)
}
}
Loading