diff --git a/.changelog/42659.txt b/.changelog/42659.txt new file mode 100644 index 000000000000..f84a0f182f4a --- /dev/null +++ b/.changelog/42659.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_iam_service_linked_role +``` \ No newline at end of file diff --git a/internal/service/iam/service_linked_role_data_source.go b/internal/service/iam/service_linked_role_data_source.go new file mode 100644 index 000000000000..c85c3d029550 --- /dev/null +++ b/internal/service/iam/service_linked_role_data_source.go @@ -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 +} diff --git a/internal/service/iam/service_linked_role_data_source_test.go b/internal/service/iam/service_linked_role_data_source_test.go new file mode 100644 index 000000000000..31a8819400c3 --- /dev/null +++ b/internal/service/iam/service_linked_role_data_source_test.go @@ -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) +} diff --git a/internal/service/iam/service_package_gen.go b/internal/service/iam/service_package_gen.go index 3bb2a1050246..1e163b0dade3 100644 --- a/internal/service/iam/service_package_gen.go +++ b/internal/service/iam/service_package_gen.go @@ -15,7 +15,13 @@ import ( type servicePackage struct{} func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.ServicePackageFrameworkDataSource { - return []*types.ServicePackageFrameworkDataSource{} + return []*types.ServicePackageFrameworkDataSource{ + { + Factory: newDataSourceServiceLinkedRole, + TypeName: "aws_iam_service_linked_role", + Name: "Service Linked Role", + }, + } } func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.ServicePackageFrameworkResource { diff --git a/internal/service/iam/sweep.go b/internal/service/iam/sweep.go index ac7c6dc28b55..b3d90b04260d 100644 --- a/internal/service/iam/sweep.go +++ b/internal/service/iam/sweep.go @@ -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) diff --git a/website/docs/d/iam_service_linked_role.markdown b/website/docs/d/iam_service_linked_role.markdown new file mode 100644 index 000000000000..af36af395256 --- /dev/null +++ b/website/docs/d/iam_service_linked_role.markdown @@ -0,0 +1,43 @@ +--- +subcategory: "IAM (Identity & Access Management)" +layout: "aws" +page_title: "AWS: aws_iam_service_linked_role" +description: |- + Get information on a Amazon IAM Service Linked role +--- + +# Data Source: aws_iam_service_linked_role + +This data source can be used to fetch information about a specific +IAM Service Linked role. By using this data source, you can reference IAM role +properties without having to hard code ARNs as input. + +~> **NOTE:** This data source can ensure if the Service Linked role exists. In this scenario, if `create_if_missing` is set, Terraform will not _create_ or _adopt_ this resource, but instead will create the role to ensure if exists before exporting the data. Please use the `aws_iam_service_linked_role` resource to manage a service linked role. + +## Example Usage + +```terraform +data "aws_iam_service_linked_role" "example" { + aws_service_name = "elasticbeanstalk.amazonaws.com" +} +``` + +## Argument Reference + +This data source supports the following arguments: + +* `aws_service_name` - (Required) The AWS service to which this role is attached. You use a string similar to a URL but without the `http://` in front. For example: `elasticbeanstalk.amazonaws.com`. To find the full list of services that support service-linked roles, check [the docs](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html). +* `custom_suffix` - (Optional) Additional string appended to the role name. Not all AWS services support custom suffixes. +* `create_if_missing` - (Optional) This will create the service linked role if it does not exists. Default value is `false` + +## Attribute Reference + +This data source exports the following attributes in addition to the arguments above: + +* `id` - The Amazon Resource Name (ARN) of the role. +* `arn` - The Amazon Resource Name (ARN) specifying the role. +* `create_date` - The creation date of the IAM role. +* `description` - The description of the role. +* `name` - The name of the role. +* `path` - The path of the role. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block).