From df742ff98f6385608a104d0c3fbaba9a289384c8 Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Thu, 21 Mar 2024 10:58:24 -0400 Subject: [PATCH] r/aws_devopsguru_resource_collection: new resource (#36489) This resource will allow practitioners to manage Amazon DevOps Guru resource collections with Terraform. Resource collections are configured at the account level, so this resource should be configured only once to avoid competing definitions resulting in persistent differences. Additional information can be found the in Amazon DevOps Guru User Guide: https://docs.aws.amazon.com/devops-guru/latest/userguide/welcome.html ```console % make testacc PKG=devopsguru TESTS=TestAccDevOpsGuru_serial/ResourceCollection/ ==> Checking that code complies with gofmt requirements... TF_ACC=1 go1.21.8 test ./internal/service/devopsguru/... -v -count 1 -parallel 20 -run='TestAccDevOpsGuru_serial/ResourceCollection/' -timeout 360m --- PASS: TestAccDevOpsGuru_serial (104.94s) --- PASS: TestAccDevOpsGuru_serial/ResourceCollection (104.94s) --- PASS: TestAccDevOpsGuru_serial/ResourceCollection/disappears (11.05s) --- PASS: TestAccDevOpsGuru_serial/ResourceCollection/tags (11.20s) --- PASS: TestAccDevOpsGuru_serial/ResourceCollection/tagsAllResources (11.25s) --- PASS: TestAccDevOpsGuru_serial/ResourceCollection/basic (11.33s) --- PASS: TestAccDevOpsGuru_serial/ResourceCollection/cloudformation (60.10s) PASS ok github.com/hashicorp/terraform-provider-aws/internal/service/devopsguru 110.349s ``` --- .changelog/36489.txt | 3 + .../service/devopsguru/devopsguru_test.go | 7 + internal/service/devopsguru/exports_test.go | 4 +- .../service/devopsguru/resource_collection.go | 318 +++++++++++++++++ .../devopsguru/resource_collection_test.go | 324 ++++++++++++++++++ .../service/devopsguru/service_package_gen.go | 4 + ...vopsguru_resource_collection.html.markdown | 105 ++++++ 7 files changed, 764 insertions(+), 1 deletion(-) create mode 100644 .changelog/36489.txt create mode 100644 internal/service/devopsguru/resource_collection.go create mode 100644 internal/service/devopsguru/resource_collection_test.go create mode 100644 website/docs/r/devopsguru_resource_collection.html.markdown diff --git a/.changelog/36489.txt b/.changelog/36489.txt new file mode 100644 index 000000000000..69a64294d155 --- /dev/null +++ b/.changelog/36489.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_devopsguru_resource_collection +``` diff --git a/internal/service/devopsguru/devopsguru_test.go b/internal/service/devopsguru/devopsguru_test.go index 2694c1a1c5b3..89cddacdbf31 100644 --- a/internal/service/devopsguru/devopsguru_test.go +++ b/internal/service/devopsguru/devopsguru_test.go @@ -17,6 +17,13 @@ func TestAccDevOpsGuru_serial(t *testing.T) { "basic": testAccEventSourcesConfig_basic, "disappears": testAccEventSourcesConfig_disappears, }, + "ResourceCollection": { + "basic": testAccResourceCollection_basic, + "cloudformation": testAccResourceCollection_cloudformation, + "disappears": testAccResourceCollection_disappears, + "tags": testAccResourceCollection_tags, + "tagsAllResources": testAccResourceCollection_tagsAllResources, + }, } acctest.RunSerialTests2Levels(t, testCases, 0) diff --git a/internal/service/devopsguru/exports_test.go b/internal/service/devopsguru/exports_test.go index d518e6e8dbb4..602e786009b6 100644 --- a/internal/service/devopsguru/exports_test.go +++ b/internal/service/devopsguru/exports_test.go @@ -6,6 +6,8 @@ package devopsguru // Exports for use in tests only. var ( ResourceEventSourcesConfig = newResourceEventSourcesConfig + ResourceResourceCollection = newResourceResourceCollection - FindEventSourcesConfig = findEventSourcesConfig + FindEventSourcesConfig = findEventSourcesConfig + FindResourceCollectionByID = findResourceCollectionByID ) diff --git a/internal/service/devopsguru/resource_collection.go b/internal/service/devopsguru/resource_collection.go new file mode 100644 index 000000000000..3b46e471b3da --- /dev/null +++ b/internal/service/devopsguru/resource_collection.go @@ -0,0 +1,318 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package devopsguru + +import ( + "context" + "errors" + + "github.com/aws/aws-sdk-go-v2/service/devopsguru" + awstypes "github.com/aws/aws-sdk-go-v2/service/devopsguru/types" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource(name="Resource Collection") +func newResourceResourceCollection(_ context.Context) (resource.ResourceWithConfigure, error) { + return &resourceResourceCollection{}, nil +} + +const ( + ResNameResourceCollection = "Resource Collection" +) + +type resourceResourceCollection struct { + framework.ResourceWithConfigure +} + +func (r *resourceResourceCollection) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "aws_devopsguru_resource_collection" +} + +func (r *resourceResourceCollection) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": framework.IDAttribute(), + "type": schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.ResourceCollectionType](), + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "cloudformation": schema.ListNestedBlock{ + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + CustomType: fwtypes.NewListNestedObjectTypeOf[cloudformationData](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "stack_names": schema.ListAttribute{ + Required: true, + CustomType: fwtypes.ListOfStringType, + ElementType: types.StringType, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + }, + }, + }, + }, + "tags": schema.ListNestedBlock{ + // Attempting to specify multiple app boundary keys will result in a ValidationException + // + // ValidationException: Multiple app boundary keys are not supported + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + CustomType: fwtypes.NewListNestedObjectTypeOf[tagsData](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "app_boundary_key": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "tag_values": schema.ListAttribute{ + Required: true, + CustomType: fwtypes.ListOfStringType, + ElementType: types.StringType, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + }, + }, + }, + }, + }, + } +} + +func (r *resourceResourceCollection) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().DevOpsGuruClient(ctx) + + var plan resourceResourceCollectionData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + plan.ID = types.StringValue(plan.Type.ValueString()) + + rc := &awstypes.UpdateResourceCollectionFilter{} + resp.Diagnostics.Append(flex.Expand(ctx, plan, rc)...) + if resp.Diagnostics.HasError() { + return + } + + if !plan.Tags.IsNull() { + // Fields named "Tags" are currently hardcoded to be ignored by AutoFlex. Expanding plan.Tags + // into the request structs Tags field is a temporary workaround until the AutoFlex + // options implementation can be merged. + // + // Ref: https://github.com/hashicorp/terraform-provider-aws/pull/36437 + resp.Diagnostics.Append(flex.Expand(ctx, plan.Tags, &rc.Tags)...) + if resp.Diagnostics.HasError() { + return + } + } + + in := &devopsguru.UpdateResourceCollectionInput{ + Action: awstypes.UpdateResourceCollectionActionAdd, + ResourceCollection: rc, + } + + out, err := conn.UpdateResourceCollection(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DevOpsGuru, create.ErrActionCreating, ResNameResourceCollection, plan.ID.String(), err), + err.Error(), + ) + return + } + if out == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DevOpsGuru, create.ErrActionCreating, ResNameResourceCollection, plan.ID.String(), nil), + errors.New("empty output").Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceResourceCollection) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().DevOpsGuruClient(ctx) + + var state resourceResourceCollectionData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := findResourceCollectionByID(ctx, conn, state.ID.ValueString()) + if tfresource.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DevOpsGuru, create.ErrActionSetting, ResNameResourceCollection, state.ID.String(), err), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(flex.Flatten(ctx, out, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // Fields named "Tags" are currently hardcoded to be ignored by AutoFlex. Flattening the Tags + // struct from the response into state.Tags is a temporary workaround until the AutoFlex + // options implementation can be merged. + // + // Ref: https://github.com/hashicorp/terraform-provider-aws/pull/36437 + resp.Diagnostics.Append(flex.Flatten(ctx, out.Tags, &state.Tags)...) + if resp.Diagnostics.HasError() { + return + } + + // Copy from ID on read to support import + state.Type = fwtypes.StringEnumValue(awstypes.ResourceCollectionType(state.ID.ValueString())) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceResourceCollection) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Update is a no-op +} + +func (r *resourceResourceCollection) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().DevOpsGuruClient(ctx) + + var state resourceResourceCollectionData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + rc := &awstypes.UpdateResourceCollectionFilter{} + resp.Diagnostics.Append(flex.Expand(ctx, state, rc)...) + if resp.Diagnostics.HasError() { + return + } + + if !state.Tags.IsNull() { + // Fields named "Tags" are currently hardcoded to be ignored by AutoFlex. Expanding state.Tags + // into the request structs Tags field is a temporary workaround until the AutoFlex + // options implementation can be merged. + // + // Ref: https://github.com/hashicorp/terraform-provider-aws/pull/36437 + resp.Diagnostics.Append(flex.Expand(ctx, state.Tags, &rc.Tags)...) + if resp.Diagnostics.HasError() { + return + } + } + + in := &devopsguru.UpdateResourceCollectionInput{ + Action: awstypes.UpdateResourceCollectionActionRemove, + ResourceCollection: rc, + } + + _, err := conn.UpdateResourceCollection(ctx, in) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.DevOpsGuru, create.ErrActionDeleting, ResNameResourceCollection, state.ID.String(), err), + err.Error(), + ) + return + } +} + +func (r *resourceResourceCollection) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func findResourceCollectionByID(ctx context.Context, conn *devopsguru.Client, id string) (*awstypes.ResourceCollectionFilter, error) { + collectionType := awstypes.ResourceCollectionType(id) + in := &devopsguru.GetResourceCollectionInput{ + ResourceCollectionType: collectionType, + } + + out, err := conn.GetResourceCollection(ctx, in) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + return nil, err + } + + if out == nil || out.ResourceCollection == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + switch collectionType { + case awstypes.ResourceCollectionTypeAwsCloudFormation, awstypes.ResourceCollectionTypeAwsService: + // AWS_CLOUD_FORMATION and AWS_SERVICE collection types should have + // a non-empty array of stack names + if out.ResourceCollection.CloudFormation == nil || + len(out.ResourceCollection.CloudFormation.StackNames) == 0 { + return nil, &retry.NotFoundError{ + LastRequest: in, + } + } + case awstypes.ResourceCollectionTypeAwsTags: + // AWS_TAGS collection types should have a Tags array with 1 item, + // and that object should have a TagValues array with at least 1 item + if len(out.ResourceCollection.Tags) == 0 || + len(out.ResourceCollection.Tags) == 1 && len(out.ResourceCollection.Tags[0].TagValues) == 0 { + return nil, &retry.NotFoundError{ + LastRequest: in, + } + } + } + + return out.ResourceCollection, nil +} + +type resourceResourceCollectionData struct { + CloudFormation fwtypes.ListNestedObjectValueOf[cloudformationData] `tfsdk:"cloudformation"` + ID types.String `tfsdk:"id"` + Tags fwtypes.ListNestedObjectValueOf[tagsData] `tfsdk:"tags"` + Type fwtypes.StringEnum[awstypes.ResourceCollectionType] `tfsdk:"type"` +} + +type cloudformationData struct { + StackNames fwtypes.ListValueOf[types.String] `tfsdk:"stack_names"` +} + +type tagsData struct { + AppBoundaryKey types.String `tfsdk:"app_boundary_key"` + TagValues fwtypes.ListValueOf[types.String] `tfsdk:"tag_values"` +} diff --git a/internal/service/devopsguru/resource_collection_test.go b/internal/service/devopsguru/resource_collection_test.go new file mode 100644 index 000000000000..05f5cead2d90 --- /dev/null +++ b/internal/service/devopsguru/resource_collection_test.go @@ -0,0 +1,324 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package devopsguru_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/devopsguru/types" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + fwtypes "github.com/hashicorp/terraform-plugin-framework/types" + 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" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + tfdevopsguru "github.com/hashicorp/terraform-provider-aws/internal/service/devopsguru" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func testAccResourceCollection_basic(t *testing.T) { + ctx := acctest.Context(t) + var resourcecollection types.ResourceCollectionFilter + resourceName := "aws_devopsguru_resource_collection.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.DevOpsGuruEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.DevOpsGuruServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResourceCollectionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccResourceCollectionConfig_basic(), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceCollectionExists(ctx, resourceName, &resourcecollection), + resource.TestCheckResourceAttr(resourceName, "type", string(types.ResourceCollectionTypeAwsService)), + resource.TestCheckResourceAttr(resourceName, "cloudformation.#", "1"), + resource.TestCheckResourceAttr(resourceName, "cloudformation.0.stack_names.#", "1"), + resource.TestCheckResourceAttr(resourceName, "cloudformation.0.stack_names.0", "*"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccResourceCollection_disappears(t *testing.T) { + ctx := acctest.Context(t) + var resourcecollection types.ResourceCollectionFilter + resourceName := "aws_devopsguru_resource_collection.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.DevOpsGuruEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.DevOpsGuruServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResourceCollectionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccResourceCollectionConfig_basic(), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceCollectionExists(ctx, resourceName, &resourcecollection), + acctest.CheckFrameworkResourceDisappearsWithStateFunc(ctx, acctest.Provider, tfdevopsguru.ResourceResourceCollection, resourceName, resourceCollectionDisappearsStateFunc()), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccResourceCollection_cloudformation(t *testing.T) { + ctx := acctest.Context(t) + var resourcecollection types.ResourceCollectionFilter + resourceName := "aws_devopsguru_resource_collection.test" + cfnStackResourceName := "aws_cloudformation_stack.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.DevOpsGuruEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.DevOpsGuruServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResourceCollectionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccResourceCollectionConfig_cloudformation(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceCollectionExists(ctx, resourceName, &resourcecollection), + resource.TestCheckResourceAttr(resourceName, "type", string(types.ResourceCollectionTypeAwsCloudFormation)), + resource.TestCheckResourceAttr(resourceName, "cloudformation.#", "1"), + resource.TestCheckResourceAttr(resourceName, "cloudformation.0.stack_names.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "cloudformation.0.stack_names.0", cfnStackResourceName, "name"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccResourceCollection_tags(t *testing.T) { + ctx := acctest.Context(t) + var resourcecollection types.ResourceCollectionFilter + resourceName := "aws_devopsguru_resource_collection.test" + appBoundaryKey := "DevOps-Guru-tfacctest" + tagValue := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.DevOpsGuruEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.DevOpsGuruServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResourceCollectionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccResourceCollectionConfig_tags(appBoundaryKey, tagValue), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceCollectionExists(ctx, resourceName, &resourcecollection), + resource.TestCheckResourceAttr(resourceName, "type", string(types.ResourceCollectionTypeAwsTags)), + resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.0.app_boundary_key", appBoundaryKey), + resource.TestCheckResourceAttr(resourceName, "tags.0.tag_values.#", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.0.tag_values.0", tagValue), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccResourceCollection_tagsAllResources(t *testing.T) { + ctx := acctest.Context(t) + var resourcecollection types.ResourceCollectionFilter + resourceName := "aws_devopsguru_resource_collection.test" + appBoundaryKey := "DevOps-Guru-tfacctest" + tagValue := "*" // To include all resources with the specified app boundary key + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.DevOpsGuruEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.DevOpsGuruServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResourceCollectionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccResourceCollectionConfig_tags(appBoundaryKey, tagValue), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceCollectionExists(ctx, resourceName, &resourcecollection), + resource.TestCheckResourceAttr(resourceName, "type", string(types.ResourceCollectionTypeAwsTags)), + resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.0.app_boundary_key", appBoundaryKey), + resource.TestCheckResourceAttr(resourceName, "tags.0.tag_values.#", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.0.tag_values.0", tagValue), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func resourceCollectionDisappearsStateFunc() func(ctx context.Context, state *tfsdk.State, is *terraform.InstanceState) error { + return func(ctx context.Context, state *tfsdk.State, is *terraform.InstanceState) error { + if err := fwdiag.DiagnosticsError(state.SetAttribute(ctx, path.Root("id"), is.Attributes["id"])); err != nil { + return err + } + + // The delete operation requires passing in the configured array of stack names + // with a "REMOVE" action. Manually construct the root cloudformation attribute + // to match what is created by the _basic test configuration. + var diags diag.Diagnostics + attrType := map[string]attr.Type{"stack_names": fwtypes.ListType{ElemType: fwtypes.StringType}} + obj := map[string]attr.Value{ + "stack_names": flex.FlattenFrameworkStringValueList(ctx, []string{"*"}), + } + objVal, d := fwtypes.ObjectValue(attrType, obj) + diags.Append(d...) + + elemType := fwtypes.ObjectType{AttrTypes: attrType} + listVal, d := fwtypes.ListValue(elemType, []attr.Value{objVal}) + diags.Append(d...) + + if diags.HasError() { + return fwdiag.DiagnosticsError(diags) + } + + if err := fwdiag.DiagnosticsError(state.SetAttribute(ctx, path.Root("cloudformation"), listVal)); err != nil { + return err + } + + return nil + } +} + +func testAccCheckResourceCollectionDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).DevOpsGuruClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_devopsguru_resource_collection" { + continue + } + + _, err := tfdevopsguru.FindResourceCollectionByID(ctx, conn, rs.Primary.ID) + if errs.IsA[*types.ResourceNotFoundException](err) || tfresource.NotFound(err) { + return nil + } + if err != nil { + return create.Error(names.DevOpsGuru, create.ErrActionCheckingDestroyed, tfdevopsguru.ResNameResourceCollection, rs.Primary.ID, err) + } + + return create.Error(names.DevOpsGuru, create.ErrActionCheckingDestroyed, tfdevopsguru.ResNameResourceCollection, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckResourceCollectionExists(ctx context.Context, name string, resourcecollection *types.ResourceCollectionFilter) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.DevOpsGuru, create.ErrActionCheckingExistence, tfdevopsguru.ResNameResourceCollection, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.DevOpsGuru, create.ErrActionCheckingExistence, tfdevopsguru.ResNameResourceCollection, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).DevOpsGuruClient(ctx) + resp, err := tfdevopsguru.FindResourceCollectionByID(ctx, conn, rs.Primary.ID) + if err != nil { + return create.Error(names.DevOpsGuru, create.ErrActionCheckingExistence, tfdevopsguru.ResNameResourceCollection, rs.Primary.ID, err) + } + + *resourcecollection = *resp + + return nil + } +} + +func testAccResourceCollectionConfig_basic() string { + return ` +resource "aws_devopsguru_resource_collection" "test" { + type = "AWS_SERVICE" + cloudformation { + stack_names = ["*"] + } +} +` +} + +func testAccResourceCollectionConfig_cloudformation(rName string) string { + return fmt.Sprintf(` +resource "aws_cloudformation_stack" "test" { + name = %[1]q + on_failure = "DO_NOTHING" + + template_body = jsonencode({ + AWSTemplateFormatVersion = "2010-09-09" + Resources = { + S3Bucket = { + Type = "AWS::S3::Bucket" + } + } + }) +} + +resource "aws_devopsguru_resource_collection" "test" { + type = "AWS_CLOUD_FORMATION" + cloudformation { + stack_names = [aws_cloudformation_stack.test.name] + } +} +`, rName) +} + +func testAccResourceCollectionConfig_tags(appBoundaryKey, tagValue string) string { + return fmt.Sprintf(` +resource "aws_devopsguru_resource_collection" "test" { + type = "AWS_TAGS" + tags { + app_boundary_key = %[1]q + tag_values = [%[2]q] + } +} +`, appBoundaryKey, tagValue) +} diff --git a/internal/service/devopsguru/service_package_gen.go b/internal/service/devopsguru/service_package_gen.go index f4fae9e02f90..bdd1d16a2120 100644 --- a/internal/service/devopsguru/service_package_gen.go +++ b/internal/service/devopsguru/service_package_gen.go @@ -24,6 +24,10 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic Factory: newResourceEventSourcesConfig, Name: "Event Sources Config", }, + { + Factory: newResourceResourceCollection, + Name: "Resource Collection", + }, } } diff --git a/website/docs/r/devopsguru_resource_collection.html.markdown b/website/docs/r/devopsguru_resource_collection.html.markdown new file mode 100644 index 000000000000..717e77f6349a --- /dev/null +++ b/website/docs/r/devopsguru_resource_collection.html.markdown @@ -0,0 +1,105 @@ +--- +subcategory: "DevOps Guru" +layout: "aws" +page_title: "AWS: aws_devopsguru_resource_collection" +description: |- + Terraform resource for managing an AWS DevOps Guru Resource Collection. +--- +# Resource: aws_devopsguru_resource_collection + +Terraform resource for managing an AWS DevOps Guru Resource Collection. + +~> Only one type of resource collection (All Account Resources, CloudFormation, or Tags) can be enabled in an account at a time. To avoid persistent differences, this resource should be defined only once. + +## Example Usage + +### All Account Resources + +```terraform +resource "aws_devopsguru_resource_collection" "example" { + type = "AWS_SERVICE" + cloudformation { + stack_names = ["*"] + } +} +``` + +### CloudFormation Stacks + +```terraform +resource "aws_devopsguru_resource_collection" "example" { + type = "AWS_CLOUD_FORMATION" + cloudformation { + stack_names = ["ExampleStack"] + } +} +``` + +### Tags + +```terraform +resource "aws_devopsguru_resource_collection" "example" { + type = "AWS_TAGS" + tags { + app_boundary_key = "DevOps-Guru-Example" + tag_values = ["Example-Value"] + } +} +``` + +### Tags All Resources + +To analyze all resources with the `app_boundary_key` regardless of the corresponding tag value, set `tag_values` to `["*"]`. + +```terraform +resource "aws_devopsguru_resource_collection" "example" { + type = "AWS_TAGS" + tags { + app_boundary_key = "DevOps-Guru-Example" + tag_values = ["*"] + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `type` - (Required) Type of AWS resource collection to create. Valid values are `AWS_CLOUD_FORMATION`, `AWS_SERVICE`, and `AWS_TAGS`. + +The following arguments are optional: + +* `cloudformation` - (Optional) A collection of AWS CloudFormation stacks. See [`cloudformation`](#cloudformation-argument-reference) below for additional details. +* `tags` - (Optional) AWS tags used to filter the resources in the resource collection See [`tags`](#tags-argument-reference) below for additional details. + +### `cloudformation` Argument Reference + +* `stack_names` - (Required) Array of the names of the AWS CloudFormation stacks. If `type` is `AWS_SERVICE` (all acccount resources) this array should be a single item containing a wildcard (`"*"`). + +### `tags` Argument Reference + +* `app_boundary_key` - (Required) An AWS tag key that is used to identify the AWS resources that DevOps Guru analyzes. All AWS resources in your account and Region tagged with this key make up your DevOps Guru application and analysis boundary. The key must begin with the prefix `DevOps-Guru-`. Any casing can be used for the prefix, but the associated tags __must use the same casing__ in their tag key. +* `tag_values` - (Required) Array of tag values. These can be used to further filter for specific resources within the application boundary. To analyze all resources tagged with the `app_boundary_key` regardless of the corresponding tag value, this array should be a single item containing a wildcard (`"*"`). + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `id` - Type of AWS resource collection to create (same value as `type`). + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import DevOps Guru Resource Collection using the `id`. For example: + +```terraform +import { + to = aws_devopsguru_resource_collection.example + id = "AWS_CLOUD_FORMATION" +} +``` + +Using `terraform import`, import DevOps Guru Resource Collection using the `id`. For example: + +```console +% terraform import aws_devopsguru_resource_collection.example AWS_CLOUD_FORMATION +```