diff --git a/.changelog/46869.txt b/.changelog/46869.txt new file mode 100644 index 000000000000..97e560cb0c43 --- /dev/null +++ b/.changelog/46869.txt @@ -0,0 +1,6 @@ +```release-note:new-list-resource +aws_iam_user +``` +```release-note:enhancement +resource/aws_iam_user: Add resource identity support +``` diff --git a/internal/service/iam/service_package_gen.go b/internal/service/iam/service_package_gen.go index 5fadf6fb6ed1..dfd3cdca003d 100644 --- a/internal/service/iam/service_package_gen.go +++ b/internal/service/iam/service_package_gen.go @@ -407,6 +407,12 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*inttypes.ServicePa ResourceType: "User", }), Region: unique.Make(inttypes.ResourceRegionDisabled()), + Identity: inttypes.GlobalSingleParameterIdentity(names.AttrName, + inttypes.WithMutableIdentity(), + ), + Import: inttypes.SDKv2Import{ + WrappedImport: true, + }, }, { Factory: resourceUserGroupMembership, @@ -495,6 +501,19 @@ func (p *servicePackage) SDKListResources(ctx context.Context) iter.Seq[*inttype inttypes.StringIdentityAttribute("policy_arn", true), }), }, + { + Factory: newUserResourceAsListResource, + TypeName: "aws_iam_user", + Name: "User", + Region: unique.Make(inttypes.ResourceRegionDisabled()), + Tags: unique.Make(inttypes.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrID, + ResourceType: "User", + }), + Identity: inttypes.GlobalSingleParameterIdentity(names.AttrName, + inttypes.WithMutableIdentity(), + ), + }, }) } diff --git a/internal/service/iam/testdata/User/basic/main_gen.tf b/internal/service/iam/testdata/User/basic/main_gen.tf new file mode 100644 index 000000000000..8ebab2776d8e --- /dev/null +++ b/internal/service/iam/testdata/User/basic/main_gen.tf @@ -0,0 +1,12 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_iam_user" "test" { + name = var.rName +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} diff --git a/internal/service/iam/testdata/User/basic_v6.35.1/main_gen.tf b/internal/service/iam/testdata/User/basic_v6.35.1/main_gen.tf new file mode 100644 index 000000000000..38e8a0af104f --- /dev/null +++ b/internal/service/iam/testdata/User/basic_v6.35.1/main_gen.tf @@ -0,0 +1,22 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_iam_user" "test" { + name = var.rName +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "6.35.1" + } + } +} + +provider "aws" {} diff --git a/internal/service/iam/testdata/User/list_basic/main.tf b/internal/service/iam/testdata/User/list_basic/main.tf new file mode 100644 index 000000000000..8bdd225b5859 --- /dev/null +++ b/internal/service/iam/testdata/User/list_basic/main.tf @@ -0,0 +1,20 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_iam_user" "test" { + count = var.resource_count + + name = "${var.rName}-${count.index}" +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "resource_count" { + description = "Number of resources to create" + type = number + nullable = false +} diff --git a/internal/service/iam/testdata/User/list_basic/query.tfquery.hcl b/internal/service/iam/testdata/User/list_basic/query.tfquery.hcl new file mode 100644 index 000000000000..300de6344518 --- /dev/null +++ b/internal/service/iam/testdata/User/list_basic/query.tfquery.hcl @@ -0,0 +1,6 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +list "aws_iam_user" "test" { + provider = aws +} diff --git a/internal/service/iam/testdata/User/list_include_resource/main.tf b/internal/service/iam/testdata/User/list_include_resource/main.tf new file mode 100644 index 000000000000..171e1af513c8 --- /dev/null +++ b/internal/service/iam/testdata/User/list_include_resource/main.tf @@ -0,0 +1,28 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_iam_user" "test" { + count = var.resource_count + + name = "${var.rName}-${count.index}" + + tags = var.resource_tags +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "resource_count" { + description = "Number of resources to create" + type = number + nullable = false +} + +variable "resource_tags" { + description = "Tags to set on resource" + type = map(string) + nullable = false +} diff --git a/internal/service/iam/testdata/User/list_include_resource/query.tfquery.hcl b/internal/service/iam/testdata/User/list_include_resource/query.tfquery.hcl new file mode 100644 index 000000000000..bb2907d13d8a --- /dev/null +++ b/internal/service/iam/testdata/User/list_include_resource/query.tfquery.hcl @@ -0,0 +1,8 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +list "aws_iam_user" "test" { + provider = aws + + include_resource = true +} diff --git a/internal/service/iam/testdata/User/list_path_prefix/main.tf b/internal/service/iam/testdata/User/list_path_prefix/main.tf new file mode 100644 index 000000000000..fafd27ed2275 --- /dev/null +++ b/internal/service/iam/testdata/User/list_path_prefix/main.tf @@ -0,0 +1,40 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_iam_user" "expected" { + count = var.resource_count + + name = "${var.rName}-${count.index}" + path = var.expected_path_name +} + +resource "aws_iam_user" "not_expected" { + count = var.resource_count + + name = "${var.rName}-other-${count.index}" + path = var.other_path_name +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "resource_count" { + description = "Number of resources to create" + type = number + nullable = false +} + +variable "expected_path_name" { + description = "Path name for expected resources" + type = string + nullable = false +} + +variable "other_path_name" { + description = "Path name for non-expected resources" + type = string + nullable = false +} diff --git a/internal/service/iam/testdata/User/list_path_prefix/query.tfquery.hcl b/internal/service/iam/testdata/User/list_path_prefix/query.tfquery.hcl new file mode 100644 index 000000000000..252f8b568285 --- /dev/null +++ b/internal/service/iam/testdata/User/list_path_prefix/query.tfquery.hcl @@ -0,0 +1,10 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +list "aws_iam_user" "test" { + provider = aws + + config { + path_prefix = var.expected_path_name + } +} diff --git a/internal/service/iam/user.go b/internal/service/iam/user.go index b9d4b2c0a8c6..6ae144f7ce0a 100644 --- a/internal/service/iam/user.go +++ b/internal/service/iam/user.go @@ -29,8 +29,13 @@ import ( ) // @SDKResource("aws_iam_user", name="User") +// @IdentityAttribute("name") +// @MutableIdentity // @Tags(identifierAttribute="id", resourceType="User") -// @Testing(existsType="github.com/aws/aws-sdk-go-v2/service/iam/types;types.User", importIgnore="force_destroy") +// @Testing(existsType="github.com/aws/aws-sdk-go-v2/service/iam/types;types.User") +// @Testing(importIgnore="force_destroy") +// @Testing(plannableImportAction="NoOp") +// @Testing(preIdentityVersion="v6.35.1") func resourceUser() *schema.Resource { return &schema.Resource{ CreateWithoutTimeout: resourceUserCreate, @@ -38,10 +43,6 @@ func resourceUser() *schema.Resource { UpdateWithoutTimeout: resourceUserUpdate, DeleteWithoutTimeout: resourceUserDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - Schema: map[string]*schema.Schema{ names.AttrARN: { Type: schema.TypeString, @@ -156,16 +157,7 @@ func resourceUserRead(ctx context.Context, d *schema.ResourceData, meta any) dia return sdkdiag.AppendErrorf(diags, "reading IAM User (%s): %s", d.Id(), err) } - d.Set(names.AttrARN, user.Arn) - d.Set(names.AttrName, user.UserName) - d.Set(names.AttrPath, user.Path) - if user.PermissionsBoundary != nil { - d.Set("permissions_boundary", user.PermissionsBoundary.PermissionsBoundaryArn) - } else { - d.Set("permissions_boundary", nil) - } - d.Set("unique_id", user.UserId) - + resourceUserFlatten(user, d) setTagsOut(ctx, user.Tags) return diags @@ -666,3 +658,15 @@ func retryCreateUser(ctx context.Context, conn *iam.Client, input *iam.CreateUse return output, err } + +func resourceUserFlatten(user *awstypes.User, d *schema.ResourceData) { + d.Set(names.AttrARN, user.Arn) + d.Set(names.AttrName, user.UserName) + d.Set(names.AttrPath, user.Path) + if user.PermissionsBoundary != nil { + d.Set("permissions_boundary", user.PermissionsBoundary.PermissionsBoundaryArn) + } else { + d.Set("permissions_boundary", nil) + } + d.Set("unique_id", user.UserId) +} diff --git a/internal/service/iam/user_identity_gen_test.go b/internal/service/iam/user_identity_gen_test.go new file mode 100644 index 000000000000..827cf61d84e9 --- /dev/null +++ b/internal/service/iam/user_identity_gen_test.go @@ -0,0 +1,222 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by internal/generate/identitytests/main.go; DO NOT EDIT. + +package iam_test + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/service/iam/types" + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfknownvalue "github.com/hashicorp/terraform-provider-aws/internal/acctest/knownvalue" + tfstatecheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/statecheck" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccIAMUser_Identity_basic(t *testing.T) { + ctx := acctest.Context(t) + + var v types.User + resourceName := "aws_iam_user.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID), + CheckDestroy: testAccCheckUserDestroy(ctx, t), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/User/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckUserExists(ctx, t, resourceName, &v), + ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrAccountID: tfknownvalue.AccountID(), + names.AttrName: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrName)), + }, + }, + + // Step 2: Import command + { + ConfigDirectory: config.StaticDirectory("testdata/User/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ImportStateKind: resource.ImportCommandWithID, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + names.AttrForceDestroy, + }, + }, + + // Step 3: Import block with Import ID + { + ConfigDirectory: config.StaticDirectory("testdata/User/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrName), knownvalue.NotNull()), + }, + }, + }, + + // Step 4: Import block with Resource Identity + { + ConfigDirectory: config.StaticDirectory("testdata/User/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrName), knownvalue.NotNull()), + }, + }, + }, + }, + }) +} + +// Resource Identity was added after v6.35.1 +func TestAccIAMUser_Identity_ExistingResource_basic(t *testing.T) { + ctx := acctest.Context(t) + + var v types.User + resourceName := "aws_iam_user.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID), + CheckDestroy: testAccCheckUserDestroy(ctx, t), + Steps: []resource.TestStep{ + // Step 1: Create pre-Identity + { + ConfigDirectory: config.StaticDirectory("testdata/User/basic_v6.35.1/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckUserExists(ctx, t, resourceName, &v), + ), + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectNoIdentity(resourceName), + }, + }, + + // Step 2: Current version + { + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ConfigDirectory: config.StaticDirectory("testdata/User/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrAccountID: tfknownvalue.AccountID(), + names.AttrName: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrName)), + }, + }, + }, + }) +} + +// Resource Identity was added after v6.35.1 +func TestAccIAMUser_Identity_ExistingResource_noRefreshNoChange(t *testing.T) { + ctx := acctest.Context(t) + + var v types.User + resourceName := "aws_iam_user.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID), + CheckDestroy: testAccCheckUserDestroy(ctx, t), + AdditionalCLIOptions: &resource.AdditionalCLIOptions{ + Plan: resource.PlanOptions{ + NoRefresh: true, + }, + }, + Steps: []resource.TestStep{ + // Step 1: Create pre-Identity + { + ConfigDirectory: config.StaticDirectory("testdata/User/basic_v6.35.1/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckUserExists(ctx, t, resourceName, &v), + ), + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectNoIdentity(resourceName), + }, + }, + + // Step 2: Current version + { + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ConfigDirectory: config.StaticDirectory("testdata/User/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectNoIdentity(resourceName), + }, + }, + }, + }) +} diff --git a/internal/service/iam/user_list.go b/internal/service/iam/user_list.go new file mode 100644 index 000000000000..e61e5f6273f2 --- /dev/null +++ b/internal/service/iam/user_list.go @@ -0,0 +1,129 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package iam + +import ( + "context" + "fmt" + "iter" + + "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/list" + listschema "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/logging" + inttypes "github.com/hashicorp/terraform-provider-aws/internal/types" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @SDKListResource("aws_iam_user") +func newUserResourceAsListResource() inttypes.ListResourceForSDK { + l := userListResource{} + l.SetResourceSchema(resourceUser()) + return &l +} + +var _ list.ListResource = &userListResource{} + +type userListResource struct { + framework.ListResourceWithSDKv2Resource +} + +type listUserModel struct { + PathPrefix types.String `tfsdk:"path_prefix"` +} + +func (l *userListResource) ListResourceConfigSchema(ctx context.Context, request list.ListResourceSchemaRequest, response *list.ListResourceSchemaResponse) { + response.Schema = listschema.Schema{ + Attributes: map[string]listschema.Attribute{ + "path_prefix": listschema.StringAttribute{ + Optional: true, + Description: "Path prefix on which to filter user names.", + Validators: []validator.String{ + validPolicyPathFramework, + }, + }, + }, + } +} + +func (l *userListResource) List(ctx context.Context, request list.ListRequest, stream *list.ListResultsStream) { + conn := l.Meta().IAMClient(ctx) + + var query listUserModel + if request.Config.Raw.IsKnown() && !request.Config.Raw.IsNull() { + if diags := request.Config.Get(ctx, &query); diags.HasError() { + stream.Results = list.ListResultsStreamDiagnostics(diags) + return + } + } + + pathPrefix := query.PathPrefix.ValueString() + + tflog.Info(ctx, "Listing Resources", map[string]any{ + logging.ResourceAttributeKey("path_prefix"): pathPrefix, + }) + + stream.Results = func(yield func(list.ListResult) bool) { + input := iam.ListUsersInput{ + PathPrefix: query.PathPrefix.ValueStringPointer(), + } + for item, err := range listUsers(ctx, conn, &input) { + if err != nil { + result := fwdiag.NewListResultErrorDiagnostic(err) + yield(result) + return + } + name := aws.ToString(item.UserName) + ctx := tflog.SetField(ctx, logging.ResourceAttributeKey(names.AttrName), name) + + result := request.NewListResult(ctx) + + rd := l.ResourceData() + rd.SetId(name) + rd.Set(names.AttrName, name) + + if request.IncludeResource { + resourceUserFlatten(&item, rd) + } + + result.DisplayName = aws.ToString(item.UserName) + + l.SetResult(ctx, l.Meta(), request.IncludeResource, rd, &result) + if result.Diagnostics.HasError() { + yield(result) + return + } + + if !yield(result) { + return + } + } + } +} + +func listUsers(ctx context.Context, conn *iam.Client, input *iam.ListUsersInput) iter.Seq2[awstypes.User, error] { + return func(yield func(awstypes.User, error) bool) { + pages := iam.NewListUsersPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if err != nil { + yield(awstypes.User{}, fmt.Errorf("listing IAM User resources: %w", err)) + return + } + + for _, item := range page.Users { + if !yield(item, nil) { + return + } + } + } + } +} diff --git a/internal/service/iam/user_list_test.go b/internal/service/iam/user_list_test.go new file mode 100644 index 000000000000..c722a5d5f38f --- /dev/null +++ b/internal/service/iam/user_list_test.go @@ -0,0 +1,223 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package iam_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfknownvalue "github.com/hashicorp/terraform-provider-aws/internal/acctest/knownvalue" + tfquerycheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/querycheck" + tfqueryfilter "github.com/hashicorp/terraform-provider-aws/internal/acctest/queryfilter" + tfstatecheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/statecheck" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccIAMUser_List_basic(t *testing.T) { + ctx := acctest.Context(t) + + resourceName1 := "aws_iam_user.test[0]" + resourceName2 := "aws_iam_user.test[1]" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + identity1 := tfstatecheck.Identity() + identity2 := tfstatecheck.Identity() + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { + acctest.PreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID), + CheckDestroy: testAccCheckUserDestroy(ctx, t), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/User/list_basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + }, + ConfigStateChecks: []statecheck.StateCheck{ + identity1.GetIdentity(resourceName1), + statecheck.ExpectKnownValue(resourceName1, tfjsonpath.New(names.AttrName), knownvalue.StringExact(rName+"-0")), + + identity2.GetIdentity(resourceName2), + statecheck.ExpectKnownValue(resourceName2, tfjsonpath.New(names.AttrName), knownvalue.StringExact(rName+"-1")), + }, + }, + + // Step 2: Query + { + Query: true, + ConfigDirectory: config.StaticDirectory("testdata/User/list_basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + }, + QueryResultChecks: []querycheck.QueryResultCheck{ + tfquerycheck.ExpectIdentityFunc("aws_iam_user.test", identity1.Checks()), + querycheck.ExpectResourceDisplayName("aws_iam_user.test", tfqueryfilter.ByResourceIdentityFunc(identity1.Checks()), knownvalue.StringExact(rName+"-0")), + tfquerycheck.ExpectNoResourceObject("aws_iam_user.test", tfqueryfilter.ByResourceIdentityFunc(identity1.Checks())), + + tfquerycheck.ExpectIdentityFunc("aws_iam_user.test", identity2.Checks()), + querycheck.ExpectResourceDisplayName("aws_iam_user.test", tfqueryfilter.ByResourceIdentityFunc(identity2.Checks()), knownvalue.StringExact(rName+"-1")), + tfquerycheck.ExpectNoResourceObject("aws_iam_user.test", tfqueryfilter.ByResourceIdentityFunc(identity2.Checks())), + }, + }, + }, + }) +} + +func TestAccIAMUser_List_includeResource(t *testing.T) { + ctx := acctest.Context(t) + + resourceName1 := "aws_iam_user.test[0]" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + identity1 := tfstatecheck.Identity() + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { + acctest.PreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID), + CheckDestroy: testAccCheckUserDestroy(ctx, t), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/User/list_include_resource/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(1), + acctest.CtResourceTags: config.MapVariable(map[string]config.Variable{ + acctest.CtKey1: config.StringVariable(acctest.CtValue1), + }), + }, + ConfigStateChecks: []statecheck.StateCheck{ + identity1.GetIdentity(resourceName1), + statecheck.ExpectKnownValue(resourceName1, tfjsonpath.New(names.AttrName), knownvalue.StringExact(rName+"-0")), + }, + }, + + // Step 2: Query + { + Query: true, + ConfigDirectory: config.StaticDirectory("testdata/User/list_include_resource/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(1), + acctest.CtResourceTags: config.MapVariable(map[string]config.Variable{ + acctest.CtKey1: config.StringVariable(acctest.CtValue1), + }), + }, + QueryResultChecks: []querycheck.QueryResultCheck{ + tfquerycheck.ExpectIdentityFunc("aws_iam_user.test", identity1.Checks()), + querycheck.ExpectResourceDisplayName("aws_iam_user.test", tfqueryfilter.ByResourceIdentityFunc(identity1.Checks()), knownvalue.StringExact(rName+"-0")), + querycheck.ExpectResourceKnownValues("aws_iam_user.test", tfqueryfilter.ByResourceIdentityFunc(identity1.Checks()), []querycheck.KnownValueCheck{ + tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrARN), tfknownvalue.GlobalARNExact("iam", "user/"+rName+"-0")), + tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrForceDestroy), knownvalue.Null()), + tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrID), knownvalue.StringExact(rName+"-0")), + tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrName), knownvalue.StringExact(rName+"-0")), + tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrPath), knownvalue.StringExact("/")), + tfquerycheck.KnownValueCheck(tfjsonpath.New("permissions_boundary"), knownvalue.NotNull()), + tfquerycheck.KnownValueCheck(tfjsonpath.New("unique_id"), knownvalue.NotNull()), + tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrTags), knownvalue.MapExact(map[string]knownvalue.Check{ + acctest.CtKey1: knownvalue.StringExact(acctest.CtValue1), + })), + tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrTagsAll), knownvalue.MapExact(map[string]knownvalue.Check{ + acctest.CtKey1: knownvalue.StringExact(acctest.CtValue1), + })), + }), + }, + }, + }, + }) +} + +func TestAccIAMUser_List_pathPrefix(t *testing.T) { + ctx := acctest.Context(t) + + resourceNameExpected1 := "aws_iam_user.expected[0]" + resourceNameExpected2 := "aws_iam_user.expected[1]" + resourceNameNotExpected1 := "aws_iam_user.not_expected[0]" + resourceNameNotExpected2 := "aws_iam_user.not_expected[1]" + + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + rPathName := "/" + acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + "/" + rOtherPathName := "/" + acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + "/" + + identityExpected1 := tfstatecheck.Identity() + identityExpected2 := tfstatecheck.Identity() + identityNotExpected1 := tfstatecheck.Identity() + identityNotExpected2 := tfstatecheck.Identity() + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { + acctest.PreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID), + CheckDestroy: testAccCheckUserDestroy(ctx, t), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/User/list_path_prefix/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + "expected_path_name": config.StringVariable(rPathName), + "other_path_name": config.StringVariable(rOtherPathName), + }, + ConfigStateChecks: []statecheck.StateCheck{ + identityExpected1.GetIdentity(resourceNameExpected1), + statecheck.ExpectKnownValue(resourceNameExpected1, tfjsonpath.New(names.AttrName), knownvalue.StringExact(rName+"-0")), + + identityExpected2.GetIdentity(resourceNameExpected2), + statecheck.ExpectKnownValue(resourceNameExpected2, tfjsonpath.New(names.AttrName), knownvalue.StringExact(rName+"-1")), + + identityNotExpected1.GetIdentity(resourceNameNotExpected1), + statecheck.ExpectKnownValue(resourceNameNotExpected1, tfjsonpath.New(names.AttrName), knownvalue.StringExact(rName+"-other-0")), + + identityNotExpected2.GetIdentity(resourceNameNotExpected2), + statecheck.ExpectKnownValue(resourceNameNotExpected2, tfjsonpath.New(names.AttrName), knownvalue.StringExact(rName+"-other-1")), + }, + }, + + // Step 2: Query + { + Query: true, + ConfigDirectory: config.StaticDirectory("testdata/User/list_path_prefix/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + "expected_path_name": config.StringVariable(rPathName), + "other_path_name": config.StringVariable(rOtherPathName), + }, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectLength("aws_iam_user.test", 2), + tfquerycheck.ExpectIdentityFunc("aws_iam_user.test", identityExpected1.Checks()), + tfquerycheck.ExpectIdentityFunc("aws_iam_user.test", identityExpected2.Checks()), + }, + }, + }, + }) +} diff --git a/names/data/names_data.hcl b/names/data/names_data.hcl index 0ae4522ee6fb..8752f20e806d 100644 --- a/names/data/names_data.hcl +++ b/names/data/names_data.hcl @@ -4203,8 +4203,9 @@ service "iam" { } names { - provider_name_upper = "IAM" - human_friendly = "IAM (Identity & Access Management)" + provider_name_upper = "IAM" + human_friendly = "IAM (Identity & Access Management)" + human_friendly_short = "IAM" } env_var { diff --git a/website/docs/list-resources/iam_user.html.markdown b/website/docs/list-resources/iam_user.html.markdown new file mode 100644 index 000000000000..c945edd46b6d --- /dev/null +++ b/website/docs/list-resources/iam_user.html.markdown @@ -0,0 +1,43 @@ +--- +subcategory: "IAM (Identity & Access Management)" +layout: "aws" +page_title: "AWS: aws_iam_user" +description: |- + Lists IAM (Identity & Access Management) User resources. +--- + +# List Resource: aws_iam_user + +Lists IAM (Identity & Access Management) User resources. + +## Example Usage + +### Basic Usage + +```terraform +list "aws_iam_user" "example" { + provider = aws +} +``` + +### Filter by Path Prefix + +This example will return IAM Users with a `path` equal to or beginning with `/example/`. + +```terraform +list "aws_iam_user" "example" { + provider = aws + + config { + path_prefix = "/example/" + } +} +``` + +## Argument Reference + +This list resource supports the following arguments: + +* `path_prefix` - (Optional) Limits the returned IAM Users to those within this path. + If `path_prefix` is not specified, or is `"/"`, returns all IAM Users. + Must begin and end with a slash (`/`) and contain uppercase or lowercase alphanumeric characters or any of the following: `/`, `,`, `.`, `+`, `@`, `=`, `_`, or `-`. diff --git a/website/docs/r/iam_user.html.markdown b/website/docs/r/iam_user.html.markdown index af4de367decb..43b469fb0c98 100644 --- a/website/docs/r/iam_user.html.markdown +++ b/website/docs/r/iam_user.html.markdown @@ -69,17 +69,43 @@ This resource exports the following attributes in addition to the arguments abov ## Import +In Terraform v1.12.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `identity` attribute. For example: + +```terraform +import { + to = aws_iam_user.example + identity = { + name = "example-user" + } +} + +resource "aws_iam_user" "example" { + ### Configuration omitted for brevity ### +} +``` + +### Identity Schema + +#### Required + +* `name` (String) User name. + +#### Optional + +* `account_id` (String) AWS Account where this resource is managed. +* `region` (String) Region where this resource is managed. + In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import IAM Users using the `name`. For example: ```terraform import { - to = aws_iam_user.lb - id = "loadbalancer" + to = aws_iam_user.example + id = "example-user" } ``` Using `terraform import`, import IAM Users using the `name`. For example: ```console -% terraform import aws_iam_user.lb loadbalancer +% terraform import aws_iam_user.example example-user ```