Skip to content

Commit

Permalink
Merge pull request #36723 from hashicorp/f-trim_iam_role_path
Browse files Browse the repository at this point in the history
[New Function]: `trim_iam_role_path`
  • Loading branch information
jar-b authored Apr 4, 2024
2 parents 2a0c603 + 0c04284 commit 3e38415
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/36723.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-function
trim_iam_role_path
```
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
92 changes: 92 additions & 0 deletions internal/function/trim_iam_role_path.go
Original file line number Diff line number Diff line change
@@ -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
}
146 changes: 146 additions & 0 deletions internal/function/trim_iam_role_path_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions internal/provider/fwprovider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ func (p *fwprovider) Functions(_ context.Context) []func() function.Function {
return []func() function.Function{
tffunction.NewARNBuildFunction,
tffunction.NewARNParseFunction,
tffunction.NewTrimIAMRolePathFunction,
}
}

Expand Down
35 changes: 35 additions & 0 deletions website/docs/functions/trim_iam_role_path.html.markdown
Original file line number Diff line number Diff line change
@@ -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).

0 comments on commit 3e38415

Please sign in to comment.