diff --git a/CHANGELOG.md b/CHANGELOG.md index e8ab17bf..eeaede29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # v2.0.0 (Unreleased) +ENHANCEMENTS + +* Adds top-level `endpoints` package containing AWS partition and Region metadata ([#1176](https://github.com/hashicorp/aws-sdk-go-base/pull/1176)) + # v2.0.0-beta.56 (2024-09-11) ENHANCEMENTS diff --git a/aws_config.go b/aws_config.go index 069cf26b..eba1b817 100644 --- a/aws_config.go +++ b/aws_config.go @@ -24,9 +24,9 @@ import ( "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" "github.com/aws/smithy-go/middleware" "github.com/hashicorp/aws-sdk-go-base/v2/diag" + "github.com/hashicorp/aws-sdk-go-base/v2/endpoints" "github.com/hashicorp/aws-sdk-go-base/v2/internal/awsconfig" "github.com/hashicorp/aws-sdk-go-base/v2/internal/constants" - "github.com/hashicorp/aws-sdk-go-base/v2/internal/endpoints" "github.com/hashicorp/aws-sdk-go-base/v2/logging" "github.com/hashicorp/terraform-plugin-log/tflog" ) @@ -337,7 +337,9 @@ func GetAwsAccountIDAndPartition(ctx context.Context, awsConfig aws.Config, c *C "Errors: %w", err)) } - return "", endpoints.PartitionForRegion(awsConfig.Region), nil + partition, _ := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), awsConfig.Region) + + return "", partition.ID(), nil } func commonLoadOptions(ctx context.Context, c *Config) ([]func(*config.LoadOptions) error, error) { diff --git a/endpoints/endpoints.go b/endpoints/endpoints.go new file mode 100644 index 00000000..68a29284 --- /dev/null +++ b/endpoints/endpoints.go @@ -0,0 +1,8 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package endpoints + +const ( + AwsGlobalRegionID = "aws-global" // AWS Standard global region. +) diff --git a/endpoints/endpoints_gen.go b/endpoints/endpoints_gen.go new file mode 100644 index 00000000..ceb98e58 --- /dev/null +++ b/endpoints/endpoints_gen.go @@ -0,0 +1,282 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by internal/generate/endpoints/main.go; DO NOT EDIT. + +package endpoints + +import ( + "regexp" +) + +// All known partition IDs. +const ( + AwsPartitionID = "aws" // AWS Standard + AwsCnPartitionID = "aws-cn" // AWS China + AwsIsoPartitionID = "aws-iso" // AWS ISO (US) + AwsIsoBPartitionID = "aws-iso-b" // AWS ISOB (US) + AwsIsoEPartitionID = "aws-iso-e" // AWS ISOE (Europe) + AwsIsoFPartitionID = "aws-iso-f" // AWS ISOF + AwsUsGovPartitionID = "aws-us-gov" // AWS GovCloud (US) +) + +// All known Region IDs. +const ( + // AWS Standard partition's Regions. + AfSouth1RegionID = "af-south-1" // Africa (Cape Town) + ApEast1RegionID = "ap-east-1" // Asia Pacific (Hong Kong) + ApNortheast1RegionID = "ap-northeast-1" // Asia Pacific (Tokyo) + ApNortheast2RegionID = "ap-northeast-2" // Asia Pacific (Seoul) + ApNortheast3RegionID = "ap-northeast-3" // Asia Pacific (Osaka) + ApSouth1RegionID = "ap-south-1" // Asia Pacific (Mumbai) + ApSouth2RegionID = "ap-south-2" // Asia Pacific (Hyderabad) + ApSoutheast1RegionID = "ap-southeast-1" // Asia Pacific (Singapore) + ApSoutheast2RegionID = "ap-southeast-2" // Asia Pacific (Sydney) + ApSoutheast3RegionID = "ap-southeast-3" // Asia Pacific (Jakarta) + ApSoutheast4RegionID = "ap-southeast-4" // Asia Pacific (Melbourne) + ApSoutheast5RegionID = "ap-southeast-5" // Asia Pacific (Malaysia) + CaCentral1RegionID = "ca-central-1" // Canada (Central) + CaWest1RegionID = "ca-west-1" // Canada West (Calgary) + EuCentral1RegionID = "eu-central-1" // Europe (Frankfurt) + EuCentral2RegionID = "eu-central-2" // Europe (Zurich) + EuNorth1RegionID = "eu-north-1" // Europe (Stockholm) + EuSouth1RegionID = "eu-south-1" // Europe (Milan) + EuSouth2RegionID = "eu-south-2" // Europe (Spain) + EuWest1RegionID = "eu-west-1" // Europe (Ireland) + EuWest2RegionID = "eu-west-2" // Europe (London) + EuWest3RegionID = "eu-west-3" // Europe (Paris) + IlCentral1RegionID = "il-central-1" // Israel (Tel Aviv) + MeCentral1RegionID = "me-central-1" // Middle East (UAE) + MeSouth1RegionID = "me-south-1" // Middle East (Bahrain) + SaEast1RegionID = "sa-east-1" // South America (Sao Paulo) + UsEast1RegionID = "us-east-1" // US East (N. Virginia) + UsEast2RegionID = "us-east-2" // US East (Ohio) + UsWest1RegionID = "us-west-1" // US West (N. California) + UsWest2RegionID = "us-west-2" // US West (Oregon) + // AWS China partition's Regions. + CnNorth1RegionID = "cn-north-1" // China (Beijing) + CnNorthwest1RegionID = "cn-northwest-1" // China (Ningxia) + // AWS ISO (US) partition's Regions. + UsIsoEast1RegionID = "us-iso-east-1" // US ISO East + UsIsoWest1RegionID = "us-iso-west-1" // US ISO WEST + // AWS ISOB (US) partition's Regions. + UsIsobEast1RegionID = "us-isob-east-1" // US ISOB East (Ohio) + // AWS ISOE (Europe) partition's Regions. + EuIsoeWest1RegionID = "eu-isoe-west-1" // EU ISOE West + // AWS ISOF partition's Regions. + // AWS GovCloud (US) partition's Regions. + UsGovEast1RegionID = "us-gov-east-1" // AWS GovCloud (US-East) + UsGovWest1RegionID = "us-gov-west-1" // AWS GovCloud (US-West) +) + +var ( + partitions = map[string]Partition{ + AwsPartitionID: { + id: AwsPartitionID, + name: "AWS Standard", + dnsSuffix: "amazonaws.com", + regionRegex: regexp.MustCompile(`^(us|eu|ap|sa|ca|me|af|il|mx)\-\w+\-\d+$`), + regions: map[string]Region{ + AfSouth1RegionID: { + id: AfSouth1RegionID, + description: "Africa (Cape Town)", + }, + ApEast1RegionID: { + id: ApEast1RegionID, + description: "Asia Pacific (Hong Kong)", + }, + ApNortheast1RegionID: { + id: ApNortheast1RegionID, + description: "Asia Pacific (Tokyo)", + }, + ApNortheast2RegionID: { + id: ApNortheast2RegionID, + description: "Asia Pacific (Seoul)", + }, + ApNortheast3RegionID: { + id: ApNortheast3RegionID, + description: "Asia Pacific (Osaka)", + }, + ApSouth1RegionID: { + id: ApSouth1RegionID, + description: "Asia Pacific (Mumbai)", + }, + ApSouth2RegionID: { + id: ApSouth2RegionID, + description: "Asia Pacific (Hyderabad)", + }, + ApSoutheast1RegionID: { + id: ApSoutheast1RegionID, + description: "Asia Pacific (Singapore)", + }, + ApSoutheast2RegionID: { + id: ApSoutheast2RegionID, + description: "Asia Pacific (Sydney)", + }, + ApSoutheast3RegionID: { + id: ApSoutheast3RegionID, + description: "Asia Pacific (Jakarta)", + }, + ApSoutheast4RegionID: { + id: ApSoutheast4RegionID, + description: "Asia Pacific (Melbourne)", + }, + ApSoutheast5RegionID: { + id: ApSoutheast5RegionID, + description: "Asia Pacific (Malaysia)", + }, + CaCentral1RegionID: { + id: CaCentral1RegionID, + description: "Canada (Central)", + }, + CaWest1RegionID: { + id: CaWest1RegionID, + description: "Canada West (Calgary)", + }, + EuCentral1RegionID: { + id: EuCentral1RegionID, + description: "Europe (Frankfurt)", + }, + EuCentral2RegionID: { + id: EuCentral2RegionID, + description: "Europe (Zurich)", + }, + EuNorth1RegionID: { + id: EuNorth1RegionID, + description: "Europe (Stockholm)", + }, + EuSouth1RegionID: { + id: EuSouth1RegionID, + description: "Europe (Milan)", + }, + EuSouth2RegionID: { + id: EuSouth2RegionID, + description: "Europe (Spain)", + }, + EuWest1RegionID: { + id: EuWest1RegionID, + description: "Europe (Ireland)", + }, + EuWest2RegionID: { + id: EuWest2RegionID, + description: "Europe (London)", + }, + EuWest3RegionID: { + id: EuWest3RegionID, + description: "Europe (Paris)", + }, + IlCentral1RegionID: { + id: IlCentral1RegionID, + description: "Israel (Tel Aviv)", + }, + MeCentral1RegionID: { + id: MeCentral1RegionID, + description: "Middle East (UAE)", + }, + MeSouth1RegionID: { + id: MeSouth1RegionID, + description: "Middle East (Bahrain)", + }, + SaEast1RegionID: { + id: SaEast1RegionID, + description: "South America (Sao Paulo)", + }, + UsEast1RegionID: { + id: UsEast1RegionID, + description: "US East (N. Virginia)", + }, + UsEast2RegionID: { + id: UsEast2RegionID, + description: "US East (Ohio)", + }, + UsWest1RegionID: { + id: UsWest1RegionID, + description: "US West (N. California)", + }, + UsWest2RegionID: { + id: UsWest2RegionID, + description: "US West (Oregon)", + }, + }, + }, + AwsCnPartitionID: { + id: AwsCnPartitionID, + name: "AWS China", + dnsSuffix: "amazonaws.com.cn", + regionRegex: regexp.MustCompile(`^cn\-\w+\-\d+$`), + regions: map[string]Region{ + CnNorth1RegionID: { + id: CnNorth1RegionID, + description: "China (Beijing)", + }, + CnNorthwest1RegionID: { + id: CnNorthwest1RegionID, + description: "China (Ningxia)", + }, + }, + }, + AwsIsoPartitionID: { + id: AwsIsoPartitionID, + name: "AWS ISO (US)", + dnsSuffix: "c2s.ic.gov", + regionRegex: regexp.MustCompile(`^us\-iso\-\w+\-\d+$`), + regions: map[string]Region{ + UsIsoEast1RegionID: { + id: UsIsoEast1RegionID, + description: "US ISO East", + }, + UsIsoWest1RegionID: { + id: UsIsoWest1RegionID, + description: "US ISO WEST", + }, + }, + }, + AwsIsoBPartitionID: { + id: AwsIsoBPartitionID, + name: "AWS ISOB (US)", + dnsSuffix: "sc2s.sgov.gov", + regionRegex: regexp.MustCompile(`^us\-isob\-\w+\-\d+$`), + regions: map[string]Region{ + UsIsobEast1RegionID: { + id: UsIsobEast1RegionID, + description: "US ISOB East (Ohio)", + }, + }, + }, + AwsIsoEPartitionID: { + id: AwsIsoEPartitionID, + name: "AWS ISOE (Europe)", + dnsSuffix: "cloud.adc-e.uk", + regionRegex: regexp.MustCompile(`^eu\-isoe\-\w+\-\d+$`), + regions: map[string]Region{ + EuIsoeWest1RegionID: { + id: EuIsoeWest1RegionID, + description: "EU ISOE West", + }, + }, + }, + AwsIsoFPartitionID: { + id: AwsIsoFPartitionID, + name: "AWS ISOF", + dnsSuffix: "csp.hci.ic.gov", + regionRegex: regexp.MustCompile(`^us\-isof\-\w+\-\d+$`), + regions: map[string]Region{}, + }, + AwsUsGovPartitionID: { + id: AwsUsGovPartitionID, + name: "AWS GovCloud (US)", + dnsSuffix: "amazonaws.com", + regionRegex: regexp.MustCompile(`^us\-gov\-\w+\-\d+$`), + regions: map[string]Region{ + UsGovEast1RegionID: { + id: UsGovEast1RegionID, + description: "AWS GovCloud (US-East)", + }, + UsGovWest1RegionID: { + id: UsGovWest1RegionID, + description: "AWS GovCloud (US-West)", + }, + }, + }, + } +) diff --git a/endpoints/generate.go b/endpoints/generate.go new file mode 100644 index 00000000..296b8921 --- /dev/null +++ b/endpoints/generate.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +//go:generate go run ../internal/generate/endpoints/main.go -- https://raw.githubusercontent.com/aws/aws-sdk-go-v2/main/codegen/smithy-aws-go-codegen/src/main/resources/software/amazon/smithy/aws/go/codegen/endpoints.json + +package endpoints diff --git a/endpoints/partition.go b/endpoints/partition.go new file mode 100644 index 00000000..ddc20a01 --- /dev/null +++ b/endpoints/partition.go @@ -0,0 +1,66 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package endpoints + +import ( + "maps" + "regexp" +) + +// Partition represents an AWS partition. +// See https://docs.aws.amazon.com/whitepapers/latest/aws-fault-isolation-boundaries/partitions.html. +type Partition struct { + id string + name string + dnsSuffix string + regionRegex *regexp.Regexp + regions map[string]Region +} + +// ID returns the identifier of the partition. +func (p Partition) ID() string { + return p.id +} + +// Name returns the name of the partition. +func (p Partition) Name() string { + return p.name +} + +// DNSSuffix returns the base domain name of the partition. +func (p Partition) DNSSuffix() string { + return p.dnsSuffix +} + +// RegionRegex return the regular expression that matches Region IDs for the partition. +func (p Partition) RegionRegex() *regexp.Regexp { + return p.regionRegex +} + +// Regions returns a map of Regions for the partition, indexed by their ID. +func (p Partition) Regions() map[string]Region { + return maps.Clone(p.regions) +} + +// DefaultPartitions returns a list of the partitions. +func DefaultPartitions() []Partition { + ps := make([]Partition, 0, len(partitions)) + + for _, p := range partitions { + ps = append(ps, p) + } + + return ps +} + +// PartitionForRegion returns the first partition which includes the specific Region. +func PartitionForRegion(ps []Partition, regionID string) (Partition, bool) { + for _, p := range ps { + if _, ok := p.regions[regionID]; ok || p.regionRegex.MatchString(regionID) { + return p, true + } + } + + return Partition{}, false +} diff --git a/endpoints/partition_test.go b/endpoints/partition_test.go new file mode 100644 index 00000000..b7bc87f9 --- /dev/null +++ b/endpoints/partition_test.go @@ -0,0 +1,83 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package endpoints_test + +import ( + "testing" + + "github.com/hashicorp/aws-sdk-go-base/v2/endpoints" +) + +func TestDefaultPartitions(t *testing.T) { + t.Parallel() + + got := endpoints.DefaultPartitions() + if len(got) == 0 { + t.Fatalf("expected partitions, got none") + } +} + +func TestPartitionForRegion(t *testing.T) { + t.Parallel() + + testcases := map[string]struct { + expectedFound bool + expectedID string + }{ + "us-east-1": { + expectedFound: true, + expectedID: "aws", + }, + "us-gov-west-1": { + expectedFound: true, + expectedID: "aws-us-gov", + }, + "not-found": { + expectedFound: false, + }, + "us-east-17": { + expectedFound: true, + expectedID: "aws", + }, + } + + ps := endpoints.DefaultPartitions() + for region, testcase := range testcases { + gotID, gotFound := endpoints.PartitionForRegion(ps, region) + + if gotFound != testcase.expectedFound { + t.Errorf("expected PartitionFound %t for Region %q, got %t", testcase.expectedFound, region, gotFound) + } + if gotID.ID() != testcase.expectedID { + t.Errorf("expected PartitionID %q for Region %q, got %q", testcase.expectedID, region, gotID.ID()) + } + } +} + +func TestPartitionRegions(t *testing.T) { + t.Parallel() + + testcases := map[string]struct { + expectedRegions bool + }{ + "us-east-1": { + expectedRegions: true, + }, + "us-gov-west-1": { + expectedRegions: true, + }, + "not-found": { + expectedRegions: false, + }, + } + + ps := endpoints.DefaultPartitions() + for region, testcase := range testcases { + gotID, _ := endpoints.PartitionForRegion(ps, region) + + if got, want := len(gotID.Regions()) > 0, testcase.expectedRegions; got != want { + t.Errorf("expected Regions %t for Region %q, got %t", want, region, got) + } + } +} diff --git a/endpoints/region.go b/endpoints/region.go new file mode 100644 index 00000000..e2741b50 --- /dev/null +++ b/endpoints/region.go @@ -0,0 +1,21 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package endpoints + +// Region represents an AWS Region. +// See https://docs.aws.amazon.com/whitepapers/latest/aws-fault-isolation-boundaries/regions.html. +type Region struct { + id string + description string +} + +// ID returns the Region's identifier. +func (r Region) ID() string { + return r.id +} + +// Description returns the Region's description. +func (r Region) Description() string { + return r.description +} diff --git a/internal/endpoints/endpoints.go b/internal/endpoints/endpoints.go deleted file mode 100644 index e98bf508..00000000 --- a/internal/endpoints/endpoints.go +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package endpoints - -import ( - "regexp" -) - -func Partitions() []Partition { - ps := make([]Partition, len(partitions)) - for i := 0; i < len(partitions); i++ { - ps[i] = partitions[i].Partition() - } - return ps -} - -type Partition struct { - id string - p *partition -} - -func (p Partition) Regions() []string { - rs := make([]string, len(p.p.regions)) - copy(rs, p.p.regions) - return rs -} - -func PartitionForRegion(regionID string) string { - for _, p := range partitions { - if p.regionRegex.MatchString(regionID) { - return p.id - } - } - - return "" -} - -type partition struct { - id string - regionRegex *regexp.Regexp - regions []string -} - -func (p partition) Partition() Partition { - return Partition{ - id: p.id, - p: &p, - } -} - -// TODO: this should be generated from the AWS SDK source data -// Data from https://github.com/aws/aws-sdk-go/blob/main/models/endpoints/endpoints.json. -var partitions = []partition{ - { - id: "aws", - regionRegex: regexp.MustCompile(`^(us|eu|ap|sa|ca|me|af|il)\-\w+\-\d+$`), - regions: []string{ - "af-south-1", // Africa (Cape Town). - "ap-east-1", // Asia Pacific (Hong Kong). - "ap-northeast-1", // Asia Pacific (Tokyo). - "ap-northeast-2", // Asia Pacific (Seoul). - "ap-northeast-3", // Asia Pacific (Osaka). - "ap-south-1", // Asia Pacific (Mumbai). - "ap-south-2", // Asia Pacific (Hyderabad). - "ap-southeast-1", // Asia Pacific (Singapore). - "ap-southeast-2", // Asia Pacific (Sydney). - "ap-southeast-3", // Asia Pacific (Jakarta). - "ap-southeast-4", // Asia Pacific (Melbourne). - "ap-southeast-5", // Asia Pacific (Malaysia). - "ca-central-1", // Canada (Central). - "ca-west-1", // Canada (Calgary). - "eu-central-1", // Europe (Frankfurt). - "eu-central-2", // Europe (Zurich). - "eu-north-1", // Europe (Stockholm). - "eu-south-1", // Europe (Milan). - "eu-south-2", // Europe (Spain). - "eu-west-1", // Europe (Ireland). - "eu-west-2", // Europe (London). - "eu-west-3", // Europe (Paris). - "il-central-1", // Israel (Tel Aviv). - "me-central-1", // Middle East (UAE). - "me-south-1", // Middle East (Bahrain). - "sa-east-1", // South America (Sao Paulo). - "us-east-1", // US East (N. Virginia). - "us-east-2", // US East (Ohio). - "us-west-1", // US West (N. California). - "us-west-2", // US West (Oregon). - }, - }, - { - id: "aws-cn", - regionRegex: regexp.MustCompile(`^cn\-\w+\-\d+$`), - regions: []string{ - "cn-north-1", // China (Beijing). - "cn-northwest-1", // China (Ningxia). - }, - }, - { - id: "aws-us-gov", - regionRegex: regexp.MustCompile(`^us\-gov\-\w+\-\d+$`), - regions: []string{ - "us-gov-east-1", // AWS GovCloud (US-East). - "us-gov-west-1", // AWS GovCloud (US-West). - }, - }, - { - id: "aws-iso", - regionRegex: regexp.MustCompile(`^us\-iso\-\w+\-\d+$`), - regions: []string{ - "us-iso-east-1", // US ISO East. - "us-iso-west-1", // US ISO WEST. - }, - }, - { - id: "aws-iso-b", - regionRegex: regexp.MustCompile(`^us\-isob\-\w+\-\d+$`), - regions: []string{ - "us-isob-east-1", // US ISOB East (Ohio). - }, - }, - { - id: "aws-iso-e", - regionRegex: regexp.MustCompile(`^eu\-isoe\-\w+\-\d+$`), - regions: []string{ - "eu-isoe-west-1", // EU ISOE West. - }, - }, -} diff --git a/internal/endpoints/endpoints_test.go b/internal/endpoints/endpoints_test.go deleted file mode 100644 index fcd892e8..00000000 --- a/internal/endpoints/endpoints_test.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package endpoints_test - -import ( - "testing" - - "github.com/hashicorp/aws-sdk-go-base/v2/internal/endpoints" -) - -func TestPartitionForRegion(t *testing.T) { - testcases := map[string]struct { - expected string - }{ - "us-east-1": { - expected: "aws", - }, - "me-central-1": { - expected: "aws", - }, - "cn-north-1": { - expected: "aws-cn", - }, - "us-gov-west-1": { - expected: "aws-us-gov", - }, - } - - for region, testcase := range testcases { - got := endpoints.PartitionForRegion(region) - - if got != testcase.expected { - t.Errorf("expected Partition %q for Region %q, got %q", testcase.expected, region, got) - } - } -} diff --git a/internal/generate/common/generator.go b/internal/generate/common/generator.go new file mode 100644 index 00000000..2f08b570 --- /dev/null +++ b/internal/generate/common/generator.go @@ -0,0 +1,205 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package common + +import ( + "bytes" + "fmt" + "go/format" + "io" + "maps" + "os" + "path" + "strings" + "text/template" + "unicode" + "unicode/utf8" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type Generator struct{} + +func NewGenerator() *Generator { + return &Generator{} +} + +func (g *Generator) Infof(format string, a ...interface{}) { + g.output(os.Stdout, format, a...) +} + +func (g *Generator) Warnf(format string, a ...interface{}) { + g.Errorf(format, a...) +} + +func (g *Generator) Errorf(format string, a ...interface{}) { + g.output(os.Stderr, format, a...) +} + +func (g *Generator) Fatalf(format string, a ...interface{}) { + g.Errorf(format, a...) + os.Exit(1) +} + +func (g *Generator) output(w io.Writer, format string, a ...interface{}) { + fmt.Fprintf(w, format, a...) + fmt.Fprint(w, "\n") +} + +type Destination interface { + CreateDirectories() error + Write() error + WriteBytes(body []byte) error + WriteTemplate(templateName, templateBody string, templateData any, funcMaps ...template.FuncMap) error + WriteTemplateSet(templates *template.Template, templateData any) error +} + +func (g *Generator) NewGoFileDestination(filename string) Destination { + return &fileDestination{ + baseDestination: baseDestination{formatter: format.Source}, + filename: filename, + } +} + +func (g *Generator) NewUnformattedFileDestination(filename string) Destination { + return &fileDestination{ + filename: filename, + } +} + +type fileDestination struct { + baseDestination + append bool + filename string +} + +func (d *fileDestination) CreateDirectories() error { + const ( + perm os.FileMode = 0755 + ) + dirname := path.Dir(d.filename) + err := os.MkdirAll(dirname, perm) + + if err != nil { + return fmt.Errorf("creating target directory %s: %w", dirname, err) + } + + return nil +} + +func (d *fileDestination) Write() error { + var flags int + if d.append { + flags = os.O_APPEND | os.O_CREATE | os.O_WRONLY + } else { + flags = os.O_TRUNC | os.O_CREATE | os.O_WRONLY + } + f, err := os.OpenFile(d.filename, flags, 0644) //nolint:mnd // good protection for new files + + if err != nil { + return fmt.Errorf("opening file (%s): %w", d.filename, err) + } + + defer f.Close() + + _, err = f.WriteString(d.buffer.String()) + + if err != nil { + return fmt.Errorf("writing to file (%s): %w", d.filename, err) + } + + return nil +} + +type baseDestination struct { + formatter func([]byte) ([]byte, error) + buffer strings.Builder +} + +func (d *baseDestination) WriteBytes(body []byte) error { + _, err := d.buffer.Write(body) + return err +} + +func (d *baseDestination) WriteTemplate(templateName, templateBody string, templateData any, funcMaps ...template.FuncMap) error { + body, err := parseTemplate(templateName, templateBody, templateData, funcMaps...) + + if err != nil { + return err + } + + body, err = d.format(body) + if err != nil { + return err + } + + return d.WriteBytes(body) +} + +func parseTemplate(templateName, templateBody string, templateData any, funcMaps ...template.FuncMap) ([]byte, error) { + funcMap := template.FuncMap{ + "FirstUpper": FirstUpper, + // Title returns a string with the first character of each word as upper case. + "Title": cases.Title(language.Und, cases.NoLower).String, + } + for _, v := range funcMaps { + maps.Copy(funcMap, v) // Extras overwrite defaults. + } + tmpl, err := template.New(templateName).Funcs(funcMap).Parse(templateBody) + + if err != nil { + return nil, fmt.Errorf("parsing function template: %w", err) + } + + return executeTemplate(tmpl, templateData) +} + +func executeTemplate(tmpl *template.Template, templateData any) ([]byte, error) { + var buffer bytes.Buffer + err := tmpl.Execute(&buffer, templateData) + + if err != nil { + return nil, fmt.Errorf("executing template: %w", err) + } + + return buffer.Bytes(), nil +} + +func (d *baseDestination) WriteTemplateSet(templates *template.Template, templateData any) error { + body, err := executeTemplate(templates, templateData) + if err != nil { + return err + } + + body, err = d.format(body) + if err != nil { + return err + } + + return d.WriteBytes(body) +} + +func (d *baseDestination) format(body []byte) ([]byte, error) { + if d.formatter == nil { + return body, nil + } + + unformattedBody := body + body, err := d.formatter(unformattedBody) + if err != nil { + return nil, fmt.Errorf("formatting parsed template:\n%s\n%w", unformattedBody, err) + } + + return body, nil +} + +// FirstUpper returns a string with the first character as upper case. +func FirstUpper(s string) string { + if s == "" { + return "" + } + r, n := utf8.DecodeRuneInString(s) + return string(unicode.ToUpper(r)) + s[n:] +} diff --git a/internal/generate/endpoints/main.go b/internal/generate/endpoints/main.go new file mode 100644 index 00000000..d08c942f --- /dev/null +++ b/internal/generate/endpoints/main.go @@ -0,0 +1,189 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +//go:build generate +// +build generate + +package main + +import ( + _ "embed" + "encoding/json" + "flag" + "fmt" + "html/template" + "io" + "net/http" + "os" + "sort" + "strings" + + "github.com/hashicorp/aws-sdk-go-base/v2/internal/generate/common" + "github.com/hashicorp/aws-sdk-go-base/v2/internal/slices" +) + +type PartitionDatum struct { + ID string + Name string + DNSSuffix string + RegionRegex string + Regions []RegionDatum +} + +type RegionDatum struct { + ID string + Description string +} + +type TemplateData struct { + Partitions []PartitionDatum +} + +func usage() { + fmt.Fprintf(os.Stderr, "Usage:\n") + fmt.Fprintf(os.Stderr, "\tmain.go \n\n") +} + +func main() { + flag.Usage = usage + flag.Parse() + + args := flag.Args() + + if len(args) < 1 { + flag.Usage() + os.Exit(2) + } + + inputURL := args[0] + filename := `endpoints_gen.go` + target := map[string]any{} + + g := common.NewGenerator() + g.Infof("Generating endpoints/%s", filename) + + if err := readHTTPJSON(inputURL, &target); err != nil { + g.Fatalf("error reading JSON from %s: %s", inputURL, err) + } + + td := TemplateData{} + templateFuncMap := template.FuncMap{ + // KebabToTitle splits a kebab case string and returns a string with each part title cased. + "KebabToTitle": func(s string) (string, error) { + parts := strings.Split(s, "-") + return strings.Join(slices.ApplyToAll(parts, func(s string) string { + return common.FirstUpper(s) + }), ""), nil + }, + } + + if version, ok := target["version"].(float64); ok { + if version != 3.0 { + g.Fatalf("unsupported endpoints document version: %d", int(version)) + } + } else { + g.Fatalf("can't parse endpoints document version") + } + + /* + See https://github.com/aws/aws-sdk-go/blob/main/aws/endpoints/v3model.go. + e.g. + { + "partitions": [{ + "partition": "aws", + "partitionName": "AWS Standard", + "regions" : { + "af-south-1" : { + "description" : "Africa (Cape Town)" + }, + ... + } + ... + }, ...] + } + */ + if partitions, ok := target["partitions"].([]any); ok { + for _, partition := range partitions { + if partition, ok := partition.(map[string]any); ok { + partitionDatum := PartitionDatum{} + + if id, ok := partition["partition"].(string); ok { + partitionDatum.ID = id + } + if name, ok := partition["partitionName"].(string); ok { + partitionDatum.Name = name + } + if dnsSuffix, ok := partition["dnsSuffix"].(string); ok { + partitionDatum.DNSSuffix = dnsSuffix + } + if regionRegex, ok := partition["regionRegex"].(string); ok { + partitionDatum.RegionRegex = regionRegex + } + if regions, ok := partition["regions"].(map[string]any); ok { + for id, region := range regions { + regionDatum := RegionDatum{ + ID: id, + } + + if region, ok := region.(map[string]any); ok { + if description, ok := region["description"].(string); ok { + regionDatum.Description = description + } + } + + partitionDatum.Regions = append(partitionDatum.Regions, regionDatum) + } + } + + td.Partitions = append(td.Partitions, partitionDatum) + } + } + } + + sort.SliceStable(td.Partitions, func(i, j int) bool { + return td.Partitions[i].ID < td.Partitions[j].ID + }) + + for i := 0; i < len(td.Partitions); i++ { + sort.SliceStable(td.Partitions[i].Regions, func(j, k int) bool { + return td.Partitions[i].Regions[j].ID < td.Partitions[i].Regions[k].ID + }) + } + + d := g.NewGoFileDestination(filename) + + if err := d.WriteTemplate("endpoints", tmpl, td, templateFuncMap); err != nil { + g.Fatalf("error generating endpoint resolver: %s", err) + } + + if err := d.Write(); err != nil { + g.Fatalf("generating file (%s): %s", filename, err) + } +} + +func readHTTPJSON(url string, to any) error { + r, err := http.Get(url) + if err != nil { + return err + } + defer r.Body.Close() + + return decodeFromReader(r.Body, to) +} + +func decodeFromReader(r io.Reader, to any) error { + dec := json.NewDecoder(r) + + for { + if err := dec.Decode(to); err == io.EOF { + break + } else if err != nil { + return err + } + } + + return nil +} + +//go:embed output.go.gtpl +var tmpl string diff --git a/internal/generate/endpoints/output.go.gtpl b/internal/generate/endpoints/output.go.gtpl new file mode 100644 index 00000000..481e71fb --- /dev/null +++ b/internal/generate/endpoints/output.go.gtpl @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by internal/generate/endpoints/main.go; DO NOT EDIT. + +package endpoints + +import ( + "regexp" +) + +// All known partition IDs. +const ( +{{- range .Partitions }} + {{ .ID | KebabToTitle}}PartitionID = "{{ .ID }}" // {{ .Name }} +{{- end }} +) + +// All known Region IDs. +const ( +{{- range .Partitions }} + // {{ .Name }} partition's Regions. + {{- range .Regions }} + {{ .ID | KebabToTitle}}RegionID = "{{ .ID }}" // {{ .Description }} + {{- end }} +{{- end }} +) + +var ( + partitions = map[string]Partition{ +{{- range .Partitions }} + {{ .ID | KebabToTitle}}PartitionID: { + id: {{ .ID | KebabToTitle}}PartitionID, + name: "{{ .Name }}", + dnsSuffix: "{{ .DNSSuffix }}", + regionRegex: regexp.MustCompile(`{{ .RegionRegex }}`), + regions: map[string]Region{ + {{- range .Regions }} + {{ .ID | KebabToTitle}}RegionID: { + id: {{ .ID | KebabToTitle}}RegionID, + description: "{{ .Description }}", + }, + {{- end }} + }, + }, +{{- end }} + } +) \ No newline at end of file diff --git a/validation/region.go b/validation/region.go index 4241a665..cd6aa651 100644 --- a/validation/region.go +++ b/validation/region.go @@ -5,8 +5,9 @@ package validation import ( "fmt" + "slices" - "github.com/hashicorp/aws-sdk-go-base/v2/internal/endpoints" + "github.com/hashicorp/aws-sdk-go-base/v2/endpoints" ) type InvalidRegionError struct { @@ -14,17 +15,16 @@ type InvalidRegionError struct { } func (e *InvalidRegionError) Error() string { - return fmt.Sprintf("Invalid AWS Region: %s", e.region) + return fmt.Sprintf("invalid AWS Region: %s", e.region) } // SupportedRegion checks if the given region is a valid AWS region. func SupportedRegion(region string) error { - for _, partition := range endpoints.Partitions() { - for _, partitionRegion := range partition.Regions() { - if region == partitionRegion { - return nil - } - } + if slices.ContainsFunc(endpoints.DefaultPartitions(), func(p endpoints.Partition) bool { + _, ok := p.Regions()[region] + return ok + }) { + return nil } return &InvalidRegionError{