diff --git a/.changelog/36723.txt b/.changelog/36723.txt new file mode 100644 index 000000000000..284767c85f49 --- /dev/null +++ b/.changelog/36723.txt @@ -0,0 +1,3 @@ +```release-note:new-function +trim_iam_role_path +``` diff --git a/internal/function/arn_build_function.go b/internal/function/arn_build.go similarity index 100% rename from internal/function/arn_build_function.go rename to internal/function/arn_build.go diff --git a/internal/function/arn_build_function_test.go b/internal/function/arn_build_test.go similarity index 100% rename from internal/function/arn_build_function_test.go rename to internal/function/arn_build_test.go diff --git a/internal/function/arn_parse_function.go b/internal/function/arn_parse.go similarity index 100% rename from internal/function/arn_parse_function.go rename to internal/function/arn_parse.go diff --git a/internal/function/arn_parse_function_test.go b/internal/function/arn_parse_test.go similarity index 100% rename from internal/function/arn_parse_function_test.go rename to internal/function/arn_parse_test.go diff --git a/internal/function/trim_iam_role_path.go b/internal/function/trim_iam_role_path.go new file mode 100644 index 000000000000..7bcfb2c230fd --- /dev/null +++ b/internal/function/trim_iam_role_path.go @@ -0,0 +1,92 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws/arn" + "github.com/hashicorp/terraform-plugin-framework/function" +) + +const ( + // IAM role ARN reference: + // https://docs.aws.amazon.com/IAM/latest/UserGuide/list_awsidentityandaccessmanagementiam.html#awsidentityandaccessmanagementiam-resources-for-iam-policies + + // resourceSectionPrefix is the expected prefix in the resource section of + // an IAM role ARN + resourceSectionPrefix = "role/" + + // serviceSection is the expected service section of an IAM role ARN + serviceSection = "iam" +) + +var _ function.Function = trimIAMRolePathFunction{} + +func NewTrimIAMRolePathFunction() function.Function { + return &trimIAMRolePathFunction{} +} + +type trimIAMRolePathFunction struct{} + +func (f trimIAMRolePathFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "trim_iam_role_path" +} + +func (f trimIAMRolePathFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Summary: "trim_iam_role_path Function", + MarkdownDescription: "Trims the path prefix from an IAM role Amazon Resource Name (ARN). This " + + "function can be used when services require role ARNs to be passed without a path.", + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "arn", + MarkdownDescription: "IAM role Amazon Resource Name (ARN)", + }, + }, + Return: function.StringReturn{}, + } +} + +func (f trimIAMRolePathFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var arg string + + resp.Error = function.ConcatFuncErrors(req.Arguments.Get(ctx, &arg)) + if resp.Error != nil { + return + } + + result, err := trimPath(arg) + if err != nil { + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(err.Error())) + return + } + + resp.Error = function.ConcatFuncErrors(resp.Result.Set(ctx, result)) +} + +// trimPath removes all path prefixes from the resource section of a role ARN +func trimPath(s string) (string, error) { + rarn, err := arn.Parse(s) + if err != nil { + return "", err + } + + if rarn.Service != serviceSection { + return "", fmt.Errorf(`service must be "%s"`, serviceSection) + } + if rarn.Region != "" { + return "", fmt.Errorf("region must be empty") + } + if !strings.HasPrefix(rarn.Resource, resourceSectionPrefix) { + return "", fmt.Errorf(`resource must begin with "%s"`, resourceSectionPrefix) + } + + sec := strings.Split(rarn.Resource, "/") + rarn.Resource = fmt.Sprintf("%s%s", resourceSectionPrefix, sec[len(sec)-1]) + + return rarn.String(), nil +} diff --git a/internal/function/trim_iam_role_path_test.go b/internal/function/trim_iam_role_path_test.go new file mode 100644 index 000000000000..5f77132007a7 --- /dev/null +++ b/internal/function/trim_iam_role_path_test.go @@ -0,0 +1,146 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "fmt" + "testing" + + "github.com/YakDriver/regexache" + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" +) + +var ( + // ExpectError parses the human readable output of a terraform apply run, in which + // formatting (including line breaks) may change over time. For extra safety, we add + // optional whitespace between each word in the expected error text. + + expectedErrorInvalidARN = regexache.MustCompile(`invalid[\s\n]*prefix`) + expectedErrorInvalidService = regexache.MustCompile(`service[\s\n]*must`) + expectedErrorInvalidRegion = regexache.MustCompile(`region[\s\n]*must`) + expectedErrorInvalidResource = regexache.MustCompile(`resource[\s\n]*must`) +) + +func TestTrimIAMRolePathFunction_valid(t *testing.T) { + t.Parallel() + arg := "arn:aws:iam::444455556666:role/example" + + resource.UnitTest(t, resource.TestCase{ + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.8.0-beta1"))), + }, + Steps: []resource.TestStep{ + { + Config: testTrimIAMRolePathFunctionConfig(arg), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckOutput("test", arg), + ), + }, + }, + }) +} + +func TestTrimIAMRolePathFunction_validWithPath(t *testing.T) { + t.Parallel() + arg := "arn:aws:iam::444455556666:role/with/some/path/parts/example" + expected := "arn:aws:iam::444455556666:role/example" + + resource.UnitTest(t, resource.TestCase{ + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.8.0-beta1"))), + }, + Steps: []resource.TestStep{ + { + Config: testTrimIAMRolePathFunctionConfig(arg), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckOutput("test", expected), + ), + }, + }, + }) +} + +func TestTrimIAMRolePathFunction_invalidARN(t *testing.T) { + t.Parallel() + arg := "foo" + + resource.UnitTest(t, resource.TestCase{ + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.8.0-beta1"))), + }, + Steps: []resource.TestStep{ + { + Config: testTrimIAMRolePathFunctionConfig(arg), + ExpectError: expectedErrorInvalidARN, + }, + }, + }) +} + +func TestTrimIAMRolePathFunction_invalidService(t *testing.T) { + t.Parallel() + arg := "arn:aws:s3:::bucket/foo" + + resource.UnitTest(t, resource.TestCase{ + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.8.0-beta1"))), + }, + Steps: []resource.TestStep{ + { + Config: testTrimIAMRolePathFunctionConfig(arg), + ExpectError: expectedErrorInvalidService, + }, + }, + }) +} + +func TestTrimIAMRolePathFunction_invalidRegion(t *testing.T) { + t.Parallel() + arg := "arn:aws:iam:us-east-1:444455556666:role/example" + + resource.UnitTest(t, resource.TestCase{ + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.8.0-beta1"))), + }, + Steps: []resource.TestStep{ + { + Config: testTrimIAMRolePathFunctionConfig(arg), + ExpectError: expectedErrorInvalidRegion, + }, + }, + }) +} + +func TestTrimIAMRolePathFunction_invalidResource(t *testing.T) { + t.Parallel() + arg := "arn:aws:iam::444455556666:policy/example" + + resource.UnitTest(t, resource.TestCase{ + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.8.0-beta1"))), + }, + Steps: []resource.TestStep{ + { + Config: testTrimIAMRolePathFunctionConfig(arg), + ExpectError: expectedErrorInvalidResource, + }, + }, + }) +} + +func testTrimIAMRolePathFunctionConfig(arg string) string { + return fmt.Sprintf(` +output "test" { + value = provider::aws::trim_iam_role_path(%[1]q) +}`, arg) +} diff --git a/internal/provider/fwprovider/provider.go b/internal/provider/fwprovider/provider.go index 027b39dd4300..6415ce235513 100644 --- a/internal/provider/fwprovider/provider.go +++ b/internal/provider/fwprovider/provider.go @@ -466,6 +466,7 @@ func (p *fwprovider) Functions(_ context.Context) []func() function.Function { return []func() function.Function{ tffunction.NewARNBuildFunction, tffunction.NewARNParseFunction, + tffunction.NewTrimIAMRolePathFunction, } } diff --git a/website/docs/functions/trim_iam_role_path.html.markdown b/website/docs/functions/trim_iam_role_path.html.markdown new file mode 100644 index 000000000000..f611d224dad2 --- /dev/null +++ b/website/docs/functions/trim_iam_role_path.html.markdown @@ -0,0 +1,35 @@ +--- +subcategory: "" +layout: "aws" +page_title: "AWS: trim_iam_role_path" +description: |- + Trims the path prefix from an IAM role Amazon Resource Name (ARN). +--- + +# Function: trim_iam_role_path + +~> Provider-defined function support is in technical preview and offered without compatibility promises until Terraform 1.8 is generally available. + +Trims the path prefix from an IAM role Amazon Resource Name (ARN). +This function can be used when services require role ARNs to be passed without a path. + +See the [AWS IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/list_awsidentityandaccessmanagementiam.html#awsidentityandaccessmanagementiam-resources-for-iam-policies) for additional information on IAM role ARNs. + +## Example Usage + +```terraform +# result: arn:aws:iam::444455556666:role/example +output "example" { + value = provider::aws::trim_iam_role_path("arn:aws:iam::444455556666:role/with/path/example") +} +``` + +## Signature + +```text +trim_iam_role_path(arn string) string +``` + +## Arguments + +1. `arn` (String) IAM role Amazon Resource Name (ARN).