Skip to content

d/aws_iam_service_linked_role: new data source can ensure the role exists #42659

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/42659.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-data-source
aws_iam_service_linked_role
```
167 changes: 167 additions & 0 deletions internal/service/iam/service_linked_role_data_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package iam

import (
"context"
"fmt"

"github.com/YakDriver/regexache"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam"
awstypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-provider-aws/internal/framework"
fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex"
"github.com/hashicorp/terraform-provider-aws/names"
)

// @FrameworkDataSource("aws_iam_service_linked_role",name="Service Linked Role")
func newDataSourceServiceLinkedRole(context.Context) (datasource.DataSourceWithConfigure, error) {
return &dataSourceServiceLinkedRole{}, nil
}

type dataSourceServiceLinkedRole struct {
framework.DataSourceWithConfigure
}

func (d *dataSourceServiceLinkedRole) Schema(ctx context.Context, request datasource.SchemaRequest, response *datasource.SchemaResponse) {
response.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
names.AttrARN: schema.StringAttribute{
Computed: true,
},
"aws_service_name": schema.StringAttribute{
Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
regexache.MustCompile(`\.`),
"must be a full service hostname e.g. elasticbeanstalk.amazonaws.com",
),
},
},
"custom_suffix": schema.StringAttribute{
Optional: true,
},
"create_if_missing": schema.BoolAttribute{
Optional: true,
},
"create_date": schema.StringAttribute{
CustomType: timetypes.RFC3339Type{},
Computed: true,
},
names.AttrDescription: schema.StringAttribute{
Computed: true,
},
names.AttrName: schema.StringAttribute{
Computed: true,
},
names.AttrPath: schema.StringAttribute{
Computed: true,
},
"unique_id": schema.StringAttribute{
Computed: true,
},
},
}
}

func (d *dataSourceServiceLinkedRole) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) {
var data ServiceLinkedRoleDataSourceModel

response.Diagnostics.Append(request.Config.Get(ctx, &data)...)
if response.Diagnostics.HasError() {
return
}

var role *awstypes.Role
conn := d.Meta().IAMClient(ctx)

//AWS API does not provide a Get/List method for Service Linked Roles.
//Matching the role path prefix and role name using regex is the only option to find Service Linked roles
var nameRegex string
pathPrefix := fmt.Sprintf("/aws-service-role/%s", data.AWSServiceName.ValueString())
if data.CustomSuffix.ValueString() == "" {
//regex to match AWSServiceRole prefix and 1 or more characters exluding _ (underscore)
nameRegex = `AWSServiceRole[^_]+$`
} else {
//regex to match AWSServiceRole prefix and any role name, _ (underscore) and the provided suffix
nameRegex = fmt.Sprintf(`AWSServiceRole[0-9A-Za-z]+_%s$`, data.CustomSuffix.ValueString())
}
roles, err := findRoles(ctx, conn, pathPrefix, nameRegex)
if err != nil {
response.Diagnostics.AddError(fmt.Sprintf("reading IAM Service Linked Role (%s)", data.AWSServiceName.ValueString()), err.Error())
return
}
switch len(roles) {
case 0:
if data.CreateIfMissing.ValueBool() {
input := &iam.CreateServiceLinkedRoleInput{
AWSServiceName: data.AWSServiceName.ValueStringPointer(),
}
if data.CustomSuffix.ValueString() != "" {
input.CustomSuffix = data.CustomSuffix.ValueStringPointer()
}
output, err := conn.CreateServiceLinkedRole(ctx, input)
if err != nil {
response.Diagnostics.AddError(fmt.Sprintf("creating IAM Service Linked Role (%s)", data.AWSServiceName.ValueString()), err.Error())
return
}
role = output.Role
} else {
response.Diagnostics.AddError(fmt.Sprintf("reading IAM Service Linked Role (%s)", data.AWSServiceName.ValueString()), "Role was not found.")
return
}
case 1:
role = &roles[0]
default:
response.Diagnostics.AddError(fmt.Sprintf("reading IAM Service Linked Role (%s)", data.AWSServiceName.ValueString()), "More than one role was returned.")
}
response.Diagnostics.Append(fwflex.Flatten(ctx, role, &data)...)
if response.Diagnostics.HasError() {
return
}
response.Diagnostics.Append(response.State.Set(ctx, &data)...)
}

type ServiceLinkedRoleDataSourceModel struct {
ARN types.String `tfsdk:"arn"`
AWSServiceName types.String `tfsdk:"aws_service_name"`
CreateDate timetypes.RFC3339 `tfsdk:"create_date"`
CreateIfMissing types.Bool `tfsdk:"create_if_missing"`
CustomSuffix types.String `tfsdk:"custom_suffix"`
Description types.String `tfsdk:"description"`
Path types.String `tfsdk:"path"`
RoleId types.String `tfsdk:"unique_id"`
RoleName types.String `tfsdk:"name"`
}

func findRoles(ctx context.Context, conn *iam.Client, pathPrefix string, nameRegex string) ([]awstypes.Role, error) {
var results []awstypes.Role

input := &iam.ListRolesInput{}
if pathPrefix != "" {
input.PathPrefix = &pathPrefix
}

pages := iam.NewListRolesPaginator(conn, input)
for pages.HasMorePages() {
page, err := pages.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("reading IAM roles: %s", err)
}
for _, role := range page.Roles {
if nameRegex != "" && !regexache.MustCompile(nameRegex).MatchString(aws.ToString(role.RoleName)) {
continue
}
results = append(results, role)
}
}
return results, nil
}
188 changes: 188 additions & 0 deletions internal/service/iam/service_linked_role_data_source_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package iam_test

import (
"fmt"
"testing"

"github.com/YakDriver/regexache"
"github.com/aws/aws-sdk-go-v2/aws/arn"
sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
tfiam "github.com/hashicorp/terraform-provider-aws/internal/service/iam"
"github.com/hashicorp/terraform-provider-aws/names"
)

func TestAccIAMServiceLinkedRoleDataSource_basic(t *testing.T) {
ctx := acctest.Context(t)
dataSourceName := "data.aws_iam_service_linked_role.test"
resourceName := "aws_iam_service_linked_role.test"
awsServiceName := "airflow.amazonaws.com"
name := "AWSServiceRoleForAmazonMWAA"
path := fmt.Sprintf("/aws-service-role/%s/", awsServiceName)
arnResource := fmt.Sprintf("role%s%s", path, name)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{

{
PreConfig: func() {
// Remove existing if possible
client := acctest.Provider.Meta().(*conns.AWSClient)
arn := arn.ARN{
Partition: client.Partition(ctx),
Service: "iam",
Region: client.Region(ctx),
AccountID: client.AccountID(ctx),
Resource: arnResource,
}.String()
r := tfiam.ResourceServiceLinkedRole()
d := r.Data(nil)
d.SetId(arn)
err := acctest.DeleteResource(ctx, r, d, client)

if err != nil {
t.Fatalf("deleting service-linked role %s: %s", name, err)
}
},
Config: testAccServiceLinkedRoleDataSourceConfig_basic(awsServiceName),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrPair(dataSourceName, names.AttrARN, resourceName, names.AttrARN),
resource.TestCheckResourceAttrPair(dataSourceName, "aws_service_name", resourceName, "aws_service_name"),
acctest.CheckResourceAttrRFC3339(dataSourceName, "create_date"),
resource.TestCheckResourceAttrPair(dataSourceName, "unique_id", resourceName, "unique_id"),
resource.TestCheckResourceAttrPair(dataSourceName, names.AttrName, resourceName, names.AttrName),
resource.TestCheckResourceAttrPair(dataSourceName, names.AttrPath, resourceName, names.AttrPath),
resource.TestCheckResourceAttr(dataSourceName, acctest.CtTagsPercent, "0"),
),
},
},
})
}

func TestAccIAMServiceLinkedRoleDataSource_customSuffix(t *testing.T) {
ctx := acctest.Context(t)
dataSourceName := "data.aws_iam_service_linked_role.test"
resourceName := "aws_iam_service_linked_role.test"
awsServiceName := "autoscaling.amazonaws.com"
rCustomSufix := sdkacctest.RandString(10)
rDescription := "This is a service linked role"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{

{
Config: testAccServiceLinkedRoleDataSourceConfig_customSuffix(awsServiceName, rCustomSufix, rDescription),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrPair(dataSourceName, names.AttrARN, resourceName, names.AttrARN),
resource.TestCheckResourceAttrPair(dataSourceName, "aws_service_name", resourceName, "aws_service_name"),
resource.TestCheckResourceAttrPair(dataSourceName, "custom_suffix", resourceName, "custom_suffix"),
resource.TestCheckResourceAttrPair(dataSourceName, names.AttrDescription, resourceName, names.AttrDescription),
acctest.CheckResourceAttrRFC3339(dataSourceName, "create_date"),
resource.TestCheckResourceAttrPair(dataSourceName, "unique_id", resourceName, "unique_id"),
resource.TestCheckResourceAttr(dataSourceName, acctest.CtTagsPercent, "0"),
),
},
},
})
}

func TestAccIAMServiceLinkedRoleDataSource_createIfMissing(t *testing.T) {
ctx := acctest.Context(t)
dataSourceName := "data.aws_iam_service_linked_role.test"
awsServiceName := "autoscaling.amazonaws.com"
name := "AWSServiceRoleForAutoScaling"
customSuffix := "ServiceLinkedRoleDataSource"
path := fmt.Sprintf("/aws-service-role/%s/", awsServiceName)
arnResource := fmt.Sprintf("role%s%s_%s", path, name, customSuffix)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{

{
PreConfig: func() {
// Remove existing if possible
client := acctest.Provider.Meta().(*conns.AWSClient)
arn := arn.ARN{
Partition: client.Partition(ctx),
Service: "iam",
Region: client.Region(ctx),
AccountID: client.AccountID(ctx),
Resource: arnResource,
}.String()
r := tfiam.ResourceServiceLinkedRole()
d := r.Data(nil)
d.SetId(arn)
err := acctest.DeleteResource(ctx, r, d, client)

if err != nil {
t.Fatalf("deleting service-linked role %s: %s", name, err)
}
},
Config: testAccServiceLinkedRoleDataSourceConfig_createIfMissing(awsServiceName, customSuffix, false),
ExpectError: regexache.MustCompile("Role was not found"),
},
{
Config: testAccServiceLinkedRoleDataSourceConfig_createIfMissing(awsServiceName, customSuffix, true),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(dataSourceName, "aws_service_name", awsServiceName),
acctest.CheckResourceAttrRFC3339(dataSourceName, "create_date"),
resource.TestCheckResourceAttrSet(dataSourceName, "unique_id"),
resource.TestCheckResourceAttr(dataSourceName, acctest.CtTagsPercent, "0"),
),
},
},
})
}

func testAccServiceLinkedRoleDataSourceConfig_basic(awsServiceName string) string {
return fmt.Sprintf(`
resource "aws_iam_service_linked_role" "test" {
aws_service_name = %[1]q
description = "This is a service linked role"
}

data "aws_iam_service_linked_role" "test" {
aws_service_name = aws_iam_service_linked_role.test.aws_service_name
}
`, awsServiceName)
}

func testAccServiceLinkedRoleDataSourceConfig_customSuffix(awsServiceName string, customSuffix string, description string) string {
return fmt.Sprintf(`
resource "aws_iam_service_linked_role" "test" {
aws_service_name = %[1]q
custom_suffix = %[2]q
description = %[3]q
}

data "aws_iam_service_linked_role" "test" {
aws_service_name = aws_iam_service_linked_role.test.aws_service_name
custom_suffix = aws_iam_service_linked_role.test.custom_suffix
}
`, awsServiceName, customSuffix, description)
}

func testAccServiceLinkedRoleDataSourceConfig_createIfMissing(awsServiceName string, customSufix string, createIfMissing bool) string {
return fmt.Sprintf(`

data "aws_iam_service_linked_role" "test" {
aws_service_name = %[1]q
create_if_missing = %[2]t
custom_suffix = %[3]q
}
`, awsServiceName, createIfMissing, customSufix)
}
8 changes: 7 additions & 1 deletion internal/service/iam/service_package_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion internal/service/iam/sweep.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,8 @@ func sweepServiceLinkedRoles(ctx context.Context, client *conns.AWSClient) ([]sw
// include generic service role names created by:
// TestAccIAMServiceLinkedRole_basic
// TestAccIAMServiceLinkedRole_CustomSuffix_diffSuppressFunc
customSuffixRegex := regexache.MustCompile(`_?(tf-acc-test-\d+|ServiceRoleForApplicationAutoScaling_CustomResource)$`)
// TestAccIAMServiceLinkedRoleDataSource_createIfMissing
customSuffixRegex := regexache.MustCompile(`_?(tf-acc-test-\d+|ServiceRoleForApplicationAutoScaling_CustomResource|AWSServiceRoleForAutoScaling_ServiceLinkedRoleDataSource)$`)
pages := iam.NewListRolesPaginator(conn, input)
for pages.HasMorePages() {
page, err := pages.NextPage(ctx)
Expand Down
Loading
Loading