Skip to content

Commit

Permalink
aws/endpoints: Add endpoint metadata to SDK (aws#961)
Browse files Browse the repository at this point in the history
Adds Region and Endpoint metadata to the SDK. This allows you to enumerate regions and endpoint metadata based on a defined model embedded in the SDK.

This adds constants for each partition, region, and service for all Regions and Endpoints metadata. The metadata are embedded in the SDK, and is accessible as an Endpoint Resolver via `DefaultResolver`. To enumerate over the partition data case the response of EndpointResolver to a `EnumPartitions` and calls Partitions.
  • Loading branch information
jasdel committed Dec 5, 2016
1 parent 2b30c0d commit 173fcf0
Show file tree
Hide file tree
Showing 128 changed files with 6,343 additions and 691 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ gen-protocol-test:
go generate ./private/protocol/...

gen-endpoints:
go generate ./private/endpoints
go generate ./models/endpoints/

build:
@echo "go build SDK and vendor packages"
Expand Down
8 changes: 5 additions & 3 deletions aws/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import (

// A Config provides configuration to a service client instance.
type Config struct {
Config *aws.Config
Handlers request.Handlers
Endpoint, SigningRegion string
Config *aws.Config
Handlers request.Handlers
Endpoint string
SigningRegion string
SigningName string
}

// ConfigProvider provides a generic way for a service client to receive
Expand Down
16 changes: 16 additions & 0 deletions aws/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"time"

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

// UseServiceDefaultRetries instructs the config to use the service's own
Expand Down Expand Up @@ -48,6 +49,10 @@ type Config struct {
// endpoint for a client.
Endpoint *string

// The resolver to use for looking up endpoints for AWS service clients
// to use based on region.
EndpointResolver endpoints.Resolver

// The region to send requests to. This parameter is required and must
// be configured globally or on a per-client basis unless otherwise
// noted. A full list of regions is found in the "Regions and Endpoints"
Expand Down Expand Up @@ -235,6 +240,13 @@ func (c *Config) WithEndpoint(endpoint string) *Config {
return c
}

// WithEndpointResolver sets a config EndpointResolver value returning a
// Config pointer for chaining.
func (c *Config) WithEndpointResolver(resolver endpoints.Resolver) *Config {
c.EndpointResolver = resolver
return c
}

// WithRegion sets a config Region value returning a Config pointer for
// chaining.
func (c *Config) WithRegion(region string) *Config {
Expand Down Expand Up @@ -357,6 +369,10 @@ func mergeInConfig(dst *Config, other *Config) {
dst.Endpoint = other.Endpoint
}

if other.EndpointResolver != nil {
dst.EndpointResolver = other.EndpointResolver
}

if other.Region != nil {
dst.Region = other.Region
}
Expand Down
14 changes: 9 additions & 5 deletions aws/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import (
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/credentials/endpointcreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/private/endpoints"
)

// A Defaults provides a collection of default values for SDK clients.
Expand Down Expand Up @@ -56,7 +56,8 @@ func Config() *aws.Config {
WithMaxRetries(aws.UseServiceDefaultRetries).
WithLogger(aws.NewDefaultLogger()).
WithLogLevel(aws.LogOff).
WithSleepDelay(time.Sleep)
WithSleepDelay(time.Sleep).
WithEndpointResolver(endpoints.DefaultResolver())
}

// Handlers returns the default request handlers.
Expand Down Expand Up @@ -120,11 +121,14 @@ func ecsCredProvider(cfg aws.Config, handlers request.Handlers, uri string) cred
}

func ec2RoleProvider(cfg aws.Config, handlers request.Handlers) credentials.Provider {
endpoint, signingRegion := endpoints.EndpointForRegion(ec2metadata.ServiceName,
aws.StringValue(cfg.Region), true, false)
resolver := cfg.EndpointResolver
if resolver == nil {
resolver = endpoints.DefaultResolver()
}

e, _ := resolver.EndpointFor(endpoints.Ec2metadataServiceID, "")
return &ec2rolecreds.EC2RoleProvider{
Client: ec2metadata.NewClient(cfg, handlers, endpoint, signingRegion),
Client: ec2metadata.NewClient(cfg, handlers, e.URL, e.SigningRegion),
ExpiryWindow: 5 * time.Minute,
}
}
5 changes: 5 additions & 0 deletions aws/defaults/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,9 @@ func TestDefaultEC2RoleProvider(t *testing.T) {
ec2Provider, ok := provider.(*ec2rolecreds.EC2RoleProvider)
assert.NotNil(t, ec2Provider)
assert.True(t, ok)

fmt.Println(ec2Provider.Client.Endpoint)

assert.Equal(t, fmt.Sprintf("http://169.254.169.254/latest"),
ec2Provider.Client.Endpoint)
}
133 changes: 133 additions & 0 deletions aws/endpoints/decode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package endpoints

import (
"encoding/json"
"fmt"
"io"

"github.com/aws/aws-sdk-go/aws/awserr"
)

type modelDefinition map[string]json.RawMessage

// A DecodeModelOptions are the options for how the endpoints model definition
// are decoded.
type DecodeModelOptions struct {
SkipCustomizations bool
}

// Set combines all of the option functions together.
func (d *DecodeModelOptions) Set(optFns ...func(*DecodeModelOptions)) {
for _, fn := range optFns {
fn(d)
}
}

// DecodeModel unmarshals a Regions and Endpoint model definition file into
// a endpoint Resolver. If the file format is not supported, or an error occurs
// when unmarshaling the model an error will be returned.
//
// Casting the return value of this func to a EnumPartitions will
// allow you to get a list of the partitions in the order the endpoints
// will be resolved in.
//
// resolver, err := endpoints.DecodeModel(reader)
//
// partitions := resolver.(endpoints.EnumPartitions).Partitions()
// for _, p := range partitions {
// // ... inspect partitions
// }
func DecodeModel(r io.Reader, optFns ...func(*DecodeModelOptions)) (Resolver, error) {
var opts DecodeModelOptions
opts.Set(optFns...)

// Get the version of the partition file to determine what
// unmarshaling model to use.
modelDef := modelDefinition{}
if err := json.NewDecoder(r).Decode(&modelDef); err != nil {
return nil, newDecodeModelError("failed to decode endpoints model", err)
}

var version string
if b, ok := modelDef["version"]; ok {
version = string(b)
} else {
return nil, newDecodeModelError("endpoints version not found in model", nil)
}

if version == "3" {
return decodeV3Endpoints(modelDef, opts)
}

return nil, newDecodeModelError(
fmt.Sprintf("endpoints version %s, not supported", version), nil)
}

func decodeV3Endpoints(modelDef modelDefinition, opts DecodeModelOptions) (Resolver, error) {
b, ok := modelDef["partitions"]
if !ok {
return nil, newDecodeModelError("endpoints model missing partitions", nil)
}

ps := partitions{}
if err := json.Unmarshal(b, &ps); err != nil {
return nil, newDecodeModelError("failed to decode endpoints model", err)
}

if opts.SkipCustomizations {
return ps, nil
}

// Customization
for i := 0; i < len(ps); i++ {
p := &ps[i]
custAddEC2Metadata(p)
custAddS3DualStack(p)
custRmIotDataService(p)
}

return ps, nil
}

func custAddS3DualStack(p *partition) {
if p.ID != "aws" {
return
}

s, ok := p.Services["s3"]
if !ok {
return
}

s.Defaults.HasDualStack = boxedTrue
s.Defaults.DualStackHostname = "{service}.dualstack.{region}.{dnsSuffix}"

p.Services["s3"] = s
}

func custAddEC2Metadata(p *partition) {
p.Services["ec2metadata"] = service{
IsRegionalized: boxedFalse,
PartitionEndpoint: "aws-global",
Endpoints: endpoints{
"aws-global": endpoint{
Hostname: "169.254.169.254/latest",
Protocols: []string{"http"},
},
},
}
}

func custRmIotDataService(p *partition) {
delete(p.Services, "data.iot")
}

type decodeModelError struct {
awsError
}

func newDecodeModelError(msg string, err error) decodeModelError {
return decodeModelError{
awsError: awserr.New("DecodeEndpointsModelError", msg, err),
}
}
117 changes: 117 additions & 0 deletions aws/endpoints/decode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package endpoints

import (
"strings"
"testing"
)

func TestDecodeEndpoints_V3(t *testing.T) {
const v3Doc = `
{
"version": 3,
"partitions": [
{
"defaults": {
"hostname": "{service}.{region}.{dnsSuffix}",
"protocols": [
"https"
],
"signatureVersions": [
"v4"
]
},
"dnsSuffix": "amazonaws.com",
"partition": "aws",
"partitionName": "AWS Standard",
"regionRegex": "^(us|eu|ap|sa|ca)\\-\\w+\\-\\d+$",
"regions": {
"ap-northeast-1": {
"description": "Asia Pacific (Tokyo)"
}
},
"services": {
"acm": {
"endpoints": {
"ap-northeast-1": {}
}
},
"s3": {
"endpoints": {
"ap-northeast-1": {}
}
}
}
}
]
}`

resolver, err := DecodeModel(strings.NewReader(v3Doc))
if err != nil {
t.Fatalf("expected no error, got %v", err)
}

endpoint, err := resolver.EndpointFor("acm", "ap-northeast-1")
if err != nil {
t.Fatalf("failed to resolve endpoint, %v", err)
}

if a, e := endpoint.URL, "https://acm.ap-northeast-1.amazonaws.com"; a != e {
t.Errorf("expected %q URL got %q", e, a)
}

p := resolver.(partitions)[0]

s3Defaults := p.Services["s3"].Defaults
if a, e := s3Defaults.HasDualStack, boxedTrue; a != e {
t.Errorf("expect s3 service to have dualstack enabled")
}
if a, e := s3Defaults.DualStackHostname, "{service}.dualstack.{region}.{dnsSuffix}"; a != e {
t.Errorf("expect s3 dualstack host pattern to be %q, got %q", e, a)
}

ec2metaEndpoint := p.Services["ec2metadata"].Endpoints["aws-global"]
if a, e := ec2metaEndpoint.Hostname, "169.254.169.254/latest"; a != e {
t.Errorf("expect ec2metadata host to be %q, got %q", e, a)
}
}

func TestDecodeEndpoints_NoPartitions(t *testing.T) {
const doc = `{ "version": 3 }`

resolver, err := DecodeModel(strings.NewReader(doc))
if err == nil {
t.Fatalf("expected error")
}

if resolver != nil {
t.Errorf("expect resolver to be nil")
}
}

func TestDecodeEndpoints_UnsupportedVersion(t *testing.T) {
const doc = `{ "version": 2 }`

resolver, err := DecodeModel(strings.NewReader(doc))
if err == nil {
t.Fatalf("expected error decoding model")
}

if resolver != nil {
t.Errorf("expect resolver to be nil")
}
}

func TestDecodeModelOptionsSet(t *testing.T) {
var actual DecodeModelOptions
actual.Set(func(o *DecodeModelOptions) {
o.SkipCustomizations = true
})

expect := DecodeModelOptions{
SkipCustomizations: true,
}

if actual != expect {
t.Errorf("expect %v options got %v", expect, actual)
}
}
Loading

0 comments on commit 173fcf0

Please sign in to comment.