Skip to content

Commit

Permalink
r/aws_devopsguru_resource_collection: new resource (#36489)
Browse files Browse the repository at this point in the history
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
```
  • Loading branch information
jar-b authored Mar 21, 2024
1 parent f49b8f2 commit df742ff
Show file tree
Hide file tree
Showing 7 changed files with 764 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .changelog/36489.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
aws_devopsguru_resource_collection
```
7 changes: 7 additions & 0 deletions internal/service/devopsguru/devopsguru_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion internal/service/devopsguru/exports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package devopsguru
// Exports for use in tests only.
var (
ResourceEventSourcesConfig = newResourceEventSourcesConfig
ResourceResourceCollection = newResourceResourceCollection

FindEventSourcesConfig = findEventSourcesConfig
FindEventSourcesConfig = findEventSourcesConfig
FindResourceCollectionByID = findResourceCollectionByID
)
318 changes: 318 additions & 0 deletions internal/service/devopsguru/resource_collection.go
Original file line number Diff line number Diff line change
@@ -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"`
}
Loading

0 comments on commit df742ff

Please sign in to comment.