Skip to content

Commit

Permalink
Merge pull request #2 from erhancagirici/partition-gen-auto-config
Browse files Browse the repository at this point in the history
generate default IAM regions per partition
  • Loading branch information
erhancagirici authored Jan 24, 2025
2 parents 0538256 + 41a3c39 commit 9561358
Show file tree
Hide file tree
Showing 11 changed files with 536 additions and 16 deletions.
2 changes: 1 addition & 1 deletion apis/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ type URLConfig struct {
// and region by choosing Static type. Alternatively, you can provide
// configuration for dynamically resolving the URL with the config you provide
// once you set the type as Dynamic.
// +kubebuilder:validation:Enum=Static;Dynamic
// +kubebuilder:validation:Enum=Static;Dynamic;Auto
Type string `json:"type"`

// Static is the full URL you'd like the AWS SDK to use.
Expand Down
220 changes: 220 additions & 0 deletions cmd/partitiongen/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package main

import (
"bytes"
_ "embed"
"encoding/json"
"flag"
"fmt"
"go/format"
"html/template"
"io"
"log"
"net/http"
"os"
"sort"

"github.com/crossplane/crossplane-runtime/pkg/errors"
)

//go:embed partitions_gen.go.tmpl
var templateBody string

const documentVersion = 3

type PartitionDatum struct {
ID string
Name string
DNSSuffix string
RegionRegex string
CredentialScopeRegion string
IAMRegions map[string]string
GlobalServiceSigningRegions map[string]string
Regions []RegionDatum
}

type RegionDatum struct {
ID string
Description string
}

type TemplateData struct {
Partitions []PartitionDatum
}

type EndpointsDocument struct {
Partitions []PartitionModel `json:"partitions"`
Version uint64 `json:"version"`
}

type PartitionModel struct {
Defaults DefaultsModel `json:"defaults"`
DnsSuffix string `json:"dnsSuffix"`
Partition string `json:"partition"`
PartitionName string `json:"partitionName"`
RegionRegex string `json:"regionRegex"`
Regions map[string]RegionModel `json:"regions"`
Services map[string]ServiceModel `json:"services"`
}

type DefaultsModel struct {
Hostname string `json:"hostname"`
Protocols []string `json:"protocols"`
SignatureVersions []string `json:"signatureVersions"`
Variants []VariantModel `json:"variants"`
}

type VariantModel struct {
DnsSuffix string `json:"dnsSuffix"`
Hostname string `json:"hostname"`
Tags []string `json:"tags"`
}

type RegionModel struct {
Description string `json:"description"`
}

type ServiceModel struct {
Endpoints map[string]EndpointModel `json:"endpoints"`
IsRegionalized bool `json:"isRegionalized,omitempty"`
PartitionEndpoint string `json:"partitionEndpoint,omitempty"`
Defaults *DefaultsModel `json:"defaults,omitempty"`
}

type EndpointModel struct {
CredentialScope *CredentialScopeModel `json:"credentialScope,omitempty"`
Hostname string `json:"hostname"`
Protocols []string `json:"protocols,omitempty"`
Variants []VariantModel `json:"variants,omitempty"`
Deprecated bool `json:"deprecated,omitempty"`
}

type CredentialScopeModel struct {
Region string `json:"region,omitempty"`
Service string `json:"service,omitempty"`
}

func usage() {
fmt.Fprintf(os.Stderr, "Usage:\n")
fmt.Fprintf(os.Stderr, "\tmain.go <aws-sdk-go-v2-endpoints-json-url>\n\n")
}

func main() {
flag.Usage = usage
flag.Parse()

args := flag.Args()

if len(args) < 1 {
flag.Usage()
os.Exit(2)
}

inputURL := args[0]
targetFilename := `zz_partitions_gen.go`
var endpointDocu EndpointsDocument

log.Println("Generating AWS partition definitions file", targetFilename)

if err := readEndpointsDocumentFromURL(inputURL, &endpointDocu); err != nil {
log.Fatalf("error reading JSON from %s: %s", inputURL, err)
}

templateData := TemplateData{}

if endpointDocu.Version != documentVersion {
log.Fatalf("unsupported endpoints document version: %d, expected version: %d", endpointDocu.Version, documentVersion)
}

for _, partition := range endpointDocu.Partitions {
partitionDatum := PartitionDatum{
GlobalServiceSigningRegions: make(map[string]string),
}
partitionDatum.ID = partition.Partition
partitionDatum.Name = partition.PartitionName
partitionDatum.DNSSuffix = partition.DnsSuffix
partitionDatum.RegionRegex = partition.RegionRegex
for id, region := range partition.Regions {
regionDatum := RegionDatum{
ID: id,
Description: region.Description,
}
partitionDatum.Regions = append(partitionDatum.Regions, regionDatum)
}

for svcName, svc := range partition.Services {
if svc.PartitionEndpoint != "" {
defaultEndpoint, ok := svc.Endpoints[svc.PartitionEndpoint]
if !ok {
log.Fatalf("partition endpoint %q not found for service %q in partition %q", svc.PartitionEndpoint, svcName, partition.Partition)
}
if defaultEndpoint.CredentialScope == nil {
continue
}
partitionDatum.GlobalServiceSigningRegions[svcName] = defaultEndpoint.CredentialScope.Region
}
}
templateData.Partitions = append(templateData.Partitions, partitionDatum)

}

sort.SliceStable(templateData.Partitions, func(i, j int) bool {
return templateData.Partitions[i].ID < templateData.Partitions[j].ID
})

for i := 0; i < len(templateData.Partitions); i++ {
sort.SliceStable(templateData.Partitions[i].Regions, func(j, k int) bool {
return templateData.Partitions[i].Regions[j].ID < templateData.Partitions[i].Regions[k].ID
})
}

tmpl, err := template.New("partitions").Parse(templateBody)

if err != nil {
log.Fatalf("parsing function template: %v", err)
}

targetFile, err := os.OpenFile(targetFilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("failed to open file %q for write: %v", targetFilename, err)
}
defer func() {
if err := targetFile.Close(); err != nil {
log.Fatalf("Failed to close the templated main %q: %s", targetFilename, err.Error())
}
}()

buf := &bytes.Buffer{}
if err := tmpl.Execute(buf, templateData); err != nil {
log.Fatalf("cannot execute template: %v", err)
}
gofmtFormattedBytes, err := format.Source(buf.Bytes())
if err != nil {
log.Fatalf("cannot gofmt generated partitions file: %v", err)
}
if _, err := targetFile.Write(gofmtFormattedBytes); err != nil {
log.Fatalf("cannot write generated file: %v", err)
}

log.Println("Successfully generated AWS partition definitions file", targetFilename)
}

func readEndpointsDocumentFromURL(url string, to *EndpointsDocument) error {
r, err := http.Get(url)
if err != nil {
return errors.Wrap(err, "cannot fetch remote endpoints document")
}
if r.StatusCode < 200 || r.StatusCode > 299 {
return errors.Errorf("fetching endpoints document returned non-2xx HTTP status code: %s", r.Status)
}
defer r.Body.Close()

epDocumentRaw, err := io.ReadAll(r.Body)
if err != nil {
return errors.Wrap(err, "cannot read HTTP body")
}
return errors.Wrap(json.Unmarshal(epDocumentRaw, to), "cannot unmarshal endpoints document")
}
24 changes: 24 additions & 0 deletions cmd/partitiongen/partitions_gen.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2024 The Crossplane Authors <https://crossplane.io>
//
// SPDX-License-Identifier: Apache-2.0

// Code generated by cmd/endpoints/main.go; DO NOT EDIT.

package clients

var (
partitions = map[string]awsPartition{
{{- range .Partitions }}
"{{ .ID }}": {
id: "{{ .ID }}",
name: "{{ .Name }}",
dnsSuffix: "{{ .DNSSuffix }}",
serviceToDefaultRegions: map[string]string{
{{- range $svc, $region := .GlobalServiceSigningRegions }}
"{{ $svc }}": "{{ $region }}",
{{- end }}
},
},
{{- end }}
}
)
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/crossplane/upjet v1.4.1-0.20240911184956-3afbb7796d46
github.com/go-ini/ini v1.46.0
github.com/google/go-cmp v0.6.0
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.58
github.com/hashicorp/awspolicyequivalence v1.6.0
github.com/hashicorp/terraform-json v0.22.1
github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0
Expand Down Expand Up @@ -331,7 +332,6 @@ require (
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/aws-cloudformation-resource-schema-sdk-go v0.23.0 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.58 // indirect
github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2 v2.0.0-beta.59 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
Expand Down
45 changes: 35 additions & 10 deletions internal/clients/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/upjet/pkg/metrics"
"github.com/crossplane/upjet/pkg/terraform"
"github.com/hashicorp/aws-sdk-go-base/v2/endpoints"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-aws/xpprovider"
"github.com/pkg/errors"
Expand All @@ -40,14 +41,7 @@ type SetupConfig struct {
}

// iamRegions holds the region used for signing IAM credentials for each AWS partition.
var iamRegions = map[string]string{
"aws": "us-east-1",
"aws-us-gov": "us-gov-west-1",
"aws-cn": "cn-north-1",
"aws-iso": "us-iso-east-1",
"aws-iso-b": "us-isob-east-1",
"aws-iso-e": "eu-isoe-west-1",
}
var iamRegions = getIAMDefaultSigningRegions()

func SelectTerraformSetup(config *SetupConfig) terraform.SetupFn { // nolint:gocyclo
credsCache := NewAWSCredentialsProviderCache(WithCacheLogger(config.Logger))
Expand Down Expand Up @@ -107,8 +101,8 @@ func SelectTerraformSetup(config *SetupConfig) terraform.SetupFn { // nolint:goc
keyPartition: "aws",
}

if pc.Spec.Endpoint != nil && pc.Spec.Endpoint.PartitionID != nil {
ps.ClientMetadata[keyPartition] = *pc.Spec.Endpoint.PartitionID
if err := setPartition(awsCfg, pc, &ps); err != nil {
return terraform.Setup{}, errors.Wrap(err, "cannot configure AWS partition")
}

// several external name configs depend on the setup.Configuration for templating region
Expand All @@ -122,6 +116,37 @@ func SelectTerraformSetup(config *SetupConfig) terraform.SetupFn { // nolint:goc
}
}

func setPartition(awsCfg *aws.Config, pc *v1beta1.ProviderConfig, ps *terraform.Setup) error {
var partitionFromConfig string
if pc.Spec.Endpoint != nil && pc.Spec.Endpoint.PartitionID != nil {
partitionFromConfig = *pc.Spec.Endpoint.PartitionID
ps.ClientMetadata[keyPartition] = partitionFromConfig
}
// region should never be empty, but defensively code to preserve existing behavior
if awsCfg.Region == "" {
return nil
}

// TODO(erhan): localstack environments with ALLOW_NONSTANDARD_REGIONS configuration
// might fail this check. Consider introducing a config that opt-out from partition
// resolution
partitionFromRegion, ok := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), awsCfg.Region)
if !ok || partitionFromRegion.ID() == "" {
// tolerate unknown region and honor when explicit partition config exists
if partitionFromConfig != "" {
return nil
}
return errors.Errorf("managed resource region %q does not belong to a known partition", awsCfg.Region)
}

if partitionFromConfig != "" && partitionFromConfig != partitionFromRegion.ID() {
return errors.Errorf("conflicting partition config: managed resource region %q does not belong to configured partition %q at provider config", awsCfg.Region, *pc.Spec.Endpoint.PartitionID)
}

ps.ClientMetadata[keyPartition] = partitionFromRegion.ID()
return nil
}

// getAccountId retrieves the account ID associated with the given credentials.
// Results are cached.
func getAccountId(ctx context.Context, cfg *aws.Config, creds aws.Credentials) (string, error) {
Expand Down
2 changes: 2 additions & 0 deletions internal/clients/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
//go:generate go run ../../cmd/partitiongen/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 clients
29 changes: 29 additions & 0 deletions internal/clients/partitions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package clients

import "regexp"

type awsPartition struct {
id string
name string
regionRegex *regexp.Regexp
dnsSuffix string
serviceToDefaultRegions map[string]string
regions map[string]awsRegion
}

type awsRegion struct {
id string
description string
}

func getIAMDefaultSigningRegions() map[string]string {
var partitionToDefaultRegion = map[string]string{}
for name, partition := range partitions {
reg, ok := partition.serviceToDefaultRegions["iam"]
if !ok {
continue
}
partitionToDefaultRegion[name] = reg
}
return partitionToDefaultRegion
}
Loading

0 comments on commit 9561358

Please sign in to comment.