diff --git a/.changelog/31430.txt b/.changelog/31430.txt new file mode 100644 index 00000000000..80f3f768e49 --- /dev/null +++ b/.changelog/31430.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_resourcegroups_resource +``` diff --git a/internal/service/resourcegroups/resource.go b/internal/service/resourcegroups/resource.go new file mode 100644 index 00000000000..a7074479207 --- /dev/null +++ b/internal/service/resourcegroups/resource.go @@ -0,0 +1,157 @@ +package resourcegroups + +import ( + "context" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/resourcegroups" + "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/create" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @SDKResource("aws_resourcegroups_resource", name="Resource") +func ResourceResource() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceResourceCreate, + ReadWithoutTimeout: resourceResourceRead, + DeleteWithoutTimeout: resourceResourceDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "group_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "resource_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "resource_type": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +const ( + ResNameResource = "Resource" +) + +func resourceResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).ResourceGroupsConn() + + group := d.Get("group_arn").(string) + resourceArn := d.Get("resource_arn").(string) + + in := &resourcegroups.GroupResourcesInput{ + Group: aws.String(group), + ResourceArns: []*string{&resourceArn}, + } + + _, err := conn.GroupResourcesWithContext(ctx, in) + + if err != nil { + return create.DiagError(names.ResourceGroups, create.ErrActionCreating, ResNameResource, d.Get("name").(string), err) + } + + vars := []string{ + strings.Split(strings.ToLower(d.Get("group_arn").(string)), "/")[1], + strings.Split(d.Get("resource_arn").(string), "/")[1], + } + + d.SetId(strings.Join(vars, "_")) + + return resourceResourceRead(ctx, d, meta) +} + +func resourceResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).ResourceGroupsConn() + + out, err := FindResourceByARN(ctx, conn, d.Get("group_arn").(string), d.Get("resource_arn").(string)) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] ResourceGroups Resource (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return create.DiagError(names.ResourceGroups, create.ErrActionReading, ResNameResource, d.Id(), err) + } + + d.Set("resource_arn", out.Identifier.ResourceArn) + d.Set("resource_type", out.Identifier.ResourceType) + + return nil +} + +func resourceResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).ResourceGroupsConn() + + log.Printf("[INFO] Deleting ResourceGroups Resource %s", d.Id()) + + group := d.Get("group_arn").(string) + resourceArn := d.Get("resource_arn").(string) + + _, err := conn.UngroupResourcesWithContext(ctx, &resourcegroups.UngroupResourcesInput{ + Group: aws.String(group), + ResourceArns: []*string{&resourceArn}, + }) + + if err != nil { + return create.DiagError(names.ResourceGroups, create.ErrActionDeleting, ResNameResource, d.Id(), err) + } + + _, err = tfresource.RetryUntilNotFound(ctx, d.Timeout(schema.TimeoutDelete), func() (interface{}, error) { + return FindResourceByARN(ctx, conn, d.Get("group_arn").(string), d.Get("resource_arn").(string)) + }) + + if err != nil { + return create.DiagError(names.ResourceGroups, create.ErrActionDeleting, ResNameResource, d.Id(), err) + } + + return nil +} + +func FindResourceByARN(ctx context.Context, conn *resourcegroups.ResourceGroups, groupArn, resourceArn string) (*resourcegroups.ListGroupResourcesItem, error) { + input := &resourcegroups.ListGroupResourcesInput{ + Group: aws.String(groupArn), + } + + output, err := conn.ListGroupResourcesWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, resourcegroups.ErrCodeNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + for _, resourceItem := range output.Resources { + if aws.StringValue(resourceItem.Identifier.ResourceArn) == resourceArn { + return resourceItem, nil + } + } + + return nil, tfresource.NewEmptyResultError(input) +} diff --git a/internal/service/resourcegroups/resource_test.go b/internal/service/resourcegroups/resource_test.go new file mode 100644 index 00000000000..d06192d3ba2 --- /dev/null +++ b/internal/service/resourcegroups/resource_test.go @@ -0,0 +1,171 @@ +package resourcegroups_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/resourcegroups" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "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" + "github.com/hashicorp/terraform-provider-aws/internal/create" + tfresourcegroups "github.com/hashicorp/terraform-provider-aws/internal/service/resourcegroups" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccResourceGroupsResource_basic(t *testing.T) { + ctx := context.Background() + var r resourcegroups.ListGroupResourcesItem + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_resourcegroups_resource.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, resourcegroups.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccResourceConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(ctx, resourceName, &r), + resource.TestCheckResourceAttr(resourceName, "resource_type", "AWS::EC2::Host"), + resource.TestCheckResourceAttrSet(resourceName, "group_arn"), + resource.TestCheckResourceAttrSet(resourceName, "resource_arn"), + ), + }, + }, + }) +} + +func testAccCheckResourceDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).ResourceGroupsConn() + ctx := context.Background() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_resourcegroups_resource" { + continue + } + + _, err := tfresourcegroups.FindResourceByARN(ctx, conn, rs.Primary.Attributes["group_arn"], rs.Primary.Attributes["resource_arn"]) + + if err != nil { + if tfawserr.ErrCodeEquals(err, resourcegroups.ErrCodeNotFoundException) { + return nil + } + return err + } + + return create.Error(names.ResourceGroups, create.ErrActionCheckingDestroyed, tfresourcegroups.ResNameResource, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil +} + +func testAccCheckResourceExists(ctx context.Context, name string, resource *resourcegroups.ListGroupResourcesItem) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.ResourceGroups, create.ErrActionCheckingExistence, tfresourcegroups.ResNameResource, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.ResourceGroups, create.ErrActionCheckingExistence, tfresourcegroups.ResNameResource, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).ResourceGroupsConn() + + resp, err := tfresourcegroups.FindResourceByARN(ctx, conn, rs.Primary.Attributes["group_arn"], rs.Primary.Attributes["resource_arn"]) + + if err != nil { + return create.Error(names.ResourceGroups, create.ErrActionCheckingExistence, tfresourcegroups.ResNameResource, rs.Primary.ID, err) + } + + *resource = *resp + + return nil + } +} + +func testAccResourceConfig_base(rName string) string { + return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` +resource "aws_ec2_host" "test" { + auto_placement = "on" + availability_zone = data.aws_availability_zones.available.names[0] + host_recovery = "off" + instance_family = "c5" + + tags = { + Name = %[1]q + } +} + +resource "aws_resourcegroups_group" "test" { + name = %[1]q + + configuration { + type = "AWS::EC2::HostManagement" + parameters { + name = "any-host-based-license-configuration" + values = [ + "true" + ] + } + + parameters { + name = "auto-allocate-host" + values = [ + "false" + ] + } + parameters { + name = "auto-host-recovery" + values = [ + "false" + ] + } + parameters { + name = "auto-release-host" + values = [ + "false" + ] + } + + } + + configuration { + type = "AWS::ResourceGroups::Generic" + parameters { + name = "allowed-resource-types" + values = [ + "AWS::EC2::Host" + ] + } + parameters { + name = "deletion-protection" + values = [ + "UNLESS_EMPTY" + ] + } + } +} + + +`, rName)) +} + +func testAccResourceConfig_basic(rName string) string { + return acctest.ConfigCompose(testAccResourceConfig_base(rName), ` +resource "aws_resourcegroups_resource" "test" { + group_arn = aws_resourcegroups_group.test.arn + resource_arn = aws_ec2_host.test.arn +} + `) +} diff --git a/internal/service/resourcegroups/service_package_gen.go b/internal/service/resourcegroups/service_package_gen.go index 41b49a1e4c3..2385feb5dce 100644 --- a/internal/service/resourcegroups/service_package_gen.go +++ b/internal/service/resourcegroups/service_package_gen.go @@ -33,6 +33,11 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka IdentifierAttribute: "arn", }, }, + { + Factory: ResourceResource, + TypeName: "aws_resourcegroups_resource", + Name: "Resource", + }, } } diff --git a/website/docs/r/resourcegroups_resource.html.markdown b/website/docs/r/resourcegroups_resource.html.markdown new file mode 100644 index 00000000000..e881d18d187 --- /dev/null +++ b/website/docs/r/resourcegroups_resource.html.markdown @@ -0,0 +1,57 @@ +--- +subcategory: "Resource Groups" +layout: "aws" +page_title: "AWS: aws_resourcegroups_resource" +description: |- + Terraform resource for managing an AWS Resource Groups Resource. +--- + +# Resource: aws_resourcegroups_resource + +Terraform resource for managing an AWS Resource Groups Resource. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_ec2_host" "example" { + instance_family = "t3" + availability_zone = "us-east-1a" + host_recovery = "off" + auto_placement = "on" +} + +resource "aws_resourcegroups_group" "example" { + name = "example" +} + +resource "aws_resourcegroups_resource" "example" { + group_arn = aws_resourcegroups_group.example.arn + resource_arn = aws_ec2_host.example.arn +} + +``` + +## Argument Reference + +The following arguments are required: + +* `group_arn` - (Required) The name or the ARN of the resource group to add resources to. + +The following arguments are optional: + +* `resource_arn` - (Required) The ARN of the resource to be added to the group. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `resource_type` - The resource type of a resource, such as `AWS::EC2::Instance`. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `5m`) +* `delete` - (Default `5m`)