Skip to content

Commit

Permalink
r/aws_devopsguru_service_integration: new resource (#36694)
Browse files Browse the repository at this point in the history
This resource will allow practitioners to manage AWS DevOps Guru service integration settings with Terraform.

```console
% make testacc PKG=devopsguru TESTS=TestAccDevOpsGuru_serial/ServiceIntegration/
==> 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/ServiceIntegration/'  -timeout 360m

--- PASS: TestAccDevOpsGuru_serial (39.21s)
    --- PASS: TestAccDevOpsGuru_serial/ServiceIntegration (39.21s)
        --- PASS: TestAccDevOpsGuru_serial/ServiceIntegration/basic (19.21s)
        --- PASS: TestAccDevOpsGuru_serial/ServiceIntegration/kms (20.00s)
PASS
ok      github.com/hashicorp/terraform-provider-aws/internal/service/devopsguru 44.690s
```
  • Loading branch information
jar-b authored Apr 3, 2024
1 parent 05a78a3 commit a1a3361
Show file tree
Hide file tree
Showing 7 changed files with 672 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/36694.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
aws_devopsguru_service_integration
```
4 changes: 4 additions & 0 deletions internal/service/devopsguru/devopsguru_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ func TestAccDevOpsGuru_serial(t *testing.T) {
"ResourceCollectionDataSource": {
"basic": testAccResourceCollectionDataSource_basic,
},
"ServiceIntegration": {
"basic": testAccServiceIntegration_basic,
"kms": testAccServiceIntegration_kms,
},
}

acctest.RunSerialTests2Levels(t, testCases, 0)
Expand Down
1 change: 1 addition & 0 deletions internal/service/devopsguru/exports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ var (
FindEventSourcesConfig = findEventSourcesConfig
FindNotificationChannelByID = findNotificationChannelByID
FindResourceCollectionByID = findResourceCollectionByID
FindServiceIntegration = findServiceIntegration
)
345 changes: 345 additions & 0 deletions internal/service/devopsguru/service_integration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package devopsguru

import (
"context"

"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-provider-aws/internal/create"
"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="Service Integration")
func newResourceServiceIntegration(_ context.Context) (resource.ResourceWithConfigure, error) {
return &resourceServiceIntegration{}, nil
}

const (
ResNameServiceIntegration = "Service Integration"
)

type resourceServiceIntegration struct {
framework.ResourceWithConfigure
}

func (r *resourceServiceIntegration) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "aws_devopsguru_service_integration"
}

func (r *resourceServiceIntegration) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": framework.IDAttribute(),
},
Blocks: map[string]schema.Block{
"kms_server_side_encryption": schema.ListNestedBlock{
CustomType: fwtypes.NewListNestedObjectTypeOf[kmsServerSideEncryptionData](ctx),
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
Validators: []validator.List{
listvalidator.SizeAtMost(1),
listvalidator.IsRequired(),
},
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"kms_key_id": schema.StringAttribute{
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"opt_in_status": schema.StringAttribute{
CustomType: fwtypes.StringEnumType[awstypes.OptInStatus](),
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"type": schema.StringAttribute{
CustomType: fwtypes.StringEnumType[awstypes.ServerSideEncryptionType](),
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
},
},
"logs_anomaly_detection": schema.ListNestedBlock{
CustomType: fwtypes.NewListNestedObjectTypeOf[logsAnomalyDetectionData](ctx),
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
Validators: []validator.List{
listvalidator.SizeAtMost(1),
listvalidator.IsRequired(),
},
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"opt_in_status": schema.StringAttribute{
CustomType: fwtypes.StringEnumType[awstypes.OptInStatus](),
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
},
},
"ops_center": schema.ListNestedBlock{
CustomType: fwtypes.NewListNestedObjectTypeOf[opsCenterData](ctx),
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
Validators: []validator.List{
listvalidator.SizeAtMost(1),
listvalidator.IsRequired(),
},
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"opt_in_status": schema.StringAttribute{
CustomType: fwtypes.StringEnumType[awstypes.OptInStatus](),
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
},
},
},
}
}

func (r *resourceServiceIntegration) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
conn := r.Meta().DevOpsGuruClient(ctx)

var plan resourceServiceIntegrationData
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
plan.ID = types.StringValue(r.Meta().Region)

integration := &awstypes.UpdateServiceIntegrationConfig{}
resp.Diagnostics.Append(flex.Expand(ctx, plan, integration)...)
if resp.Diagnostics.HasError() {
return
}

in := &devopsguru.UpdateServiceIntegrationInput{
ServiceIntegration: integration,
}

_, err := conn.UpdateServiceIntegration(ctx, in)
if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.DevOpsGuru, create.ErrActionCreating, ResNameServiceIntegration, plan.ID.String(), err),
err.Error(),
)
return
}

// Update API returns an empty body. Use find to populate computed fields.
out, err := findServiceIntegration(ctx, conn)
if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.DevOpsGuru, create.ErrActionCreating, ResNameServiceIntegration, plan.ID.String(), err),
err.Error(),
)
return
}

resp.Diagnostics.Append(flex.Flatten(ctx, out, &plan)...)
resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
}

func (r *resourceServiceIntegration) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
conn := r.Meta().DevOpsGuruClient(ctx)

var state resourceServiceIntegrationData
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

out, err := findServiceIntegration(ctx, conn)
if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.DevOpsGuru, create.ErrActionReading, ResNameServiceIntegration, state.ID.String(), err),
err.Error(),
)
return
}

resp.Diagnostics.Append(flex.Flatten(ctx, out, &state)...)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

func (r *resourceServiceIntegration) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
conn := r.Meta().DevOpsGuruClient(ctx)

var plan, state resourceServiceIntegrationData
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

if !plan.KMSServerSideEncryption.Equal(state.KMSServerSideEncryption) ||
!plan.LogsAnomalyDetection.Equal(state.LogsAnomalyDetection) ||
!plan.OpsCenter.Equal(state.OpsCenter) {
integration := &awstypes.UpdateServiceIntegrationConfig{}
resp.Diagnostics.Append(flex.Expand(ctx, plan, integration)...)
if resp.Diagnostics.HasError() {
return
}

in := &devopsguru.UpdateServiceIntegrationInput{
ServiceIntegration: integration,
}

_, err := conn.UpdateServiceIntegration(ctx, in)
if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.DevOpsGuru, create.ErrActionUpdating, ResNameServiceIntegration, plan.ID.String(), err),
err.Error(),
)
return
}

// Update API returns an empty body. Use find to populate computed fields.
out, err := findServiceIntegration(ctx, conn)
if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.DevOpsGuru, create.ErrActionUpdating, ResNameServiceIntegration, plan.ID.String(), err),
err.Error(),
)
return
}

resp.Diagnostics.Append(flex.Flatten(ctx, out, &plan)...)
}

resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

func (r *resourceServiceIntegration) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
// Delete is a no-op to prevent unintentionally disabling account-wide settings.
//
// The registry documentation includes a description of this behavior, indicating
// that if users want to disable any settings previously opt-ed into they should
// do so by applying those changes to an existing configuration before destroying
// this resource.
}

func (r *resourceServiceIntegration) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

func (r *resourceServiceIntegration) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
r.kmsSSEPlanModifier(ctx, req, resp)
r.destroyPlanModifier(ctx, req, resp)
}

// kmsSSEPlanModifier is a resource plan modifier to handle cases where KMS settings
// are changed from a customer managed key to an AWS owned key
func (r *resourceServiceIntegration) kmsSSEPlanModifier(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
if !req.State.Raw.IsNull() && !req.Plan.Raw.IsNull() {
var config, plan resourceServiceIntegrationData
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}

if !config.KMSServerSideEncryption.IsNull() && !plan.KMSServerSideEncryption.IsNull() {
var planKMS []kmsServerSideEncryptionData
var configKMS []kmsServerSideEncryptionData
resp.Diagnostics.Append(plan.KMSServerSideEncryption.ElementsAs(ctx, &planKMS, false)...)
resp.Diagnostics.Append(config.KMSServerSideEncryption.ElementsAs(ctx, &configKMS, false)...)
if resp.Diagnostics.HasError() {
return
}

// To avoid a ValidationException, force a replacement when KMS SSE is changed
// to an AWS owned key and the computed key ID is being copied in the plan.
//
// ValidationException: Cannot specify KMSKeyId for AWS_OWNED_KEY
if planKMS[0].Type.ValueString() == string(awstypes.ServerSideEncryptionTypeAwsOwnedKmsKey) &&
!planKMS[0].KMSKeyID.IsNull() && configKMS[0].KMSKeyID.IsNull() {
resp.RequiresReplace = []path.Path{path.Root("kms_server_side_encryption")}
}
}
}
}

// destroyPlanModifier provides context on how to disable configured settings
func (r *resourceServiceIntegration) destroyPlanModifier(_ context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
// If the entire plan is null, the resource is planned for destruction.
if req.Plan.Raw.IsNull() {
resp.Diagnostics.AddWarning(
"Resource Destruction Considerations",
"To prevent unintentional deletion of account wide settings, applying this resource destruction "+
"will only remove the resource from the Terraform state. To disable any configured settings, "+
"explicitly set the opt-in value to `DISABLED` and apply again before destroying.",
)
}
}

func findServiceIntegration(ctx context.Context, conn *devopsguru.Client) (*awstypes.ServiceIntegrationConfig, error) {
in := &devopsguru.DescribeServiceIntegrationInput{}
out, err := conn.DescribeServiceIntegration(ctx, in)
if err != nil {
return nil, err
}

if out == nil || out.ServiceIntegration == nil {
return nil, tfresource.NewEmptyResultError(in)
}

return out.ServiceIntegration, nil
}

type resourceServiceIntegrationData struct {
ID types.String `tfsdk:"id"`
KMSServerSideEncryption fwtypes.ListNestedObjectValueOf[kmsServerSideEncryptionData] `tfsdk:"kms_server_side_encryption"`
LogsAnomalyDetection fwtypes.ListNestedObjectValueOf[logsAnomalyDetectionData] `tfsdk:"logs_anomaly_detection"`
OpsCenter fwtypes.ListNestedObjectValueOf[opsCenterData] `tfsdk:"ops_center"`
}

type kmsServerSideEncryptionData struct {
KMSKeyID types.String `tfsdk:"kms_key_id"`
OptInStatus fwtypes.StringEnum[awstypes.OptInStatus] `tfsdk:"opt_in_status"`
Type fwtypes.StringEnum[awstypes.ServerSideEncryptionType] `tfsdk:"type"`
}

type logsAnomalyDetectionData struct {
OptInStatus fwtypes.StringEnum[awstypes.OptInStatus] `tfsdk:"opt_in_status"`
}

type opsCenterData struct {
OptInStatus fwtypes.StringEnum[awstypes.OptInStatus] `tfsdk:"opt_in_status"`
}
Loading

0 comments on commit a1a3361

Please sign in to comment.