From 79e113389229bcedf30a3921acf8b59f941d5139 Mon Sep 17 00:00:00 2001 From: Owen Farrell Date: Tue, 7 Jun 2022 21:06:11 -0400 Subject: [PATCH 01/21] r/aws_detective_admin_account - new resource Signed-off-by: Owen Farrell --- internal/service/detective/detective_test.go | 5 + internal/service/detective/find.go | 26 +++ .../detective/organization_admin_account.go | 112 +++++++++ .../organization_admin_account_test.go | 213 ++++++++++++++++++ .../service/detective/service_package_gen.go | 4 + internal/service/detective/status.go | 54 +++++ internal/service/detective/wait.go | 43 +++- ...e_organization_admin_account.html.markdown | 46 ++++ 8 files changed, 502 insertions(+), 1 deletion(-) create mode 100644 internal/service/detective/organization_admin_account.go create mode 100644 internal/service/detective/organization_admin_account_test.go create mode 100644 website/docs/r/detective_organization_admin_account.html.markdown diff --git a/internal/service/detective/detective_test.go b/internal/service/detective/detective_test.go index e59ff06b2a4..fed3f9a8fae 100644 --- a/internal/service/detective/detective_test.go +++ b/internal/service/detective/detective_test.go @@ -24,6 +24,11 @@ func TestAccDetective_serial(t *testing.T) { "disappear": testAccMember_disappears, "message": testAccMember_message, }, + "OrganizationAdminAccount": { + "basic": testAccOrganizationAdminAccount_basic, + "disappears": testAccOrganizationAdminAccount_disappears, + "MultiRegion": testAccOrganizationAdminAccount_MultiRegion, + }, } acctest.RunSerialTests2Levels(t, testCases, 0) diff --git a/internal/service/detective/find.go b/internal/service/detective/find.go index 7324ee02f2c..70ce14e3bd2 100644 --- a/internal/service/detective/find.go +++ b/internal/service/detective/find.go @@ -10,6 +10,32 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" ) +func FindAdminAccount(ctx context.Context, conn *detective.Detective, adminAccountID string) (*detective.Administrator, error) { + input := &detective.ListOrganizationAdminAccountsInput{} + var result *detective.Administrator + + err := conn.ListOrganizationAdminAccountsPagesWithContext(ctx, input, func(page *detective.ListOrganizationAdminAccountsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, adminAccount := range page.Administrators { + if adminAccount == nil { + continue + } + + if aws.StringValue(adminAccount.AccountId) == adminAccountID { + result = adminAccount + return false + } + } + + return !lastPage + }) + + return result, err +} + func FindGraphByARN(ctx context.Context, conn *detective.Detective, arn string) (*detective.Graph, error) { input := &detective.ListGraphsInput{} var result *detective.Graph diff --git a/internal/service/detective/organization_admin_account.go b/internal/service/detective/organization_admin_account.go new file mode 100644 index 00000000000..9164a3469aa --- /dev/null +++ b/internal/service/detective/organization_admin_account.go @@ -0,0 +1,112 @@ +package detective + +import ( + "context" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/detective" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +// @SDKResource("aws_detective_organization_admin_account") +func ResourceOrganizationAdminAccount() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceOrganizationAdminAccountCreate, + ReadWithoutTimeout: resourceOrganizationAdminAccountRead, + DeleteWithoutTimeout: resourceOrganizationAdminAccountDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "account_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: verify.ValidAccountID, + }, + }, + } +} + +func resourceOrganizationAdminAccountCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).DetectiveConn() + + accountID := d.Get("account_id").(string) + + input := &detective.EnableOrganizationAdminAccountInput{ + AccountId: aws.String(accountID), + } + + _, err := conn.EnableOrganizationAdminAccountWithContext(ctx, input) + + if err != nil { + return diag.Errorf("error enabling Detective Organization Admin Account (%s): %s", accountID, err) + } + + d.SetId(accountID) + + if _, err := waitAdminAccountFound(ctx, conn, d.Id()); err != nil { + return diag.Errorf("error waiting for Detective Organization Admin Account (%s) to enable: %s", d.Id(), err) + } + + return resourceOrganizationAdminAccountRead(ctx, d, meta) +} + +func resourceOrganizationAdminAccountRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).DetectiveConn() + + adminAccount, err := FindAdminAccount(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] Detective Organization Admin Account (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("error reading Detective Organization Admin Account (%s): %s", d.Id(), err) + } + + if adminAccount == nil { + if d.IsNewResource() { + return diag.Errorf("error reading Detective Organization Admin Account (%s): %s", d.Id(), err) + } + + log.Printf("[WARN] Detective Organization Admin Account (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + d.Set("account_id", adminAccount.AccountId) + + return nil +} + +func resourceOrganizationAdminAccountDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).DetectiveConn() + + input := &detective.DisableOrganizationAdminAccountInput{} + + _, err := conn.DisableOrganizationAdminAccountWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return diag.Errorf("error disabling Detective Organization Admin Account (%s): %s", d.Id(), err) + } + + if _, err := waitAdminAccountNotFound(ctx, conn, d.Id()); err != nil { + return diag.Errorf("error waiting for Detective Organization Admin Account (%s) to disable: %s", d.Id(), err) + } + + return nil +} diff --git a/internal/service/detective/organization_admin_account_test.go b/internal/service/detective/organization_admin_account_test.go new file mode 100644 index 00000000000..e99c35389ba --- /dev/null +++ b/internal/service/detective/organization_admin_account_test.go @@ -0,0 +1,213 @@ +package detective_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/detective" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfdetective "github.com/hashicorp/terraform-provider-aws/internal/service/detective" +) + +func testAccOrganizationAdminAccount_basic(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_detective_organization_admin_account.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckOrganizationsAccount(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, detective.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckOrganizationAdminAccountDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccOrganizationAdminAccountConfig_self(), + Check: resource.ComposeTestCheckFunc( + testAccCheckOrganizationAdminAccountExists(ctx, resourceName), + acctest.CheckResourceAttrAccountID(resourceName, "account_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccOrganizationAdminAccount_disappears(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_detective_organization_admin_account.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckOrganizationsAccount(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, detective.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckOrganizationAdminAccountDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccOrganizationAdminAccountConfig_self(), + Check: resource.ComposeTestCheckFunc( + testAccCheckOrganizationAdminAccountExists(ctx, resourceName), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfdetective.ResourceOrganizationAdminAccount(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccOrganizationAdminAccount_MultiRegion(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_detective_organization_admin_account.test" + altResourceName := "aws_detective_organization_admin_account.alternate" + thirdResourceName := "aws_detective_organization_admin_account.third" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckOrganizationsAccount(ctx, t) + acctest.PreCheckMultipleRegion(t, 3) + }, + ErrorCheck: acctest.ErrorCheck(t, detective.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5FactoriesMultipleRegions(ctx, t, 3), + CheckDestroy: testAccCheckOrganizationAdminAccountDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccOrganizationAdminAccountConfig_multiRegion(), + Check: resource.ComposeTestCheckFunc( + testAccCheckOrganizationAdminAccountExists(ctx, resourceName), + testAccCheckOrganizationAdminAccountExists(ctx, altResourceName), + testAccCheckOrganizationAdminAccountExists(ctx, thirdResourceName), + ), + }, + }, + }) +} + +func testAccCheckOrganizationAdminAccountDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).DetectiveConn() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_detective_organization_admin_account" { + continue + } + + adminAccount, err := tfdetective.FindAdminAccount(ctx, conn, rs.Primary.ID) + + // Because of this resource's dependency, the Organizations organization + // will be deleted first, resulting in the following valid error + if tfawserr.ErrMessageContains(err, detective.ErrCodeValidationException, "account is not a member of an organization") { + continue + } + + if err != nil { + return err + } + + if adminAccount == nil { + continue + } + + return fmt.Errorf("expected Detective Organization Admin Account (%s) to be removed", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckOrganizationAdminAccountExists(ctx context.Context, resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).DetectiveConn() + + adminAccount, err := tfdetective.FindAdminAccount(ctx, conn, rs.Primary.ID) + + if err != nil { + return err + } + + if adminAccount == nil { + return fmt.Errorf("Detective Organization Admin Account (%s) not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccOrganizationAdminAccountConfig_self() string { + return ` +data "aws_caller_identity" "current" {} + +data "aws_partition" "current" {} + +resource "aws_organizations_organization" "test" { + aws_service_access_principals = ["detective.${data.aws_partition.current.dns_suffix}"] + feature_set = "ALL" +} + +resource "aws_detective_graph" "test" {} + +resource "aws_detective_organization_admin_account" "test" { + depends_on = [aws_organizations_organization.test] + + account_id = data.aws_caller_identity.current.account_id +} +` +} + +func testAccOrganizationAdminAccountConfig_multiRegion() string { + return acctest.ConfigCompose( + acctest.ConfigMultipleRegionProvider(3), + ` +data "aws_caller_identity" "current" {} + +data "aws_partition" "current" {} + +resource "aws_organizations_organization" "test" { + aws_service_access_principals = ["detective.${data.aws_partition.current.dns_suffix}"] + feature_set = "ALL" +} + +resource "aws_detective_graph" "test" {} + +resource "aws_detective_organization_admin_account" "test" { + depends_on = [aws_organizations_organization.test] + + account_id = data.aws_caller_identity.current.account_id +} + +resource "aws_detective_organization_admin_account" "alternate" { + provider = awsalternate + + depends_on = [aws_organizations_organization.test] + + account_id = data.aws_caller_identity.current.account_id +} + +resource "aws_detective_organization_admin_account" "third" { + provider = awsthird + + depends_on = [aws_organizations_organization.test] + + account_id = data.aws_caller_identity.current.account_id +} +`) +} diff --git a/internal/service/detective/service_package_gen.go b/internal/service/detective/service_package_gen.go index f92832db39e..7a3a63a1e2a 100644 --- a/internal/service/detective/service_package_gen.go +++ b/internal/service/detective/service_package_gen.go @@ -41,6 +41,10 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka Factory: ResourceMember, TypeName: "aws_detective_member", }, + { + Factory: ResourceOrganizationAdminAccount, + TypeName: "aws_detective_organization_admin_account", + }, } } diff --git a/internal/service/detective/status.go b/internal/service/detective/status.go index 15ec791e75e..0f56cf98ba1 100644 --- a/internal/service/detective/status.go +++ b/internal/service/detective/status.go @@ -8,6 +8,17 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" ) +const ( + // AdminAccountStatus Found + adminAccountStatusFound = "Found" + + // AdminAccountStatus NotFound + adminAccountStatusNotFound = "NotFound" + + // AdminAccountStatus Unknown + adminAccountStatusUnknown = "Unknown" +) + // MemberStatus fetches the Member and its status func MemberStatus(ctx context.Context, conn *detective.Detective, graphARN, adminAccountID string) retry.StateRefreshFunc { return func() (interface{}, string, error) { @@ -24,3 +35,46 @@ func MemberStatus(ctx context.Context, conn *detective.Detective, graphARN, admi return output, aws.StringValue(output.Status), nil } } + +// adminAccountStatus fetches the AdminAccount +func adminAccountStatus(ctx context.Context, conn *detective.Detective, adminAccountID string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + adminAccount, err := getOrganizationAdminAccount(ctx, conn, adminAccountID) + + if err != nil { + return nil, adminAccountStatusUnknown, err + } + + if adminAccount == nil { + return adminAccount, adminAccountStatusNotFound, nil + } + + return adminAccount, adminAccountStatusFound, nil + } +} + +func getOrganizationAdminAccount(ctx context.Context, conn *detective.Detective, adminAccountID string) (*detective.Administrator, error) { + input := &detective.ListOrganizationAdminAccountsInput{} + var result *detective.Administrator + + err := conn.ListOrganizationAdminAccountsPagesWithContext(ctx, input, func(page *detective.ListOrganizationAdminAccountsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, adminAccount := range page.Administrators { + if adminAccount == nil { + continue + } + + if aws.StringValue(adminAccount.AccountId) == adminAccountID { + result = adminAccount + return false + } + } + + return !lastPage + }) + + return result, err +} diff --git a/internal/service/detective/wait.go b/internal/service/detective/wait.go index 391ef23b1ba..699cebf2859 100644 --- a/internal/service/detective/wait.go +++ b/internal/service/detective/wait.go @@ -9,13 +9,18 @@ import ( ) const ( + // Maximum amount of time to wait for an Administrator to return Found + adminAccountFoundTimeout = 5 * time.Minute + // Maximum amount of time to wait for an Administrator to return NotFound + adminAccountNotFoundTimeout = 5 * time.Minute + // GraphOperationTimeout Maximum amount of time to wait for a detective graph to be created, deleted GraphOperationTimeout = 4 * time.Minute // MemberStatusPropagationTimeout Maximum amount of time to wait for a detective member status to return Invited MemberStatusPropagationTimeout = 4 * time.Minute ) -// MemberStatusUpdated waits for an AdminAccount and graph arn to return Invited +// MemberStatusUpdated waits for an MemberDetail and graph arn to return Invited func MemberStatusUpdated(ctx context.Context, conn *detective.Detective, graphARN, adminAccountID, expectedValue string) (*detective.MemberDetail, error) { stateConf := &retry.StateChangeConf{ Pending: []string{detective.MemberStatusVerificationInProgress}, @@ -32,3 +37,39 @@ func MemberStatusUpdated(ctx context.Context, conn *detective.Detective, graphAR return nil, err } + +// waitAdminAccountFound waits for an AdminAccount to return Found +func waitAdminAccountFound(ctx context.Context, conn *detective.Detective, adminAccountID string) (*detective.Administrator, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{adminAccountStatusNotFound}, + Target: []string{adminAccountStatusFound}, + Refresh: adminAccountStatus(ctx, conn, adminAccountID), + Timeout: adminAccountFoundTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*detective.Administrator); ok { + return output, err + } + + return nil, err +} + +// waitAdminAccountNotFound waits for an AdminAccount to return NotFound +func waitAdminAccountNotFound(ctx context.Context, conn *detective.Detective, adminAccountID string) (*detective.Administrator, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{adminAccountStatusFound}, + Target: []string{adminAccountStatusNotFound}, + Refresh: adminAccountStatus(ctx, conn, adminAccountID), + Timeout: adminAccountNotFoundTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*detective.Administrator); ok { + return output, err + } + + return nil, err +} diff --git a/website/docs/r/detective_organization_admin_account.html.markdown b/website/docs/r/detective_organization_admin_account.html.markdown new file mode 100644 index 00000000000..626d39bb965 --- /dev/null +++ b/website/docs/r/detective_organization_admin_account.html.markdown @@ -0,0 +1,46 @@ +--- +subcategory: "Detective" +layout: "aws" +page_title: "AWS: aws_detective_organization_admin_account" +description: |- + Manages a Detective Organization Admin Account +--- + +# Resource: aws_detective_organization_admin_account + +Manages a Detective Organization Admin Account. The AWS account utilizing this resource must be an Organizations primary account. More information about Organizations support in Detective can be found in the [Detective User Guide](https://docs.aws.amazon.com/detective/latest/adminguide/accounts-orgs-transition.html). + +## Example Usage + +```terraform +resource "aws_organizations_organization" "example" { + aws_service_access_principals = ["detective.amazonaws.com"] + feature_set = "ALL" +} + +resource "aws_detective_organization_admin_account" "example" { + depends_on = [aws_organizations_organization.example] + + account_id = "123456789012" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `account_id` - (Required) AWS account identifier to designate as a delegated administrator for Detective. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - AWS account identifier. + +## Import + +Detective Organization Admin Account can be imported using the AWS account ID, e.g., + +``` +$ terraform import aws_detective_organization_admin_account.example 123456789012 +``` From 85f43ace0eb64fe579cf88f1e73ada1728a65663 Mon Sep 17 00:00:00 2001 From: Owen Farrell Date: Tue, 7 Jun 2022 21:22:31 -0400 Subject: [PATCH 02/21] r/aws_detective_organization_configuration - new resource Signed-off-by: Owen Farrell --- internal/service/detective/detective_test.go | 3 + .../detective/organization_configuration.go | 84 +++++++++++++++++++ .../organization_configuration_test.go | 77 +++++++++++++++++ .../service/detective/service_package_gen.go | 4 + ...e_organization_configuration.html.markdown | 47 +++++++++++ 5 files changed, 215 insertions(+) create mode 100644 internal/service/detective/organization_configuration.go create mode 100644 internal/service/detective/organization_configuration_test.go create mode 100644 website/docs/r/detective_organization_configuration.html.markdown diff --git a/internal/service/detective/detective_test.go b/internal/service/detective/detective_test.go index fed3f9a8fae..c62988d528d 100644 --- a/internal/service/detective/detective_test.go +++ b/internal/service/detective/detective_test.go @@ -29,6 +29,9 @@ func TestAccDetective_serial(t *testing.T) { "disappears": testAccOrganizationAdminAccount_disappears, "MultiRegion": testAccOrganizationAdminAccount_MultiRegion, }, + "OrganizationConfiguration": { + "basic": testAccOrganizationConfiguration_basic, + }, } acctest.RunSerialTests2Levels(t, testCases, 0) diff --git a/internal/service/detective/organization_configuration.go b/internal/service/detective/organization_configuration.go new file mode 100644 index 00000000000..20a215ad36e --- /dev/null +++ b/internal/service/detective/organization_configuration.go @@ -0,0 +1,84 @@ +package detective + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/detective" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +// @SDKResource("aws_detective_organization_configuration") +func ResourceOrganizationConfiguration() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceOrganizationConfigurationUpdate, + ReadWithoutTimeout: resourceOrganizationConfigurationRead, + UpdateWithoutTimeout: resourceOrganizationConfigurationUpdate, + DeleteWithoutTimeout: schema.NoopContext, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "auto_enable": { + Type: schema.TypeBool, + Required: true, + }, + + "graph_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: verify.ValidARN, + }, + }, + } +} + +func resourceOrganizationConfigurationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).DetectiveConn() + + graphARN := d.Get("graph_arn").(string) + + input := &detective.UpdateOrganizationConfigurationInput{ + AutoEnable: aws.Bool(d.Get("auto_enable").(bool)), + GraphArn: aws.String(graphARN), + } + + _, err := conn.UpdateOrganizationConfigurationWithContext(ctx, input) + + if err != nil { + return diag.Errorf("error updating Detective Organization Configuration (%s): %s", graphARN, err) + } + + d.SetId(graphARN) + + return resourceOrganizationConfigurationRead(ctx, d, meta) +} + +func resourceOrganizationConfigurationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).DetectiveConn() + + input := &detective.DescribeOrganizationConfigurationInput{ + GraphArn: aws.String(d.Id()), + } + + output, err := conn.DescribeOrganizationConfigurationWithContext(ctx, input) + + if err != nil { + return diag.Errorf("error reading Detective Organization Configuration (%s): %s", d.Id(), err) + } + + if output == nil { + return diag.Errorf("error reading Detective Organization Configuration (%s): empty response", d.Id()) + } + + d.Set("auto_enable", output.AutoEnable) + d.Set("graph_arn", d.Id()) + + return nil +} diff --git a/internal/service/detective/organization_configuration_test.go b/internal/service/detective/organization_configuration_test.go new file mode 100644 index 00000000000..af9df659712 --- /dev/null +++ b/internal/service/detective/organization_configuration_test.go @@ -0,0 +1,77 @@ +package detective_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/detective" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" +) + +func testAccOrganizationConfiguration_basic(t *testing.T) { + ctx := acctest.Context(t) + graphResourceName := "aws_detective_graph.test" + resourceName := "aws_detective_organization_configuration.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckOrganizationsAccount(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, detective.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + // Detective Organization Configuration cannot be deleted separately. + // Ensure parent resource is destroyed instead. + CheckDestroy: testAccCheckGraphDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccOrganizationConfigurationConfig_autoEnable(true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "auto_enable", "true"), + resource.TestCheckResourceAttrPair(resourceName, "graph_arn", graphResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccOrganizationConfigurationConfig_autoEnable(false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "auto_enable", "false"), + resource.TestCheckResourceAttrPair(resourceName, "graph_arn", graphResourceName, "id"), + ), + }, + }, + }) +} + +func testAccOrganizationConfigurationConfig_autoEnable(autoEnable bool) string { + return fmt.Sprintf(` +data "aws_caller_identity" "current" {} + +data "aws_partition" "current" {} + +resource "aws_organizations_organization" "test" { + aws_service_access_principals = ["detective.${data.aws_partition.current.dns_suffix}"] + feature_set = "ALL" +} + +resource "aws_detective_graph" "test" {} + +resource "aws_detective_organization_admin_account" "test" { + depends_on = [aws_organizations_organization.test] + + account_id = data.aws_caller_identity.current.account_id +} + +resource "aws_detective_organization_configuration" "test" { + depends_on = [aws_detective_organization_admin_account.test] + + auto_enable = %[1]t + graph_arn = aws_detective_graph.test.id +} +`, autoEnable) +} diff --git a/internal/service/detective/service_package_gen.go b/internal/service/detective/service_package_gen.go index 7a3a63a1e2a..176d92f4f1e 100644 --- a/internal/service/detective/service_package_gen.go +++ b/internal/service/detective/service_package_gen.go @@ -45,6 +45,10 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka Factory: ResourceOrganizationAdminAccount, TypeName: "aws_detective_organization_admin_account", }, + { + Factory: ResourceOrganizationConfiguration, + TypeName: "aws_detective_organization_configuration", + }, } } diff --git a/website/docs/r/detective_organization_configuration.html.markdown b/website/docs/r/detective_organization_configuration.html.markdown new file mode 100644 index 00000000000..af00c4480b4 --- /dev/null +++ b/website/docs/r/detective_organization_configuration.html.markdown @@ -0,0 +1,47 @@ +--- +subcategory: "Detective" +layout: "aws" +page_title: "AWS: aws_detective_organization_configuration" +description: |- + Manages the Detective Organization Configuration +--- + +# Resource: aws_detective_organization_configuration + +Manages the Detective Organization Configuration in the current AWS Region. The AWS account utilizing this resource must have been assigned as a delegated Organization administrator account, e.g., via the [`aws_detective_organization_admin_account` resource](/docs/providers/aws/r/detective_organization_admin_account.html). More information about Organizations support in Detective can be found in the [Detective User Guide](https://docs.aws.amazon.com/detective/latest/adminguide/accounts-orgs-transition.html). + +~> **NOTE:** This is an advanced Terraform resource. Terraform will automatically assume management of the Detective Organization Configuration without import and perform no actions on removal from the Terraform configuration. + +## Example Usage + +```terraform +resource "aws_detective_graph" "example" { + enable = true +} + +resource "aws_detective_organization_configuration" "example" { + auto_enable = true + graph_arn = aws_detective_graph.example.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `auto_enable` - (Required) When this setting is enabled, all new accounts that are created in, or added to, the organization are added as a member accounts of the organization’s Detective delegated administrator and Detective is enabled in that AWS Region. +* `graph_arn` - (Required) ARN of the behavior graph. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - Identifier of the Detective Graph. + +## Import + +Detective Organization Configurations can be imported using the Detective Graph ID, e.g., + +``` +$ terraform import aws_detective_organization_configuration.example 00b00fd5aecc0ab60a708659477e9617 +``` From 254225f7867acae7b9d253de02eaa099a1a89465 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 27 Oct 2023 16:23:08 -0400 Subject: [PATCH 03/21] Add copywrite headers. --- internal/service/detective/organization_admin_account.go | 3 +++ internal/service/detective/organization_admin_account_test.go | 3 +++ internal/service/detective/organization_configuration.go | 3 +++ internal/service/detective/organization_configuration_test.go | 3 +++ 4 files changed, 12 insertions(+) diff --git a/internal/service/detective/organization_admin_account.go b/internal/service/detective/organization_admin_account.go index e3d2bdd95eb..221317903a9 100644 --- a/internal/service/detective/organization_admin_account.go +++ b/internal/service/detective/organization_admin_account.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package detective import ( diff --git a/internal/service/detective/organization_admin_account_test.go b/internal/service/detective/organization_admin_account_test.go index d36009ab68b..167e710d333 100644 --- a/internal/service/detective/organization_admin_account_test.go +++ b/internal/service/detective/organization_admin_account_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package detective_test import ( diff --git a/internal/service/detective/organization_configuration.go b/internal/service/detective/organization_configuration.go index 237674569da..4c4ad167f3c 100644 --- a/internal/service/detective/organization_configuration.go +++ b/internal/service/detective/organization_configuration.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package detective import ( diff --git a/internal/service/detective/organization_configuration_test.go b/internal/service/detective/organization_configuration_test.go index ab5cfcdbd20..2a4e041ce23 100644 --- a/internal/service/detective/organization_configuration_test.go +++ b/internal/service/detective/organization_configuration_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package detective_test import ( From b841dda601194c1f11b5e6b04d118c285948e996 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 27 Oct 2023 16:27:35 -0400 Subject: [PATCH 04/21] Fix semgrep 'ci.semgrep.errors.no-diag.Errorf-leading-error'. --- .../service/detective/organization_admin_account.go | 12 ++++++------ .../service/detective/organization_configuration.go | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/service/detective/organization_admin_account.go b/internal/service/detective/organization_admin_account.go index 221317903a9..fdc289b17ca 100644 --- a/internal/service/detective/organization_admin_account.go +++ b/internal/service/detective/organization_admin_account.go @@ -50,13 +50,13 @@ func resourceOrganizationAdminAccountCreate(ctx context.Context, d *schema.Resou _, err := conn.EnableOrganizationAdminAccountWithContext(ctx, input) if err != nil { - return diag.Errorf("error enabling Detective Organization Admin Account (%s): %s", accountID, err) + return diag.Errorf("enabling Detective Organization Admin Account (%s): %s", accountID, err) } d.SetId(accountID) if _, err := waitAdminAccountFound(ctx, conn, d.Id()); err != nil { - return diag.Errorf("error waiting for Detective Organization Admin Account (%s) to enable: %s", d.Id(), err) + return diag.Errorf("waiting for Detective Organization Admin Account (%s) to enable: %s", d.Id(), err) } return resourceOrganizationAdminAccountRead(ctx, d, meta) @@ -74,12 +74,12 @@ func resourceOrganizationAdminAccountRead(ctx context.Context, d *schema.Resourc } if err != nil { - return diag.Errorf("error reading Detective Organization Admin Account (%s): %s", d.Id(), err) + return diag.Errorf("reading Detective Organization Admin Account (%s): %s", d.Id(), err) } if adminAccount == nil { if d.IsNewResource() { - return diag.Errorf("error reading Detective Organization Admin Account (%s): %s", d.Id(), err) + return diag.Errorf("reading Detective Organization Admin Account (%s): %s", d.Id(), err) } log.Printf("[WARN] Detective Organization Admin Account (%s) not found, removing from state", d.Id()) @@ -104,11 +104,11 @@ func resourceOrganizationAdminAccountDelete(ctx context.Context, d *schema.Resou } if err != nil { - return diag.Errorf("error disabling Detective Organization Admin Account (%s): %s", d.Id(), err) + return diag.Errorf("disabling Detective Organization Admin Account (%s): %s", d.Id(), err) } if _, err := waitAdminAccountNotFound(ctx, conn, d.Id()); err != nil { - return diag.Errorf("error waiting for Detective Organization Admin Account (%s) to disable: %s", d.Id(), err) + return diag.Errorf("waiting for Detective Organization Admin Account (%s) to disable: %s", d.Id(), err) } return nil diff --git a/internal/service/detective/organization_configuration.go b/internal/service/detective/organization_configuration.go index 4c4ad167f3c..2b97a2f0b0c 100644 --- a/internal/service/detective/organization_configuration.go +++ b/internal/service/detective/organization_configuration.go @@ -55,7 +55,7 @@ func resourceOrganizationConfigurationUpdate(ctx context.Context, d *schema.Reso _, err := conn.UpdateOrganizationConfigurationWithContext(ctx, input) if err != nil { - return diag.Errorf("error updating Detective Organization Configuration (%s): %s", graphARN, err) + return diag.Errorf("updating Detective Organization Configuration (%s): %s", graphARN, err) } d.SetId(graphARN) @@ -73,11 +73,11 @@ func resourceOrganizationConfigurationRead(ctx context.Context, d *schema.Resour output, err := conn.DescribeOrganizationConfigurationWithContext(ctx, input) if err != nil { - return diag.Errorf("error reading Detective Organization Configuration (%s): %s", d.Id(), err) + return diag.Errorf("reading Detective Organization Configuration (%s): %s", d.Id(), err) } if output == nil { - return diag.Errorf("error reading Detective Organization Configuration (%s): empty response", d.Id()) + return diag.Errorf("reading Detective Organization Configuration (%s): empty response", d.Id()) } d.Set("auto_enable", output.AutoEnable) From 27b59038a5510437f2ee1614812eb7c5fcc7ae40 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 27 Oct 2023 16:33:03 -0400 Subject: [PATCH 05/21] Fix markdown-lint errors. --- ...e_organization_admin_account.html.markdown | 19 +++++++++++++++---- ...e_organization_configuration.html.markdown | 17 +++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/website/docs/r/detective_organization_admin_account.html.markdown b/website/docs/r/detective_organization_admin_account.html.markdown index 626d39bb965..d78c6f0be52 100644 --- a/website/docs/r/detective_organization_admin_account.html.markdown +++ b/website/docs/r/detective_organization_admin_account.html.markdown @@ -31,16 +31,27 @@ The following arguments are supported: * `account_id` - (Required) AWS account identifier to designate as a delegated administrator for Detective. -## Attributes Reference +## Attribute Reference -In addition to all arguments above, the following attributes are exported: +This resource exports the following attributes in addition to the arguments above: * `id` - AWS account identifier. ## Import -Detective Organization Admin Account can be imported using the AWS account ID, e.g., +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import `aws_detective_organization_admin_account` using `account_id`. For example: +```terraform +import { + to = aws_detective_organization_admin_account.example + id = 123456789012 +} ``` -$ terraform import aws_detective_organization_admin_account.example 123456789012 + +Using `terraform import`, import `aws_detective_organization_admin_account` using `account_id`. For example: + +```console +% terraform import aws_detective_organization_admin_account.example 123456789012 ``` diff --git a/website/docs/r/detective_organization_configuration.html.markdown b/website/docs/r/detective_organization_configuration.html.markdown index af00c4480b4..171ce453a30 100644 --- a/website/docs/r/detective_organization_configuration.html.markdown +++ b/website/docs/r/detective_organization_configuration.html.markdown @@ -32,16 +32,25 @@ The following arguments are supported: * `auto_enable` - (Required) When this setting is enabled, all new accounts that are created in, or added to, the organization are added as a member accounts of the organization’s Detective delegated administrator and Detective is enabled in that AWS Region. * `graph_arn` - (Required) ARN of the behavior graph. -## Attributes Reference +## Attribute Reference -In addition to all arguments above, the following attributes are exported: +This resource exports the following attributes in addition to the arguments above: * `id` - Identifier of the Detective Graph. ## Import -Detective Organization Configurations can be imported using the Detective Graph ID, e.g., +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import `aws_detective_organization_admin_account` using the Detective Graph ID. For example: +```terraform +import { + to = aws_detective_organization_configuration.example + id = 00b00fd5aecc0ab60a708659477e9617 +} ``` -$ terraform import aws_detective_organization_configuration.example 00b00fd5aecc0ab60a708659477e9617 + +Using `terraform import`, import `aws_detective_organization_admin_account` using the Detective Graph ID. For example: + +```console +% terraform import aws_detective_organization_configuration.example 00b00fd5aecc0ab60a708659477e9617 ``` From 05b3dff37e63631e899c951be63a81920bbc0607 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 27 Oct 2023 17:10:43 -0400 Subject: [PATCH 06/21] Fix terrafmt error in documentation. --- .../docs/r/detective_organization_configuration.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/detective_organization_configuration.html.markdown b/website/docs/r/detective_organization_configuration.html.markdown index 171ce453a30..5deb7f62bf1 100644 --- a/website/docs/r/detective_organization_configuration.html.markdown +++ b/website/docs/r/detective_organization_configuration.html.markdown @@ -45,7 +45,7 @@ In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashico ```terraform import { to = aws_detective_organization_configuration.example - id = 00b00fd5aecc0ab60a708659477e9617 + id = "00b00fd5aecc0ab60a708659477e9617" } ``` From cf7d89064fd9ac269e3ef00b7bfd056b31f58225 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 30 Oct 2023 07:50:52 -0400 Subject: [PATCH 07/21] r/aws_detective_graph: Tidy up Create and Delete. --- internal/service/detective/graph.go | 46 ++++++++++++----------------- internal/service/detective/wait.go | 2 -- 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/internal/service/detective/graph.go b/internal/service/detective/graph.go index bc3505089aa..7ff26c325ef 100644 --- a/internal/service/detective/graph.go +++ b/internal/service/detective/graph.go @@ -5,13 +5,13 @@ package detective import ( "context" + "log" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/detective" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" @@ -28,9 +28,11 @@ func ResourceGraph() *schema.Resource { ReadWithoutTimeout: resourceGraphRead, UpdateWithoutTimeout: resourceGraphUpdate, DeleteWithoutTimeout: resourceGraphDelete, + Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, + Schema: map[string]*schema.Schema{ "created_time": { Type: schema.TypeString, @@ -43,41 +45,30 @@ func ResourceGraph() *schema.Resource { names.AttrTags: tftags.TagsSchema(), names.AttrTagsAll: tftags.TagsSchemaComputed(), }, + CustomizeDiff: verify.SetTagsDiff, } } func resourceGraphCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + const ( + timeout = 4 * time.Minute + ) conn := meta.(*conns.AWSClient).DetectiveConn(ctx) input := &detective.CreateGraphInput{ Tags: getTagsIn(ctx), } - var output *detective.CreateGraphOutput - var err error - err = retry.RetryContext(ctx, GraphOperationTimeout, func() *retry.RetryError { - output, err = conn.CreateGraphWithContext(ctx, input) - if err != nil { - if tfawserr.ErrCodeEquals(err, detective.ErrCodeInternalServerException) { - return retry.RetryableError(err) - } - - return retry.NonRetryableError(err) - } - - return nil - }) - - if tfresource.TimedOut(err) { - output, err = conn.CreateGraphWithContext(ctx, input) - } + outputRaw, err := tfresource.RetryWhenAWSErrCodeEquals(ctx, timeout, func() (interface{}, error) { + return conn.CreateGraphWithContext(ctx, input) + }, detective.ErrCodeInternalServerException) if err != nil { - return diag.Errorf("creating detective Graph: %s", err) + return diag.Errorf("creating Detective Graph: %s", err) } - d.SetId(aws.StringValue(output.GraphArn)) + d.SetId(aws.StringValue(outputRaw.(*detective.CreateGraphOutput).GraphArn)) return resourceGraphRead(ctx, d, meta) } @@ -109,16 +100,17 @@ func resourceGraphUpdate(ctx context.Context, d *schema.ResourceData, meta inter func resourceGraphDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DetectiveConn(ctx) - input := &detective.DeleteGraphInput{ + log.Printf("[DEBUG] Deleting Detective Graph: %s", d.Id()) + _, err := conn.DeleteGraphWithContext(ctx, &detective.DeleteGraphInput{ GraphArn: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { + return nil } - _, err := conn.DeleteGraphWithContext(ctx, input) if err != nil { - if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { - return nil - } - return diag.Errorf("deleting detective Graph (%s): %s", d.Id(), err) + return diag.Errorf("deleting Detective Graph (%s): %s", d.Id(), err) } return nil diff --git a/internal/service/detective/wait.go b/internal/service/detective/wait.go index 77b2c4c4ba7..9e7ae2d59dd 100644 --- a/internal/service/detective/wait.go +++ b/internal/service/detective/wait.go @@ -17,8 +17,6 @@ const ( // Maximum amount of time to wait for an Administrator to return NotFound adminAccountNotFoundTimeout = 5 * time.Minute - // GraphOperationTimeout Maximum amount of time to wait for a detective graph to be created, deleted - GraphOperationTimeout = 4 * time.Minute // MemberStatusPropagationTimeout Maximum amount of time to wait for a detective member status to return Invited MemberStatusPropagationTimeout = 4 * time.Minute ) From bc2c8837fcb5f4d7fd2089e42e26e88de73fdb3f Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 30 Oct 2023 08:02:06 -0400 Subject: [PATCH 08/21] r/aws_detective_graph: Tidy up Read. --- internal/service/detective/find.go | 41 ------------------- internal/service/detective/graph.go | 63 ++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/internal/service/detective/find.go b/internal/service/detective/find.go index 9de879ca25f..75ee85b27f3 100644 --- a/internal/service/detective/find.go +++ b/internal/service/detective/find.go @@ -39,47 +39,6 @@ func FindAdminAccount(ctx context.Context, conn *detective.Detective, adminAccou return result, err } -func FindGraphByARN(ctx context.Context, conn *detective.Detective, arn string) (*detective.Graph, error) { - input := &detective.ListGraphsInput{} - var result *detective.Graph - - err := conn.ListGraphsPagesWithContext(ctx, input, func(page *detective.ListGraphsOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, graph := range page.GraphList { - if graph == nil { - continue - } - - if aws.StringValue(graph.Arn) == arn { - result = graph - return false - } - } - return !lastPage - }) - if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - if err != nil { - return nil, err - } - - if result == nil { - return nil, &retry.NotFoundError{ - Message: fmt.Sprintf("No detective graph with arn %q", arn), - LastRequest: input, - } - } - - return result, nil -} - func FindInvitationByGraphARN(ctx context.Context, conn *detective.Detective, graphARN string) (*string, error) { input := &detective.ListInvitationsInput{} diff --git a/internal/service/detective/graph.go b/internal/service/detective/graph.go index 7ff26c325ef..700eed211a1 100644 --- a/internal/service/detective/graph.go +++ b/internal/service/detective/graph.go @@ -12,8 +12,10 @@ import ( "github.com/aws/aws-sdk-go/service/detective" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" @@ -76,18 +78,20 @@ func resourceGraphCreate(ctx context.Context, d *schema.ResourceData, meta inter func resourceGraphRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DetectiveConn(ctx) - resp, err := FindGraphByARN(ctx, conn, d.Id()) + graph, err := FindGraphByARN(ctx, conn, d.Id()) - if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) || resp == nil { + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Detective Graph (%s) not found, removing from state", d.Id()) d.SetId("") return nil } + if err != nil { - return diag.Errorf("reading detective Graph (%s): %s", d.Id(), err) + return diag.Errorf("reading Detective Graph (%s): %s", d.Id(), err) } - d.Set("created_time", aws.TimeValue(resp.CreatedTime).Format(time.RFC3339)) - d.Set("graph_arn", resp.Arn) + d.Set("created_time", aws.TimeValue(graph.CreatedTime).Format(time.RFC3339)) + d.Set("graph_arn", graph.Arn) return nil } @@ -115,3 +119,52 @@ func resourceGraphDelete(ctx context.Context, d *schema.ResourceData, meta inter return nil } + +func FindGraphByARN(ctx context.Context, conn *detective.Detective, arn string) (*detective.Graph, error) { + input := &detective.ListGraphsInput{} + + return findGraph(ctx, conn, input, func(v *detective.Graph) bool { + return aws.StringValue(v.Arn) == arn + }) +} + +func findGraph(ctx context.Context, conn *detective.Detective, input *detective.ListGraphsInput, filter tfslices.Predicate[*detective.Graph]) (*detective.Graph, error) { + output, err := findGraphs(ctx, conn, input, filter) + + if err != nil { + return nil, err + } + + return tfresource.AssertSinglePtrResult(output) +} + +func findGraphs(ctx context.Context, conn *detective.Detective, input *detective.ListGraphsInput, filter tfslices.Predicate[*detective.Graph]) ([]*detective.Graph, error) { + var output []*detective.Graph + + err := conn.ListGraphsPagesWithContext(ctx, input, func(page *detective.ListGraphsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.GraphList { + if v != nil && filter(v) { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} From 2b97e1de719e81da47118b24f9534a8d43dd79fa Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 30 Oct 2023 08:04:39 -0400 Subject: [PATCH 09/21] r/aws_detective_graph: Tidy up acceptance tests. --- internal/service/detective/graph_test.go | 115 +++++++++-------------- 1 file changed, 44 insertions(+), 71 deletions(-) diff --git a/internal/service/detective/graph_test.go b/internal/service/detective/graph_test.go index 283b2022e89..fcdfa6a91f9 100644 --- a/internal/service/detective/graph_test.go +++ b/internal/service/detective/graph_test.go @@ -8,19 +8,18 @@ import ( "fmt" "testing" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/detective" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" tfdetective "github.com/hashicorp/terraform-provider-aws/internal/service/detective" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func testAccGraph_basic(t *testing.T) { ctx := acctest.Context(t) - var graphOutput detective.Graph + var graph detective.Graph resourceName := "aws_detective_graph.test" resource.Test(t, resource.TestCase{ @@ -32,7 +31,7 @@ func testAccGraph_basic(t *testing.T) { { Config: testAccGraphConfig_basic(), Check: resource.ComposeTestCheckFunc( - testAccCheckGraphExists(ctx, resourceName, &graphOutput), + testAccCheckGraphExists(ctx, resourceName, &graph), acctest.CheckResourceAttrRFC3339(resourceName, "created_time"), ), }, @@ -45,9 +44,32 @@ func testAccGraph_basic(t *testing.T) { }) } +func testAccGraph_disappears(t *testing.T) { + ctx := acctest.Context(t) + var graph detective.Graph + resourceName := "aws_detective_graph.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckGraphDestroy(ctx), + ErrorCheck: acctest.ErrorCheck(t, detective.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccGraphConfig_basic(), + Check: resource.ComposeTestCheckFunc( + testAccCheckGraphExists(ctx, resourceName, &graph), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfdetective.ResourceGraph(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + func testAccGraph_tags(t *testing.T) { ctx := acctest.Context(t) - var graph1, graph2 detective.Graph + var graph detective.Graph resourceName := "aws_detective_graph.test" resource.Test(t, resource.TestCase{ @@ -59,68 +81,34 @@ func testAccGraph_tags(t *testing.T) { { Config: testAccGraphConfig_tags1("key1", "value1"), Check: resource.ComposeTestCheckFunc( - testAccCheckGraphExists(ctx, resourceName, &graph1), + testAccCheckGraphExists(ctx, resourceName, &graph), acctest.CheckResourceAttrRFC3339(resourceName, "created_time"), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), - resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), - resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, { Config: testAccGraphConfig_tags2("key1", "value1updated", "key2", "value2"), Check: resource.ComposeTestCheckFunc( - testAccCheckGraphExists(ctx, resourceName, &graph2), - testAccCheckGraphNotRecreated(&graph1, &graph2), - acctest.CheckResourceAttrRFC3339(resourceName, "created_time"), + testAccCheckGraphExists(ctx, resourceName, &graph), resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), - resource.TestCheckResourceAttr(resourceName, "tags_all.%", "2"), - resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1updated"), - resource.TestCheckResourceAttr(resourceName, "tags_all.key2", "value2"), ), }, { Config: testAccGraphConfig_tags1("key2", "value2"), Check: resource.ComposeTestCheckFunc( - testAccCheckGraphExists(ctx, resourceName, &graph2), - testAccCheckGraphNotRecreated(&graph1, &graph2), - acctest.CheckResourceAttrRFC3339(resourceName, "created_time"), + testAccCheckGraphExists(ctx, resourceName, &graph), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), - resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), - resource.TestCheckResourceAttr(resourceName, "tags_all.key2", "value2"), ), }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - }, - }, - }) -} - -func testAccGraph_disappears(t *testing.T) { - ctx := acctest.Context(t) - var graphOutput detective.Graph - resourceName := "aws_detective_graph.test" - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(ctx, t) }, - ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckGraphDestroy(ctx), - ErrorCheck: acctest.ErrorCheck(t, detective.EndpointsID), - Steps: []resource.TestStep{ - { - Config: testAccGraphConfig_basic(), - Check: resource.ComposeTestCheckFunc( - testAccCheckGraphExists(ctx, resourceName, &graphOutput), - acctest.CheckResourceDisappears(ctx, acctest.Provider, tfdetective.ResourceGraph(), resourceName), - ), - ExpectNonEmptyPlan: true, - }, }, }) } @@ -134,9 +122,9 @@ func testAccCheckGraphDestroy(ctx context.Context) resource.TestCheckFunc { continue } - resp, err := tfdetective.FindGraphByARN(ctx, conn, rs.Primary.ID) + _, err := tfdetective.FindGraphByARN(ctx, conn, rs.Primary.ID) - if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) || resp == nil { + if tfresource.NotFound(err) { continue } @@ -144,44 +132,29 @@ func testAccCheckGraphDestroy(ctx context.Context) resource.TestCheckFunc { return err } - if resp != nil { - return fmt.Errorf("detective graph %q still exists", rs.Primary.ID) - } + return fmt.Errorf("Detective Graph %s still exists", rs.Primary.ID) } return nil } } -func testAccCheckGraphExists(ctx context.Context, resourceName string, graph *detective.Graph) resource.TestCheckFunc { +func testAccCheckGraphExists(ctx context.Context, n string, v *detective.Graph) resource.TestCheckFunc { return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[resourceName] + rs, ok := s.RootModule().Resources[n] if !ok { - return fmt.Errorf("not found: %s", resourceName) + return fmt.Errorf("Not found: %s", n) } conn := acctest.Provider.Meta().(*conns.AWSClient).DetectiveConn(ctx) - resp, err := tfdetective.FindGraphByARN(ctx, conn, rs.Primary.ID) + + output, err := tfdetective.FindGraphByARN(ctx, conn, rs.Primary.ID) if err != nil { return err } - if resp == nil { - return fmt.Errorf("detective graph %q does not exist", rs.Primary.ID) - } - - *graph = *resp - - return nil - } -} - -func testAccCheckGraphNotRecreated(before, after *detective.Graph) resource.TestCheckFunc { - return func(s *terraform.State) error { - if before, after := aws.StringValue(before.Arn), aws.StringValue(after.Arn); before != after { - return fmt.Errorf("detective graph (%s/%s) recreated", before, after) - } + *v = *output return nil } From 2fea9734bf4f98b61c956faeefeff1d6ae76356f Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 30 Oct 2023 08:08:56 -0400 Subject: [PATCH 10/21] Acceptance test output: % make testacc TESTARGS='-run=TestAccDetective_serial/Graph' PKG=detective ==> Checking that code complies with gofmt requirements... TF_ACC=1 go test ./internal/service/detective/... -v -count 1 -parallel 20 -run=TestAccDetective_serial/Graph -timeout 360m === RUN TestAccDetective_serial === PAUSE TestAccDetective_serial === CONT TestAccDetective_serial === RUN TestAccDetective_serial/Graph === RUN TestAccDetective_serial/Graph/basic === RUN TestAccDetective_serial/Graph/disappears === RUN TestAccDetective_serial/Graph/tags --- PASS: TestAccDetective_serial (121.20s) --- PASS: TestAccDetective_serial/Graph (121.20s) --- PASS: TestAccDetective_serial/Graph/basic (31.48s) --- PASS: TestAccDetective_serial/Graph/disappears (20.73s) --- PASS: TestAccDetective_serial/Graph/tags (68.99s) PASS ok github.com/hashicorp/terraform-provider-aws/internal/service/detective 127.274s From 7ea6c3a1d4d20921188de3d1e85ac8fb8ab28d76 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 30 Oct 2023 08:17:43 -0400 Subject: [PATCH 11/21] r/aws_detective_member: Tidy up Create and Delete. --- internal/service/detective/member.go | 92 +++++++++++------------ internal/service/detective/member_test.go | 4 +- 2 files changed, 45 insertions(+), 51 deletions(-) diff --git a/internal/service/detective/member.go b/internal/service/detective/member.go index 593eb4061a3..3a04d65e248 100644 --- a/internal/service/detective/member.go +++ b/internal/service/detective/member.go @@ -14,7 +14,6 @@ import ( "github.com/aws/aws-sdk-go/service/detective" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" @@ -27,9 +26,11 @@ func ResourceMember() *schema.Resource { CreateWithoutTimeout: resourceMemberCreate, ReadWithoutTimeout: resourceMemberRead, DeleteWithoutTimeout: resourceMemberDelete, + Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, + Schema: map[string]*schema.Schema{ "account_id": { Type: schema.TypeString, @@ -89,17 +90,15 @@ func ResourceMember() *schema.Resource { func resourceMemberCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DetectiveConn(ctx) - accountId := d.Get("account_id").(string) - graphArn := d.Get("graph_arn").(string) - - accountInput := &detective.Account{ - AccountId: aws.String(accountId), - EmailAddress: aws.String(d.Get("email_address").(string)), - } - + accountID := d.Get("account_id").(string) + graphARN := d.Get("graph_arn").(string) + id := memberCreateResourceID(graphARN, accountID) input := &detective.CreateMembersInput{ - Accounts: []*detective.Account{accountInput}, - GraphArn: aws.String(graphArn), + Accounts: []*detective.Account{{ + AccountId: aws.String(accountID), + EmailAddress: aws.String(d.Get("email_address").(string)), + }}, + GraphArn: aws.String(graphARN), } if v := d.Get("disable_email_notification").(bool); v { @@ -110,34 +109,19 @@ func resourceMemberCreate(ctx context.Context, d *schema.ResourceData, meta inte input.Message = aws.String(v.(string)) } - var err error - err = retry.RetryContext(ctx, d.Timeout(schema.TimeoutCreate), func() *retry.RetryError { - _, err := conn.CreateMembersWithContext(ctx, input) - - if tfawserr.ErrCodeEquals(err, detective.ErrCodeInternalServerException) { - return retry.RetryableError(err) - } - - if err != nil { - return retry.NonRetryableError(err) - } - - return nil - }) - - if tfresource.TimedOut(err) { - _, err = conn.CreateMembersWithContext(ctx, input) - } + _, err := tfresource.RetryWhenAWSErrCodeEquals(ctx, d.Timeout(schema.TimeoutCreate), func() (interface{}, error) { + return conn.CreateMembersWithContext(ctx, input) + }, detective.ErrCodeInternalServerException) if err != nil { - return diag.Errorf("creating Detective Member: %s", err) + return diag.Errorf("creating Detective Member (%s): %s", id, err) } - if _, err = MemberStatusUpdated(ctx, conn, graphArn, accountId, detective.MemberStatusInvited); err != nil { + if _, err = MemberStatusUpdated(ctx, conn, graphARN, accountID, detective.MemberStatusInvited); err != nil { return diag.Errorf("waiting for Detective Member (%s) to be invited: %s", d.Id(), err) } - d.SetId(EncodeMemberID(graphArn, accountId)) + d.SetId(id) return resourceMemberRead(ctx, d, meta) } @@ -145,7 +129,7 @@ func resourceMemberCreate(ctx context.Context, d *schema.ResourceData, meta inte func resourceMemberRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DetectiveConn(ctx) - graphArn, accountId, err := DecodeMemberID(d.Id()) + graphArn, accountId, err := MemberParseResourceID(d.Id()) if err != nil { return diag.Errorf("decoding ID Detective Member (%s): %s", d.Id(), err) } @@ -180,37 +164,47 @@ func resourceMemberRead(ctx context.Context, d *schema.ResourceData, meta interf return nil } + func resourceMemberDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DetectiveConn(ctx) - graphArn, accountId, err := DecodeMemberID(d.Id()) + graphARN, accountID, err := MemberParseResourceID(d.Id()) if err != nil { - return diag.Errorf("decoding ID Detective Member (%s): %s", d.Id(), err) + return diag.FromErr(err) } - input := &detective.DeleteMembersInput{ - AccountIds: []*string{aws.String(accountId)}, - GraphArn: aws.String(graphArn), + log.Printf("[DEBUG] Deleting Detective Member: %s", d.Id()) + _, err = conn.DeleteMembersWithContext(ctx, &detective.DeleteMembersInput{ + AccountIds: aws.StringSlice([]string{accountID}), + GraphArn: aws.String(graphARN), + }) + + if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { + return nil } - _, err = conn.DeleteMembersWithContext(ctx, input) if err != nil { - if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { - return nil - } return diag.Errorf("deleting Detective Member (%s): %s", d.Id(), err) } + return nil } -func EncodeMemberID(graphArn, accountId string) string { - return fmt.Sprintf("%s/%s", graphArn, accountId) +const memberResourceIDSeparator = "/" + +func memberCreateResourceID(graphARN, accountID string) string { + parts := []string{graphARN, accountID} + id := strings.Join(parts, memberResourceIDSeparator) + + return id } -func DecodeMemberID(id string) (string, string, error) { - idParts := strings.Split(id, "/") - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - return "", "", fmt.Errorf("expected ID in the form of graph_arn/account_id, given: %q", id) +func MemberParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, memberResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil } - return idParts[0], idParts[1], nil + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected graph_arn%[2]saccount_id", id, memberResourceIDSeparator) } diff --git a/internal/service/detective/member_test.go b/internal/service/detective/member_test.go index 99baaf6a57a..9f7952f9eea 100644 --- a/internal/service/detective/member_test.go +++ b/internal/service/detective/member_test.go @@ -140,7 +140,7 @@ func testAccCheckMemberExists(ctx context.Context, resourceName string, detectiv conn := acctest.Provider.Meta().(*conns.AWSClient).DetectiveConn(ctx) - graphArn, accountId, err := tfdetective.DecodeMemberID(rs.Primary.ID) + graphArn, accountId, err := tfdetective.MemberParseResourceID(rs.Primary.ID) if err != nil { return err } @@ -169,7 +169,7 @@ func testAccCheckMemberDestroy(ctx context.Context) resource.TestCheckFunc { continue } - graphArn, accountId, err := tfdetective.DecodeMemberID(rs.Primary.ID) + graphArn, accountId, err := tfdetective.MemberParseResourceID(rs.Primary.ID) if err != nil { return err } From c766a7eb5da06f78515396d7b07a84db56f1c98b Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 30 Oct 2023 08:31:10 -0400 Subject: [PATCH 12/21] r/aws_detective_member: Tidy up Read. --- internal/service/detective/find.go | 45 -------- internal/service/detective/member.go | 133 ++++++++++++++++++---- internal/service/detective/member_test.go | 4 +- internal/service/detective/status.go | 17 --- internal/service/detective/wait.go | 21 ---- 5 files changed, 110 insertions(+), 110 deletions(-) diff --git a/internal/service/detective/find.go b/internal/service/detective/find.go index 75ee85b27f3..91dfa45e2f5 100644 --- a/internal/service/detective/find.go +++ b/internal/service/detective/find.go @@ -72,48 +72,3 @@ func FindInvitationByGraphARN(ctx context.Context, conn *detective.Detective, gr return result, nil } - -func FindMemberByGraphARNAndAccountID(ctx context.Context, conn *detective.Detective, graphARN string, accountID string) (*detective.MemberDetail, error) { - input := &detective.ListMembersInput{ - GraphArn: aws.String(graphARN), - } - - var result *detective.MemberDetail - - err := conn.ListMembersPagesWithContext(ctx, input, func(page *detective.ListMembersOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, member := range page.MemberDetails { - if member == nil { - continue - } - - if aws.StringValue(member.AccountId) == accountID { - result = member - return false - } - } - - return !lastPage - }) - if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - if err != nil { - return nil, err - } - - if result == nil { - return nil, &retry.NotFoundError{ - Message: fmt.Sprintf("No member found with arn %q and accountID %q", graphARN, accountID), - LastRequest: input, - } - } - - return result, nil -} diff --git a/internal/service/detective/member.go b/internal/service/detective/member.go index 3a04d65e248..a363fc7dfb9 100644 --- a/internal/service/detective/member.go +++ b/internal/service/detective/member.go @@ -14,8 +14,10 @@ import ( "github.com/aws/aws-sdk-go/service/detective" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" ) @@ -117,8 +119,8 @@ func resourceMemberCreate(ctx context.Context, d *schema.ResourceData, meta inte return diag.Errorf("creating Detective Member (%s): %s", id, err) } - if _, err = MemberStatusUpdated(ctx, conn, graphARN, accountID, detective.MemberStatusInvited); err != nil { - return diag.Errorf("waiting for Detective Member (%s) to be invited: %s", d.Id(), err) + if _, err := waitMemberInvited(ctx, conn, graphARN, accountID); err != nil { + return diag.Errorf("waiting for Detective Member (%s) invited: %s", d.Id(), err) } d.SetId(id) @@ -129,38 +131,32 @@ func resourceMemberCreate(ctx context.Context, d *schema.ResourceData, meta inte func resourceMemberRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DetectiveConn(ctx) - graphArn, accountId, err := MemberParseResourceID(d.Id()) + graphARN, accountID, err := MemberParseResourceID(d.Id()) if err != nil { - return diag.Errorf("decoding ID Detective Member (%s): %s", d.Id(), err) + return diag.FromErr(err) } - resp, err := FindMemberByGraphARNAndAccountID(ctx, conn, graphArn, accountId) - - if err != nil { - if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) || - tfresource.NotFound(err) { - log.Printf("[WARN] Detective Member (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil - } - return diag.Errorf("reading Detective Member (%s): %s", d.Id(), err) - } + member, err := FindMemberByGraphByTwoPartKey(ctx, conn, graphARN, accountID) - if !d.IsNewResource() && resp == nil { + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] Detective Member (%s) not found, removing from state", d.Id()) d.SetId("") return nil } - d.Set("account_id", resp.AccountId) - d.Set("administrator_id", resp.AdministratorId) - d.Set("disabled_reason", resp.DisabledReason) - d.Set("email_address", resp.EmailAddress) - d.Set("graph_arn", resp.GraphArn) - d.Set("invited_time", aws.TimeValue(resp.InvitedTime).Format(time.RFC3339)) - d.Set("status", resp.Status) - d.Set("updated_time", aws.TimeValue(resp.UpdatedTime).Format(time.RFC3339)) - d.Set("volume_usage_in_bytes", resp.VolumeUsageInBytes) + if err != nil { + return diag.Errorf("reading Detective Member (%s): %s", d.Id(), err) + } + + d.Set("account_id", member.AccountId) + d.Set("administrator_id", member.AdministratorId) + d.Set("disabled_reason", member.DisabledReason) + d.Set("email_address", member.EmailAddress) + d.Set("graph_arn", member.GraphArn) + d.Set("invited_time", aws.TimeValue(member.InvitedTime).Format(time.RFC3339)) + d.Set("status", member.Status) + d.Set("updated_time", aws.TimeValue(member.UpdatedTime).Format(time.RFC3339)) + d.Set("volume_usage_in_bytes", member.VolumeUsageInBytes) return nil } @@ -208,3 +204,90 @@ func MemberParseResourceID(id string) (string, string, error) { return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected graph_arn%[2]saccount_id", id, memberResourceIDSeparator) } + +func FindMemberByGraphByTwoPartKey(ctx context.Context, conn *detective.Detective, graphARN, accountID string) (*detective.MemberDetail, error) { + input := &detective.ListMembersInput{ + GraphArn: aws.String(graphARN), + } + + return findMember(ctx, conn, input, func(v *detective.MemberDetail) bool { + return aws.StringValue(v.AccountId) == accountID + }) +} + +func findMember(ctx context.Context, conn *detective.Detective, input *detective.ListMembersInput, filter tfslices.Predicate[*detective.MemberDetail]) (*detective.MemberDetail, error) { + output, err := findMembers(ctx, conn, input, filter) + + if err != nil { + return nil, err + } + + return tfresource.AssertSinglePtrResult(output) +} + +func findMembers(ctx context.Context, conn *detective.Detective, input *detective.ListMembersInput, filter tfslices.Predicate[*detective.MemberDetail]) ([]*detective.MemberDetail, error) { + var output []*detective.MemberDetail + + err := conn.ListMembersPagesWithContext(ctx, input, func(page *detective.ListMembersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.MemberDetails { + if v != nil && filter(v) { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} + +func statusMember(ctx context.Context, conn *detective.Detective, graphARN, adminAccountID string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := FindMemberByGraphByTwoPartKey(ctx, conn, graphARN, adminAccountID) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} + +func waitMemberInvited(ctx context.Context, conn *detective.Detective, graphARN, adminAccountID string) (*detective.MemberDetail, error) { + const ( + timeout = 4 * time.Minute + ) + stateConf := &retry.StateChangeConf{ + Pending: []string{detective.MemberStatusVerificationInProgress}, + Target: []string{detective.MemberStatusInvited}, + Refresh: statusMember(ctx, conn, graphARN, adminAccountID), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*detective.MemberDetail); ok { + return output, err + } + + return nil, err +} diff --git a/internal/service/detective/member_test.go b/internal/service/detective/member_test.go index 9f7952f9eea..364839b50ca 100644 --- a/internal/service/detective/member_test.go +++ b/internal/service/detective/member_test.go @@ -145,7 +145,7 @@ func testAccCheckMemberExists(ctx context.Context, resourceName string, detectiv return err } - resp, err := tfdetective.FindMemberByGraphARNAndAccountID(ctx, conn, graphArn, accountId) + resp, err := tfdetective.FindMemberByGraphByTwoPartKey(ctx, conn, graphArn, accountId) if err != nil { return err } @@ -174,7 +174,7 @@ func testAccCheckMemberDestroy(ctx context.Context) resource.TestCheckFunc { return err } - resp, err := tfdetective.FindMemberByGraphARNAndAccountID(ctx, conn, graphArn, accountId) + resp, err := tfdetective.FindMemberByGraphByTwoPartKey(ctx, conn, graphArn, accountId) if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { continue } diff --git a/internal/service/detective/status.go b/internal/service/detective/status.go index 07638fa079a..b48f406e1d5 100644 --- a/internal/service/detective/status.go +++ b/internal/service/detective/status.go @@ -22,23 +22,6 @@ const ( adminAccountStatusUnknown = "Unknown" ) -// MemberStatus fetches the Member and its status -func MemberStatus(ctx context.Context, conn *detective.Detective, graphARN, adminAccountID string) retry.StateRefreshFunc { - return func() (interface{}, string, error) { - output, err := FindMemberByGraphARNAndAccountID(ctx, conn, graphARN, adminAccountID) - - if err != nil { - return nil, "Unknown", err - } - - if output == nil { - return output, "NotFound", nil - } - - return output, aws.StringValue(output.Status), nil - } -} - // adminAccountStatus fetches the AdminAccount func adminAccountStatus(ctx context.Context, conn *detective.Detective, adminAccountID string) retry.StateRefreshFunc { return func() (interface{}, string, error) { diff --git a/internal/service/detective/wait.go b/internal/service/detective/wait.go index 9e7ae2d59dd..81cedcaf432 100644 --- a/internal/service/detective/wait.go +++ b/internal/service/detective/wait.go @@ -16,29 +16,8 @@ const ( adminAccountFoundTimeout = 5 * time.Minute // Maximum amount of time to wait for an Administrator to return NotFound adminAccountNotFoundTimeout = 5 * time.Minute - - // MemberStatusPropagationTimeout Maximum amount of time to wait for a detective member status to return Invited - MemberStatusPropagationTimeout = 4 * time.Minute ) -// MemberStatusUpdated waits for an MemberDetail and graph arn to return Invited -func MemberStatusUpdated(ctx context.Context, conn *detective.Detective, graphARN, adminAccountID, expectedValue string) (*detective.MemberDetail, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{detective.MemberStatusVerificationInProgress}, - Target: []string{detective.MemberStatusInvited}, - Refresh: MemberStatus(ctx, conn, graphARN, adminAccountID), - Timeout: MemberStatusPropagationTimeout, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - - if output, ok := outputRaw.(*detective.MemberDetail); ok { - return output, err - } - - return nil, err -} - // waitAdminAccountFound waits for an AdminAccount to return Found func waitAdminAccountFound(ctx context.Context, conn *detective.Detective, adminAccountID string) (*detective.Administrator, error) { stateConf := &retry.StateChangeConf{ From f25f150b6c91caaa19950bb358606e45f056b6af Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 30 Oct 2023 08:36:10 -0400 Subject: [PATCH 13/21] Tidy up acceptance tests. --- internal/service/detective/member_test.go | 54 ++++++++--------------- 1 file changed, 19 insertions(+), 35 deletions(-) diff --git a/internal/service/detective/member_test.go b/internal/service/detective/member_test.go index 364839b50ca..f9f4f788a4c 100644 --- a/internal/service/detective/member_test.go +++ b/internal/service/detective/member_test.go @@ -9,12 +9,12 @@ import ( "testing" "github.com/aws/aws-sdk-go/service/detective" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" tfdetective "github.com/hashicorp/terraform-provider-aws/internal/service/detective" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func testAccMember_basic(t *testing.T) { @@ -99,7 +99,7 @@ func testAccMember_message(t *testing.T) { ErrorCheck: acctest.ErrorCheck(t, detective.EndpointsID), Steps: []resource.TestStep{ { - Config: testAccMemberConfig_invitationMessage(email, false), + Config: testAccMemberConfig_invitationMessage(email), Check: resource.ComposeTestCheckFunc( testAccCheckMemberExists(ctx, resourceName, &detectiveOutput), acctest.CheckResourceAttrAccountID(resourceName, "administrator_id"), @@ -110,18 +110,6 @@ func testAccMember_message(t *testing.T) { ), }, { - Config: testAccMemberConfig_invitationMessage(email, true), - Check: resource.ComposeTestCheckFunc( - testAccCheckMemberExists(ctx, resourceName, &detectiveOutput), - acctest.CheckResourceAttrAccountID(resourceName, "administrator_id"), - resource.TestCheckResourceAttrPair(resourceName, "account_id", dataSourceAlternate, "account_id"), - acctest.CheckResourceAttrRFC3339(resourceName, "invited_time"), - acctest.CheckResourceAttrRFC3339(resourceName, "updated_time"), - resource.TestCheckResourceAttr(resourceName, "status", detective.MemberStatusInvited), - ), - }, - { - Config: testAccMemberConfig_invitationMessage(email, true), ResourceName: resourceName, ImportState: true, ImportStateVerify: true, @@ -131,30 +119,27 @@ func testAccMember_message(t *testing.T) { }) } -func testAccCheckMemberExists(ctx context.Context, resourceName string, detectiveSession *detective.MemberDetail) resource.TestCheckFunc { +func testAccCheckMemberExists(ctx context.Context, n string, v *detective.MemberDetail) resource.TestCheckFunc { return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[resourceName] + rs, ok := s.RootModule().Resources[n] if !ok { - return fmt.Errorf("not found: %s", resourceName) + return fmt.Errorf("Not found: %s", n) } conn := acctest.Provider.Meta().(*conns.AWSClient).DetectiveConn(ctx) - graphArn, accountId, err := tfdetective.MemberParseResourceID(rs.Primary.ID) + graphARN, accountID, err := tfdetective.MemberParseResourceID(rs.Primary.ID) if err != nil { return err } - resp, err := tfdetective.FindMemberByGraphByTwoPartKey(ctx, conn, graphArn, accountId) + output, err := tfdetective.FindMemberByGraphByTwoPartKey(ctx, conn, graphARN, accountID) + if err != nil { return err } - if resp == nil { - return fmt.Errorf("detective Member %q does not exist", rs.Primary.ID) - } - - *detectiveSession = *resp + *v = *output return nil } @@ -169,13 +154,14 @@ func testAccCheckMemberDestroy(ctx context.Context) resource.TestCheckFunc { continue } - graphArn, accountId, err := tfdetective.MemberParseResourceID(rs.Primary.ID) + graphARN, accountID, err := tfdetective.MemberParseResourceID(rs.Primary.ID) if err != nil { return err } - resp, err := tfdetective.FindMemberByGraphByTwoPartKey(ctx, conn, graphArn, accountId) - if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { + _, err = tfdetective.FindMemberByGraphByTwoPartKey(ctx, conn, graphARN, accountID) + + if tfresource.NotFound(err) { continue } @@ -183,9 +169,7 @@ func testAccCheckMemberDestroy(ctx context.Context) resource.TestCheckFunc { return err } - if resp != nil { - return fmt.Errorf("detective Member %q still exists", rs.Primary.ID) - } + return fmt.Errorf("Detective Member %s still exists", rs.Primary.ID) } return nil @@ -193,7 +177,7 @@ func testAccCheckMemberDestroy(ctx context.Context) resource.TestCheckFunc { } func testAccMemberConfig_basic(email string) string { - return acctest.ConfigAlternateAccountProvider() + fmt.Sprintf(` + return acctest.ConfigCompose(acctest.ConfigAlternateAccountProvider(), fmt.Sprintf(` data "aws_caller_identity" "member" { provider = "awsalternate" } @@ -205,11 +189,11 @@ resource "aws_detective_member" "test" { graph_arn = aws_detective_graph.test.id email_address = %[1]q } -`, email) +`, email)) } -func testAccMemberConfig_invitationMessage(email string, invite bool) string { - return acctest.ConfigAlternateAccountProvider() + fmt.Sprintf(` +func testAccMemberConfig_invitationMessage(email string) string { + return acctest.ConfigCompose(acctest.ConfigAlternateAccountProvider(), fmt.Sprintf(` data "aws_caller_identity" "member" { provider = "awsalternate" } @@ -222,5 +206,5 @@ resource "aws_detective_member" "test" { email_address = %[1]q message = "This is a message of the invitation" } -`, email, invite) +`, email)) } From a5ecbeb49f2ac954adab6fd9c52ed54a5a99cf96 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 30 Oct 2023 08:48:08 -0400 Subject: [PATCH 14/21] r/aws_detective_invitation_accepter: Tidy up Create and Delete. --- .../service/detective/invitation_accepter.go | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/internal/service/detective/invitation_accepter.go b/internal/service/detective/invitation_accepter.go index befccf8ecc5..9bed7a085ad 100644 --- a/internal/service/detective/invitation_accepter.go +++ b/internal/service/detective/invitation_accepter.go @@ -22,9 +22,11 @@ func ResourceInvitationAccepter() *schema.Resource { CreateWithoutTimeout: resourceInvitationAccepterCreate, ReadWithoutTimeout: resourceInvitationAccepterRead, DeleteWithoutTimeout: resourceInvitationAccepterDelete, + Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, + Schema: map[string]*schema.Schema{ "graph_arn": { Type: schema.TypeString, @@ -39,19 +41,18 @@ func ResourceInvitationAccepter() *schema.Resource { func resourceInvitationAccepterCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DetectiveConn(ctx) - graphArn := d.Get("graph_arn").(string) - - acceptInvitationInput := &detective.AcceptInvitationInput{ - GraphArn: aws.String(graphArn), + graphARN := d.Get("graph_arn").(string) + input := &detective.AcceptInvitationInput{ + GraphArn: aws.String(graphARN), } - _, err := conn.AcceptInvitationWithContext(ctx, acceptInvitationInput) + _, err := conn.AcceptInvitationWithContext(ctx, input) if err != nil { - return diag.Errorf("accepting Detective InvitationAccepter (%s): %s", d.Id(), err) + return diag.Errorf("accepting Detective Invitation (%s): %s", graphARN, err) } - d.SetId(graphArn) + d.SetId(graphARN) return resourceInvitationAccepterRead(ctx, d, meta) } @@ -78,16 +79,18 @@ func resourceInvitationAccepterRead(ctx context.Context, d *schema.ResourceData, func resourceInvitationAccepterDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DetectiveConn(ctx) - input := &detective.DisassociateMembershipInput{ + log.Printf("[DEBUG] Deleting Detective Invitation Accepter: %s", d.Id()) + _, err := conn.DisassociateMembershipWithContext(ctx, &detective.DisassociateMembershipInput{ GraphArn: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { + return nil } - _, err := conn.DisassociateMembershipWithContext(ctx, input) if err != nil { - if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { - return nil - } return diag.Errorf("disassociating Detective InvitationAccepter (%s): %s", d.Id(), err) } + return nil } From ba85b4a8bf78b0caf9337593dbf7ab2c34ea231e Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 30 Oct 2023 10:16:51 -0400 Subject: [PATCH 15/21] r/aws_detective_invitation_accepter: Tidy up Read. --- internal/service/detective/find.go | 37 ----------- .../service/detective/invitation_accepter.go | 63 +++++++++++++++++-- 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/internal/service/detective/find.go b/internal/service/detective/find.go index 91dfa45e2f5..e10b689abbc 100644 --- a/internal/service/detective/find.go +++ b/internal/service/detective/find.go @@ -5,12 +5,9 @@ package detective import ( "context" - "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/detective" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" ) func FindAdminAccount(ctx context.Context, conn *detective.Detective, adminAccountID string) (*detective.Administrator, error) { @@ -38,37 +35,3 @@ func FindAdminAccount(ctx context.Context, conn *detective.Detective, adminAccou return result, err } - -func FindInvitationByGraphARN(ctx context.Context, conn *detective.Detective, graphARN string) (*string, error) { - input := &detective.ListInvitationsInput{} - - var result *string - - err := conn.ListInvitationsPagesWithContext(ctx, input, func(page *detective.ListInvitationsOutput, lastPage bool) bool { - for _, invitation := range page.Invitations { - if aws.StringValue(invitation.GraphArn) == graphARN { - result = invitation.GraphArn - return false - } - } - return !lastPage - }) - if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - if err != nil { - return nil, err - } - - if result == nil { - return nil, &retry.NotFoundError{ - Message: fmt.Sprintf("No member found with arn %q ", graphARN), - LastRequest: input, - } - } - - return result, nil -} diff --git a/internal/service/detective/invitation_accepter.go b/internal/service/detective/invitation_accepter.go index 9bed7a085ad..08743ddf041 100644 --- a/internal/service/detective/invitation_accepter.go +++ b/internal/service/detective/invitation_accepter.go @@ -11,8 +11,11 @@ import ( "github.com/aws/aws-sdk-go/service/detective" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" ) @@ -60,19 +63,20 @@ func resourceInvitationAccepterCreate(ctx context.Context, d *schema.ResourceDat func resourceInvitationAccepterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DetectiveConn(ctx) - graphArn, err := FindInvitationByGraphARN(ctx, conn, d.Id()) + member, err := FindInvitationByGraphARN(ctx, conn, d.Id()) - if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { - log.Printf("[WARN] Detective InvitationAccepter (%s) not found, removing from state", d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Detective Invitation Accepter (%s) not found, removing from state", d.Id()) d.SetId("") return nil } if err != nil { - return diag.Errorf("listing Detective InvitationAccepter (%s): %s", d.Id(), err) + return diag.Errorf("reading Detective Invitation Accepter (%s): %s", d.Id(), err) } - d.Set("graph_arn", graphArn) + d.Set("graph_arn", member.GraphArn) + return nil } @@ -94,3 +98,52 @@ func resourceInvitationAccepterDelete(ctx context.Context, d *schema.ResourceDat return nil } + +func FindInvitationByGraphARN(ctx context.Context, conn *detective.Detective, graphARN string) (*detective.MemberDetail, error) { + input := &detective.ListInvitationsInput{} + + return findInvitation(ctx, conn, input, func(v *detective.MemberDetail) bool { + return aws.StringValue(v.GraphArn) == graphARN + }) +} + +func findInvitation(ctx context.Context, conn *detective.Detective, input *detective.ListInvitationsInput, filter tfslices.Predicate[*detective.MemberDetail]) (*detective.MemberDetail, error) { + output, err := findInvitations(ctx, conn, input, filter) + + if err != nil { + return nil, err + } + + return tfresource.AssertSinglePtrResult(output) +} + +func findInvitations(ctx context.Context, conn *detective.Detective, input *detective.ListInvitationsInput, filter tfslices.Predicate[*detective.MemberDetail]) ([]*detective.MemberDetail, error) { + var output []*detective.MemberDetail + + err := conn.ListInvitationsPagesWithContext(ctx, input, func(page *detective.ListInvitationsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.Invitations { + if v != nil && filter(v) { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} From 107d0e4026ce98a2be6c12f48bd895151fc6d493 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 30 Oct 2023 10:22:01 -0400 Subject: [PATCH 16/21] r/aws_detective_invitation_accepter: Tidy up acceptance tests. --- .../detective/invitation_accepter_test.go | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/internal/service/detective/invitation_accepter_test.go b/internal/service/detective/invitation_accepter_test.go index dddd940516c..1c23aeccf8b 100644 --- a/internal/service/detective/invitation_accepter_test.go +++ b/internal/service/detective/invitation_accepter_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/aws/aws-sdk-go/service/detective" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" @@ -39,7 +38,6 @@ func testAccInvitationAccepter_basic(t *testing.T) { ), }, { - Config: testAccInvitationAccepterConfig_basic(email), ResourceName: resourceName, ImportState: true, ImportStateVerify: true, @@ -48,30 +46,18 @@ func testAccInvitationAccepter_basic(t *testing.T) { }) } -func testAccCheckInvitationAccepterExists(ctx context.Context, resourceName string) resource.TestCheckFunc { +func testAccCheckInvitationAccepterExists(ctx context.Context, n string) resource.TestCheckFunc { return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[resourceName] + rs, ok := s.RootModule().Resources[n] if !ok { - return fmt.Errorf("not found: %s", resourceName) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("resource (%s) has empty ID", resourceName) + return fmt.Errorf("Not found: %s", n) } conn := acctest.Provider.Meta().(*conns.AWSClient).DetectiveConn(ctx) - result, err := tfdetective.FindInvitationByGraphARN(ctx, conn, rs.Primary.ID) - - if err != nil { - return err - } - - if result == nil { - return fmt.Errorf("no detective invitation found for (%s): %s", resourceName, rs.Primary.ID) - } + _, err := tfdetective.FindInvitationByGraphARN(ctx, conn, rs.Primary.ID) - return nil + return err } } @@ -84,16 +70,17 @@ func testAccCheckInvitationAccepterDestroy(ctx context.Context) resource.TestChe continue } - result, err := tfdetective.FindInvitationByGraphARN(ctx, conn, rs.Primary.ID) + _, err := tfdetective.FindInvitationByGraphARN(ctx, conn, rs.Primary.ID) - if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) || - tfresource.NotFound(err) { + if tfresource.NotFound(err) { continue } - if result != nil { - return fmt.Errorf("detective InvitationAccepter %q still exists", rs.Primary.ID) + if err != nil { + return err } + + return fmt.Errorf("Detective Invitation Accepter %s still exists", rs.Primary.ID) } return nil From 0a5d4fce5e85e94de2f5cf25e0f92f1c3c87291d Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 30 Oct 2023 10:46:03 -0400 Subject: [PATCH 17/21] r/aws_detective_organization_admin_account: Tidy up Create, Read and Delete. --- internal/service/detective/find.go | 37 -------- internal/service/detective/graph.go | 8 -- .../service/detective/invitation_accepter.go | 8 -- .../detective/organization_admin_account.go | 90 ++++++++++++++----- .../organization_admin_account_test.go | 4 +- internal/service/detective/status.go | 66 -------------- internal/service/detective/wait.go | 55 ------------ 7 files changed, 71 insertions(+), 197 deletions(-) delete mode 100644 internal/service/detective/find.go delete mode 100644 internal/service/detective/status.go delete mode 100644 internal/service/detective/wait.go diff --git a/internal/service/detective/find.go b/internal/service/detective/find.go deleted file mode 100644 index e10b689abbc..00000000000 --- a/internal/service/detective/find.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package detective - -import ( - "context" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/detective" -) - -func FindAdminAccount(ctx context.Context, conn *detective.Detective, adminAccountID string) (*detective.Administrator, error) { - input := &detective.ListOrganizationAdminAccountsInput{} - var result *detective.Administrator - - err := conn.ListOrganizationAdminAccountsPagesWithContext(ctx, input, func(page *detective.ListOrganizationAdminAccountsOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, adminAccount := range page.Administrators { - if adminAccount == nil { - continue - } - - if aws.StringValue(adminAccount.AccountId) == adminAccountID { - result = adminAccount - return false - } - } - - return !lastPage - }) - - return result, err -} diff --git a/internal/service/detective/graph.go b/internal/service/detective/graph.go index 700eed211a1..d7c2667f52b 100644 --- a/internal/service/detective/graph.go +++ b/internal/service/detective/graph.go @@ -12,7 +12,6 @@ import ( "github.com/aws/aws-sdk-go/service/detective" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" @@ -155,13 +154,6 @@ func findGraphs(ctx context.Context, conn *detective.Detective, input *detective return !lastPage }) - if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - if err != nil { return nil, err } diff --git a/internal/service/detective/invitation_accepter.go b/internal/service/detective/invitation_accepter.go index 08743ddf041..b84cacffd12 100644 --- a/internal/service/detective/invitation_accepter.go +++ b/internal/service/detective/invitation_accepter.go @@ -11,7 +11,6 @@ import ( "github.com/aws/aws-sdk-go/service/detective" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" @@ -134,13 +133,6 @@ func findInvitations(ctx context.Context, conn *detective.Detective, input *dete return !lastPage }) - if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - if err != nil { return nil, err } diff --git a/internal/service/detective/organization_admin_account.go b/internal/service/detective/organization_admin_account.go index fdc289b17ca..78115928b9f 100644 --- a/internal/service/detective/organization_admin_account.go +++ b/internal/service/detective/organization_admin_account.go @@ -6,13 +6,17 @@ package detective import ( "context" "log" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/detective" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" ) @@ -42,7 +46,6 @@ func resourceOrganizationAdminAccountCreate(ctx context.Context, d *schema.Resou conn := meta.(*conns.AWSClient).DetectiveConn(ctx) accountID := d.Get("account_id").(string) - input := &detective.EnableOrganizationAdminAccountInput{ AccountId: aws.String(accountID), } @@ -55,8 +58,12 @@ func resourceOrganizationAdminAccountCreate(ctx context.Context, d *schema.Resou d.SetId(accountID) - if _, err := waitAdminAccountFound(ctx, conn, d.Id()); err != nil { - return diag.Errorf("waiting for Detective Organization Admin Account (%s) to enable: %s", d.Id(), err) + _, err = tfresource.RetryWhenNotFound(ctx, 5*time.Minute, func() (interface{}, error) { + return FindOrganizationAdminAccountByAccountID(ctx, conn, d.Id()) + }) + + if err != nil { + return diag.Errorf("waiting for Detective Organization Admin Account (%s) create: %s", d.Id(), err) } return resourceOrganizationAdminAccountRead(ctx, d, meta) @@ -65,9 +72,9 @@ func resourceOrganizationAdminAccountCreate(ctx context.Context, d *schema.Resou func resourceOrganizationAdminAccountRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DetectiveConn(ctx) - adminAccount, err := FindAdminAccount(ctx, conn, d.Id()) + administrator, err := FindOrganizationAdminAccountByAccountID(ctx, conn, d.Id()) - if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] Detective Organization Admin Account (%s) not found, removing from state", d.Id()) d.SetId("") return nil @@ -77,17 +84,7 @@ func resourceOrganizationAdminAccountRead(ctx context.Context, d *schema.Resourc return diag.Errorf("reading Detective Organization Admin Account (%s): %s", d.Id(), err) } - if adminAccount == nil { - if d.IsNewResource() { - return diag.Errorf("reading Detective Organization Admin Account (%s): %s", d.Id(), err) - } - - log.Printf("[WARN] Detective Organization Admin Account (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil - } - - d.Set("account_id", adminAccount.AccountId) + d.Set("account_id", administrator.AccountId) return nil } @@ -95,9 +92,7 @@ func resourceOrganizationAdminAccountRead(ctx context.Context, d *schema.Resourc func resourceOrganizationAdminAccountDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DetectiveConn(ctx) - input := &detective.DisableOrganizationAdminAccountInput{} - - _, err := conn.DisableOrganizationAdminAccountWithContext(ctx, input) + _, err := conn.DisableOrganizationAdminAccountWithContext(ctx, &detective.DisableOrganizationAdminAccountInput{}) if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { return nil @@ -107,9 +102,62 @@ func resourceOrganizationAdminAccountDelete(ctx context.Context, d *schema.Resou return diag.Errorf("disabling Detective Organization Admin Account (%s): %s", d.Id(), err) } - if _, err := waitAdminAccountNotFound(ctx, conn, d.Id()); err != nil { - return diag.Errorf("waiting for Detective Organization Admin Account (%s) to disable: %s", d.Id(), err) + _, err = tfresource.RetryUntilNotFound(ctx, 5*time.Minute, func() (interface{}, error) { + return FindOrganizationAdminAccountByAccountID(ctx, conn, d.Id()) + }) + + if err != nil { + return diag.Errorf("waiting for Detective Organization Admin Account (%s) delete: %s", d.Id(), err) } return nil } + +func FindOrganizationAdminAccountByAccountID(ctx context.Context, conn *detective.Detective, accountID string) (*detective.Administrator, error) { + input := &detective.ListOrganizationAdminAccountsInput{} + + return findOrganizationAdminAccount(ctx, conn, input, func(v *detective.Administrator) bool { + return aws.StringValue(v.AccountId) == accountID + }) +} + +func findOrganizationAdminAccount(ctx context.Context, conn *detective.Detective, input *detective.ListOrganizationAdminAccountsInput, filter tfslices.Predicate[*detective.Administrator]) (*detective.Administrator, error) { + output, err := findOrganizationAdminAccounts(ctx, conn, input, filter) + + if err != nil { + return nil, err + } + + return tfresource.AssertSinglePtrResult(output) +} + +func findOrganizationAdminAccounts(ctx context.Context, conn *detective.Detective, input *detective.ListOrganizationAdminAccountsInput, filter tfslices.Predicate[*detective.Administrator]) ([]*detective.Administrator, error) { + var output []*detective.Administrator + + err := conn.ListOrganizationAdminAccountsPagesWithContext(ctx, input, func(page *detective.ListOrganizationAdminAccountsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.Administrators { + if v != nil && filter(v) { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} diff --git a/internal/service/detective/organization_admin_account_test.go b/internal/service/detective/organization_admin_account_test.go index 167e710d333..334e083a8d3 100644 --- a/internal/service/detective/organization_admin_account_test.go +++ b/internal/service/detective/organization_admin_account_test.go @@ -109,7 +109,7 @@ func testAccCheckOrganizationAdminAccountDestroy(ctx context.Context) resource.T continue } - adminAccount, err := tfdetective.FindAdminAccount(ctx, conn, rs.Primary.ID) + adminAccount, err := tfdetective.FindOrganizationAdminAccountByAccountID(ctx, conn, rs.Primary.ID) // Because of this resource's dependency, the Organizations organization // will be deleted first, resulting in the following valid error @@ -141,7 +141,7 @@ func testAccCheckOrganizationAdminAccountExists(ctx context.Context, resourceNam conn := acctest.Provider.Meta().(*conns.AWSClient).DetectiveConn(ctx) - adminAccount, err := tfdetective.FindAdminAccount(ctx, conn, rs.Primary.ID) + adminAccount, err := tfdetective.FindOrganizationAdminAccountByAccountID(ctx, conn, rs.Primary.ID) if err != nil { return err diff --git a/internal/service/detective/status.go b/internal/service/detective/status.go deleted file mode 100644 index b48f406e1d5..00000000000 --- a/internal/service/detective/status.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package detective - -import ( - "context" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/detective" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" -) - -const ( - // AdminAccountStatus Found - adminAccountStatusFound = "Found" - - // AdminAccountStatus NotFound - adminAccountStatusNotFound = "NotFound" - - // AdminAccountStatus Unknown - adminAccountStatusUnknown = "Unknown" -) - -// adminAccountStatus fetches the AdminAccount -func adminAccountStatus(ctx context.Context, conn *detective.Detective, adminAccountID string) retry.StateRefreshFunc { - return func() (interface{}, string, error) { - adminAccount, err := getOrganizationAdminAccount(ctx, conn, adminAccountID) - - if err != nil { - return nil, adminAccountStatusUnknown, err - } - - if adminAccount == nil { - return adminAccount, adminAccountStatusNotFound, nil - } - - return adminAccount, adminAccountStatusFound, nil - } -} - -func getOrganizationAdminAccount(ctx context.Context, conn *detective.Detective, adminAccountID string) (*detective.Administrator, error) { - input := &detective.ListOrganizationAdminAccountsInput{} - var result *detective.Administrator - - err := conn.ListOrganizationAdminAccountsPagesWithContext(ctx, input, func(page *detective.ListOrganizationAdminAccountsOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, adminAccount := range page.Administrators { - if adminAccount == nil { - continue - } - - if aws.StringValue(adminAccount.AccountId) == adminAccountID { - result = adminAccount - return false - } - } - - return !lastPage - }) - - return result, err -} diff --git a/internal/service/detective/wait.go b/internal/service/detective/wait.go deleted file mode 100644 index 81cedcaf432..00000000000 --- a/internal/service/detective/wait.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package detective - -import ( - "context" - "time" - - "github.com/aws/aws-sdk-go/service/detective" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" -) - -const ( - // Maximum amount of time to wait for an Administrator to return Found - adminAccountFoundTimeout = 5 * time.Minute - // Maximum amount of time to wait for an Administrator to return NotFound - adminAccountNotFoundTimeout = 5 * time.Minute -) - -// waitAdminAccountFound waits for an AdminAccount to return Found -func waitAdminAccountFound(ctx context.Context, conn *detective.Detective, adminAccountID string) (*detective.Administrator, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{adminAccountStatusNotFound}, - Target: []string{adminAccountStatusFound}, - Refresh: adminAccountStatus(ctx, conn, adminAccountID), - Timeout: adminAccountFoundTimeout, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - - if output, ok := outputRaw.(*detective.Administrator); ok { - return output, err - } - - return nil, err -} - -// waitAdminAccountNotFound waits for an AdminAccount to return NotFound -func waitAdminAccountNotFound(ctx context.Context, conn *detective.Detective, adminAccountID string) (*detective.Administrator, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{adminAccountStatusFound}, - Target: []string{adminAccountStatusNotFound}, - Refresh: adminAccountStatus(ctx, conn, adminAccountID), - Timeout: adminAccountNotFoundTimeout, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - - if output, ok := outputRaw.(*detective.Administrator); ok { - return output, err - } - - return nil, err -} From 7b7b26598831815cfc273db1f283a8d4f104c090 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 30 Oct 2023 10:49:13 -0400 Subject: [PATCH 18/21] r/aws_detective_organization_admin_account: Tidy up acceptance tests. --- .../detective/organization_admin_account.go | 2 +- .../organization_admin_account_test.go | 37 +++++-------------- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/internal/service/detective/organization_admin_account.go b/internal/service/detective/organization_admin_account.go index 78115928b9f..8f7fc0aebe9 100644 --- a/internal/service/detective/organization_admin_account.go +++ b/internal/service/detective/organization_admin_account.go @@ -148,7 +148,7 @@ func findOrganizationAdminAccounts(ctx context.Context, conn *detective.Detectiv return !lastPage }) - if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { + if tfawserr.ErrMessageContains(err, detective.ErrCodeValidationException, "account is not a member of an organization") { return nil, &retry.NotFoundError{ LastError: err, LastRequest: input, diff --git a/internal/service/detective/organization_admin_account_test.go b/internal/service/detective/organization_admin_account_test.go index 334e083a8d3..6d111ab5d94 100644 --- a/internal/service/detective/organization_admin_account_test.go +++ b/internal/service/detective/organization_admin_account_test.go @@ -9,12 +9,12 @@ import ( "testing" "github.com/aws/aws-sdk-go/service/detective" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" tfdetective "github.com/hashicorp/terraform-provider-aws/internal/service/detective" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func testAccOrganizationAdminAccount_basic(t *testing.T) { @@ -73,7 +73,6 @@ func testAccOrganizationAdminAccount_disappears(t *testing.T) { func testAccOrganizationAdminAccount_MultiRegion(t *testing.T) { ctx := acctest.Context(t) - resourceName := "aws_detective_organization_admin_account.test" altResourceName := "aws_detective_organization_admin_account.alternate" thirdResourceName := "aws_detective_organization_admin_account.third" @@ -109,11 +108,9 @@ func testAccCheckOrganizationAdminAccountDestroy(ctx context.Context) resource.T continue } - adminAccount, err := tfdetective.FindOrganizationAdminAccountByAccountID(ctx, conn, rs.Primary.ID) + _, err := tfdetective.FindOrganizationAdminAccountByAccountID(ctx, conn, rs.Primary.ID) - // Because of this resource's dependency, the Organizations organization - // will be deleted first, resulting in the following valid error - if tfawserr.ErrMessageContains(err, detective.ErrCodeValidationException, "account is not a member of an organization") { + if tfresource.NotFound(err) { continue } @@ -121,37 +118,25 @@ func testAccCheckOrganizationAdminAccountDestroy(ctx context.Context) resource.T return err } - if adminAccount == nil { - continue - } - - return fmt.Errorf("expected Detective Organization Admin Account (%s) to be removed", rs.Primary.ID) + return fmt.Errorf("Detective Organization Admin Account %s still exists", rs.Primary.ID) } return nil } } -func testAccCheckOrganizationAdminAccountExists(ctx context.Context, resourceName string) resource.TestCheckFunc { +func testAccCheckOrganizationAdminAccountExists(ctx context.Context, n string) resource.TestCheckFunc { return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[resourceName] + rs, ok := s.RootModule().Resources[n] if !ok { - return fmt.Errorf("Not found: %s", resourceName) + return fmt.Errorf("Not found: %s", n) } conn := acctest.Provider.Meta().(*conns.AWSClient).DetectiveConn(ctx) - adminAccount, err := tfdetective.FindOrganizationAdminAccountByAccountID(ctx, conn, rs.Primary.ID) + _, err := tfdetective.FindOrganizationAdminAccountByAccountID(ctx, conn, rs.Primary.ID) - if err != nil { - return err - } - - if adminAccount == nil { - return fmt.Errorf("Detective Organization Admin Account (%s) not found", rs.Primary.ID) - } - - return nil + return err } } @@ -177,9 +162,7 @@ resource "aws_detective_organization_admin_account" "test" { } func testAccOrganizationAdminAccountConfig_multiRegion() string { - return acctest.ConfigCompose( - acctest.ConfigMultipleRegionProvider(3), - ` + return acctest.ConfigCompose(acctest.ConfigMultipleRegionProvider(3), ` data "aws_caller_identity" "current" {} data "aws_partition" "current" {} From 635834745722de1c4610de5e0ff014d1eefdee4d Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 30 Oct 2023 11:11:28 -0400 Subject: [PATCH 19/21] r/aws_detective_organization_configuration: Cosmetics. --- .../service/detective/organization_configuration.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/internal/service/detective/organization_configuration.go b/internal/service/detective/organization_configuration.go index 2b97a2f0b0c..c17f01be6d5 100644 --- a/internal/service/detective/organization_configuration.go +++ b/internal/service/detective/organization_configuration.go @@ -31,7 +31,6 @@ func ResourceOrganizationConfiguration() *schema.Resource { Type: schema.TypeBool, Required: true, }, - "graph_arn": { Type: schema.TypeString, Required: true, @@ -46,7 +45,6 @@ func resourceOrganizationConfigurationUpdate(ctx context.Context, d *schema.Reso conn := meta.(*conns.AWSClient).DetectiveConn(ctx) graphARN := d.Get("graph_arn").(string) - input := &detective.UpdateOrganizationConfigurationInput{ AutoEnable: aws.Bool(d.Get("auto_enable").(bool)), GraphArn: aws.String(graphARN), @@ -58,7 +56,9 @@ func resourceOrganizationConfigurationUpdate(ctx context.Context, d *schema.Reso return diag.Errorf("updating Detective Organization Configuration (%s): %s", graphARN, err) } - d.SetId(graphARN) + if d.IsNewResource() { + d.SetId(graphARN) + } return resourceOrganizationConfigurationRead(ctx, d, meta) } @@ -76,10 +76,6 @@ func resourceOrganizationConfigurationRead(ctx context.Context, d *schema.Resour return diag.Errorf("reading Detective Organization Configuration (%s): %s", d.Id(), err) } - if output == nil { - return diag.Errorf("reading Detective Organization Configuration (%s): empty response", d.Id()) - } - d.Set("auto_enable", output.AutoEnable) d.Set("graph_arn", d.Id()) From e02eacf7587e785250826b4082f2a09887601b1c Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 30 Oct 2023 11:14:59 -0400 Subject: [PATCH 20/21] Add CHANGELOG entries. --- .changelog/25237.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .changelog/25237.txt diff --git a/.changelog/25237.txt b/.changelog/25237.txt new file mode 100644 index 00000000000..7a5da2f0791 --- /dev/null +++ b/.changelog/25237.txt @@ -0,0 +1,15 @@ +```release-note:new-resource +aws_detective_organization_admin_account +``` + +```release-note:note +resource/aws_detective_organization_admin_account: Because we cannot easily test this functionality, it is best effort and we ask for community help in testing +``` + +```release-note:new-resource +aws_detective_organization_configuration +``` + +```release-note:note +resource/aws_detective_organization_configuration: Because we cannot easily test this functionality, it is best effort and we ask for community help in testing +``` \ No newline at end of file From 5ecb2916dc4aaabe07c277d4cc3d4690bcef6dc7 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 30 Oct 2023 11:48:15 -0400 Subject: [PATCH 21/21] Fix golangci-lint 'staticcheck'. --- .ci/.golangci2.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/.golangci2.yml b/.ci/.golangci2.yml index 81350b4bfe0..5a79b988b26 100644 --- a/.ci/.golangci2.yml +++ b/.ci/.golangci2.yml @@ -53,7 +53,7 @@ issues: - linters: - staticcheck path: internal/service/detective/ - text: "SA1019: resp.VolumeUsageInBytes is deprecated: This property is deprecated. Use VolumeUsageByDatasourcePackage instead" + text: "SA1019: member.VolumeUsageInBytes is deprecated: This property is deprecated. Use VolumeUsageByDatasourcePackage instead" - linters: - staticcheck path: internal/service/ecr/