mirror of https://github.com/tidwall/tile38.git
304 lines
7.0 KiB
Go
304 lines
7.0 KiB
Go
package endpoints
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type partitions []partition
|
|
|
|
func (ps partitions) EndpointFor(service, region string, opts ...func(*Options)) (ResolvedEndpoint, error) {
|
|
var opt Options
|
|
opt.Set(opts...)
|
|
|
|
for i := 0; i < len(ps); i++ {
|
|
if !ps[i].canResolveEndpoint(service, region, opt.StrictMatching) {
|
|
continue
|
|
}
|
|
|
|
return ps[i].EndpointFor(service, region, opts...)
|
|
}
|
|
|
|
// If loose matching fallback to first partition format to use
|
|
// when resolving the endpoint.
|
|
if !opt.StrictMatching && len(ps) > 0 {
|
|
return ps[0].EndpointFor(service, region, opts...)
|
|
}
|
|
|
|
return ResolvedEndpoint{}, NewUnknownEndpointError("all partitions", service, region, []string{})
|
|
}
|
|
|
|
// Partitions satisfies the EnumPartitions interface and returns a list
|
|
// of Partitions representing each partition represented in the SDK's
|
|
// endpoints model.
|
|
func (ps partitions) Partitions() []Partition {
|
|
parts := make([]Partition, 0, len(ps))
|
|
for i := 0; i < len(ps); i++ {
|
|
parts = append(parts, ps[i].Partition())
|
|
}
|
|
|
|
return parts
|
|
}
|
|
|
|
type partition struct {
|
|
ID string `json:"partition"`
|
|
Name string `json:"partitionName"`
|
|
DNSSuffix string `json:"dnsSuffix"`
|
|
RegionRegex regionRegex `json:"regionRegex"`
|
|
Defaults endpoint `json:"defaults"`
|
|
Regions regions `json:"regions"`
|
|
Services services `json:"services"`
|
|
}
|
|
|
|
func (p partition) Partition() Partition {
|
|
return Partition{
|
|
id: p.ID,
|
|
p: &p,
|
|
}
|
|
}
|
|
|
|
func (p partition) canResolveEndpoint(service, region string, strictMatch bool) bool {
|
|
s, hasService := p.Services[service]
|
|
_, hasEndpoint := s.Endpoints[region]
|
|
|
|
if hasEndpoint && hasService {
|
|
return true
|
|
}
|
|
|
|
if strictMatch {
|
|
return false
|
|
}
|
|
|
|
return p.RegionRegex.MatchString(region)
|
|
}
|
|
|
|
func (p partition) EndpointFor(service, region string, opts ...func(*Options)) (resolved ResolvedEndpoint, err error) {
|
|
var opt Options
|
|
opt.Set(opts...)
|
|
|
|
s, hasService := p.Services[service]
|
|
if !(hasService || opt.ResolveUnknownService) {
|
|
// Only return error if the resolver will not fallback to creating
|
|
// endpoint based on service endpoint ID passed in.
|
|
return resolved, NewUnknownServiceError(p.ID, service, serviceList(p.Services))
|
|
}
|
|
|
|
e, hasEndpoint := s.endpointForRegion(region)
|
|
if !hasEndpoint && opt.StrictMatching {
|
|
return resolved, NewUnknownEndpointError(p.ID, service, region, endpointList(s.Endpoints))
|
|
}
|
|
|
|
defs := []endpoint{p.Defaults, s.Defaults}
|
|
return e.resolve(service, region, p.DNSSuffix, defs, opt), nil
|
|
}
|
|
|
|
func serviceList(ss services) []string {
|
|
list := make([]string, 0, len(ss))
|
|
for k := range ss {
|
|
list = append(list, k)
|
|
}
|
|
return list
|
|
}
|
|
func endpointList(es endpoints) []string {
|
|
list := make([]string, 0, len(es))
|
|
for k := range es {
|
|
list = append(list, k)
|
|
}
|
|
return list
|
|
}
|
|
|
|
type regionRegex struct {
|
|
*regexp.Regexp
|
|
}
|
|
|
|
func (rr *regionRegex) UnmarshalJSON(b []byte) (err error) {
|
|
// Strip leading and trailing quotes
|
|
regex, err := strconv.Unquote(string(b))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to strip quotes from regex, %v", err)
|
|
}
|
|
|
|
rr.Regexp, err = regexp.Compile(regex)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to unmarshal region regex, %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type regions map[string]region
|
|
|
|
type region struct {
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
type services map[string]service
|
|
|
|
type service struct {
|
|
PartitionEndpoint string `json:"partitionEndpoint"`
|
|
IsRegionalized boxedBool `json:"isRegionalized,omitempty"`
|
|
Defaults endpoint `json:"defaults"`
|
|
Endpoints endpoints `json:"endpoints"`
|
|
}
|
|
|
|
func (s *service) endpointForRegion(region string) (endpoint, bool) {
|
|
if s.IsRegionalized == boxedFalse {
|
|
return s.Endpoints[s.PartitionEndpoint], region == s.PartitionEndpoint
|
|
}
|
|
|
|
if e, ok := s.Endpoints[region]; ok {
|
|
return e, true
|
|
}
|
|
|
|
// Unable to find any matching endpoint, return
|
|
// blank that will be used for generic endpoint creation.
|
|
return endpoint{}, false
|
|
}
|
|
|
|
type endpoints map[string]endpoint
|
|
|
|
type endpoint struct {
|
|
Hostname string `json:"hostname"`
|
|
Protocols []string `json:"protocols"`
|
|
CredentialScope credentialScope `json:"credentialScope"`
|
|
|
|
// Custom fields not modeled
|
|
HasDualStack boxedBool `json:"-"`
|
|
DualStackHostname string `json:"-"`
|
|
|
|
// Signature Version not used
|
|
SignatureVersions []string `json:"signatureVersions"`
|
|
|
|
// SSLCommonName not used.
|
|
SSLCommonName string `json:"sslCommonName"`
|
|
}
|
|
|
|
const (
|
|
defaultProtocol = "https"
|
|
defaultSigner = "v4"
|
|
)
|
|
|
|
var (
|
|
protocolPriority = []string{"https", "http"}
|
|
signerPriority = []string{"v4", "v2"}
|
|
)
|
|
|
|
func getByPriority(s []string, p []string, def string) string {
|
|
if len(s) == 0 {
|
|
return def
|
|
}
|
|
|
|
for i := 0; i < len(p); i++ {
|
|
for j := 0; j < len(s); j++ {
|
|
if s[j] == p[i] {
|
|
return s[j]
|
|
}
|
|
}
|
|
}
|
|
|
|
return s[0]
|
|
}
|
|
|
|
func (e endpoint) resolve(service, region, dnsSuffix string, defs []endpoint, opts Options) ResolvedEndpoint {
|
|
var merged endpoint
|
|
for _, def := range defs {
|
|
merged.mergeIn(def)
|
|
}
|
|
merged.mergeIn(e)
|
|
e = merged
|
|
|
|
hostname := e.Hostname
|
|
|
|
// Offset the hostname for dualstack if enabled
|
|
if opts.UseDualStack && e.HasDualStack == boxedTrue {
|
|
hostname = e.DualStackHostname
|
|
}
|
|
|
|
u := strings.Replace(hostname, "{service}", service, 1)
|
|
u = strings.Replace(u, "{region}", region, 1)
|
|
u = strings.Replace(u, "{dnsSuffix}", dnsSuffix, 1)
|
|
|
|
scheme := getEndpointScheme(e.Protocols, opts.DisableSSL)
|
|
u = fmt.Sprintf("%s://%s", scheme, u)
|
|
|
|
signingRegion := e.CredentialScope.Region
|
|
if len(signingRegion) == 0 {
|
|
signingRegion = region
|
|
}
|
|
signingName := e.CredentialScope.Service
|
|
if len(signingName) == 0 {
|
|
signingName = service
|
|
}
|
|
|
|
return ResolvedEndpoint{
|
|
URL: u,
|
|
SigningRegion: signingRegion,
|
|
SigningName: signingName,
|
|
SigningMethod: getByPriority(e.SignatureVersions, signerPriority, defaultSigner),
|
|
}
|
|
}
|
|
|
|
func getEndpointScheme(protocols []string, disableSSL bool) string {
|
|
if disableSSL {
|
|
return "http"
|
|
}
|
|
|
|
return getByPriority(protocols, protocolPriority, defaultProtocol)
|
|
}
|
|
|
|
func (e *endpoint) mergeIn(other endpoint) {
|
|
if len(other.Hostname) > 0 {
|
|
e.Hostname = other.Hostname
|
|
}
|
|
if len(other.Protocols) > 0 {
|
|
e.Protocols = other.Protocols
|
|
}
|
|
if len(other.SignatureVersions) > 0 {
|
|
e.SignatureVersions = other.SignatureVersions
|
|
}
|
|
if len(other.CredentialScope.Region) > 0 {
|
|
e.CredentialScope.Region = other.CredentialScope.Region
|
|
}
|
|
if len(other.CredentialScope.Service) > 0 {
|
|
e.CredentialScope.Service = other.CredentialScope.Service
|
|
}
|
|
if len(other.SSLCommonName) > 0 {
|
|
e.SSLCommonName = other.SSLCommonName
|
|
}
|
|
if other.HasDualStack != boxedBoolUnset {
|
|
e.HasDualStack = other.HasDualStack
|
|
}
|
|
if len(other.DualStackHostname) > 0 {
|
|
e.DualStackHostname = other.DualStackHostname
|
|
}
|
|
}
|
|
|
|
type credentialScope struct {
|
|
Region string `json:"region"`
|
|
Service string `json:"service"`
|
|
}
|
|
|
|
type boxedBool int
|
|
|
|
func (b *boxedBool) UnmarshalJSON(buf []byte) error {
|
|
v, err := strconv.ParseBool(string(buf))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if v {
|
|
*b = boxedTrue
|
|
} else {
|
|
*b = boxedFalse
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
boxedBoolUnset boxedBool = iota
|
|
boxedFalse
|
|
boxedTrue
|
|
)
|