Skip to content

Commit

Permalink
feat: arn handling provider functions
Browse files Browse the repository at this point in the history
The `arn_parse` and `arn_build` provider functions will allow
practioners to easily handle ARN (Amazon Resource Name) manipulation
with built-in functions. The `arn_parse` function provides similar
utility to the `aws_arn` data source, but with the benefit of running
earlier in the execution order.
  • Loading branch information
jar-b committed Dec 15, 2023
1 parent 4485c08 commit b247aad
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 0 deletions.
52 changes: 52 additions & 0 deletions internal/function/arn_build_function.go
@@ -0,0 +1,52 @@
package function

import (
"context"

"github.com/aws/aws-sdk-go-v2/aws/arn"
"github.com/hashicorp/terraform-plugin-framework/function"
)

var _ function.Function = arnBuildFunction{}

func NewARNBuildFunction() function.Function {
return &arnBuildFunction{}
}

type arnBuildFunction struct{}

func (f arnBuildFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) {
resp.Name = "arn_build"
}

func (f arnBuildFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Parameters: []function.Parameter{
function.StringParameter{Name: "partition"},
function.StringParameter{Name: "service"},
function.StringParameter{Name: "region"},
function.StringParameter{Name: "account_id"},
function.StringParameter{Name: "resource"},
},
Return: function.StringReturn{},
}
}

func (f arnBuildFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var partition, service, region, accountID, resource string

resp.Diagnostics.Append(req.Arguments.Get(ctx, &partition, &service, &region, &accountID, &resource)...)
if resp.Diagnostics.HasError() {
return
}

result := arn.ARN{
Partition: partition,
Service: service,
Region: region,
AccountID: accountID,
Resource: resource,
}

resp.Diagnostics.Append(resp.Result.Set(ctx, result.String())...)
}
39 changes: 39 additions & 0 deletions internal/function/arn_build_function_test.go
@@ -0,0 +1,39 @@
package function_test

import (
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
)

func TestARNBuildFunction_known(t *testing.T) {
t.Parallel()

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: testARNBuildFunctionConfig(),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckOutput("test", "arn:aws:iam::444455556666:role/example"),
),
},
},
})
}

func testARNBuildFunctionConfig() string {
return `
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
output "test" {
value = provider::aws::arn_build("aws", "iam", "", "444455556666", "role/example")
}`
}
72 changes: 72 additions & 0 deletions internal/function/arn_parse_function.go
@@ -0,0 +1,72 @@
package function

import (
"context"

"github.com/aws/aws-sdk-go-v2/aws/arn"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/types"
)

var arnParseResultAttrTypes = map[string]attr.Type{
"partition": types.StringType,
"service": types.StringType,
"region": types.StringType,
"account_id": types.StringType,
"resource": types.StringType,
}

var _ function.Function = arnParseFunction{}

func NewARNParseFunction() function.Function {
return &arnParseFunction{}
}

type arnParseFunction struct{}

func (f arnParseFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) {
resp.Name = "arn_parse"
}

func (f arnParseFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Parameters: []function.Parameter{
function.StringParameter{},
},
Return: function.ObjectReturn{
AttributeTypes: arnParseResultAttrTypes,
},
}
}

func (f arnParseFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var arg string

resp.Diagnostics.Append(req.Arguments.Get(ctx, &arg)...)
if resp.Diagnostics.HasError() {
return
}

parts, err := arn.Parse(arg)
if err != nil {
resp.Diagnostics.AddError("arn parsing failed", err.Error())
return
}

value := map[string]attr.Value{
"partition": types.StringValue(parts.Partition),
"service": types.StringValue(parts.Service),
"region": types.StringValue(parts.Region),
"account_id": types.StringValue(parts.AccountID),
"resource": types.StringValue(parts.Resource),
}

result, d := types.ObjectValue(arnParseResultAttrTypes, value)
resp.Diagnostics.Append(d...)
if resp.Diagnostics.HasError() {
return
}

resp.Diagnostics.Append(resp.Result.Set(ctx, result)...)
}
57 changes: 57 additions & 0 deletions internal/function/arn_parse_function_test.go
@@ -0,0 +1,57 @@
package function_test

import (
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
)

func TestARNParseFunction_known(t *testing.T) {
t.Parallel()

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: testARNParseFunctionConfig("arn:aws:iam::444455556666:role/example"),
Check: resource.ComposeAggregateTestCheckFunc(
// TODO: figure out how to test object output
//resource.TestCheckOutput("test", "arn:aws:iam::444455556666:role/example"),
),
},
},
})
}

func TestARNParseFunction_invalid(t *testing.T) {
t.Parallel()

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: testARNParseFunctionConfig("invalid"),
ExpectError: regexp.MustCompile("arn parsing failed"),
},
},
})
}

func testARNParseFunctionConfig(arg string) string {
return fmt.Sprintf(`
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
output "test" {
value = provider::aws::arn_parse(%[1]q)
}
`, arg)
}
17 changes: 17 additions & 0 deletions internal/provider/fwprovider/provider.go
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
Expand All @@ -18,10 +19,14 @@ import (
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types"
tffunction "github.com/hashicorp/terraform-provider-aws/internal/function"
tftags "github.com/hashicorp/terraform-provider-aws/internal/tags"
"github.com/hashicorp/terraform-provider-aws/names"
)

var _ provider.Provider = &fwprovider{}
var _ provider.ProviderWithFunctions = &fwprovider{}

// New returns a new, initialized Terraform Plugin Framework-style provider instance.
// The provider instance is fully configured once the `Configure` method has been called.
func New(primary interface{ Meta() interface{} }) provider.Provider {
Expand Down Expand Up @@ -448,6 +453,18 @@ func (p *fwprovider) Resources(ctx context.Context) []func() resource.Resource {
return resources
}

// Functions returns a slice of functions to instantiate each Function
// implementation.
//
// The function type name is determined by the Function implementing
// the Metadata method. All functions must have unique names.
func (p *fwprovider) Functions(_ context.Context) []func() function.Function {
return []func() function.Function{
tffunction.NewARNBuildFunction,
tffunction.NewARNParseFunction,
}
}

func endpointsBlock() schema.SetNestedBlock {
endpointsAttributes := make(map[string]schema.Attribute)

Expand Down

0 comments on commit b247aad

Please sign in to comment.