Skip to content

Commit

Permalink
Merge pull request hashicorp#37637 from iwarapter/f-aws_shield_subscr…
Browse files Browse the repository at this point in the history
…iption

add support for aws shield subscription resource fixes hashicorp#21430
  • Loading branch information
jar-b authored Jul 24, 2024
2 parents 26d9851 + e5016cf commit fe0ab1d
Show file tree
Hide file tree
Showing 5 changed files with 363 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .changelog/37637.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
```release-note:new-resource
aws_shield_subscription
```
```release-note:note
resource/aws_shield_subscription: Because we cannot easily test this functionality, it is best effort and we ask for community help in testing
```
4 changes: 4 additions & 0 deletions internal/service/shield/service_package_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

206 changes: 206 additions & 0 deletions internal/service/shield/subscription.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package shield

import (
"context"
"errors"

"github.com/aws/aws-sdk-go-v2/service/shield"
awstypes "github.com/aws/aws-sdk-go-v2/service/shield/types"
"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/stringdefault"
"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("aws_shield_subscription", name="Subscription")
func newResourceSubscription(_ context.Context) (resource.ResourceWithConfigure, error) {
return &resourceSubscription{}, nil
}

const (
ResNameSubscription = "Subscription"
)

type resourceSubscription struct {
framework.ResourceWithConfigure
}

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

func (r *resourceSubscription) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"auto_renew": schema.StringAttribute{
Description: "Whether to automatically renew the subscription when it expires.",
Optional: true,
Computed: true,
CustomType: fwtypes.StringEnumType[awstypes.AutoRenew](),
Default: stringdefault.StaticString(string(awstypes.AutoRenewEnabled)),
},
names.AttrID: framework.IDAttribute(),
names.AttrSkipDestroy: schema.BoolAttribute{
Optional: true,
},
},
}
}

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

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

if plan.AutoRenew.Equal(types.StringValue(string(awstypes.AutoRenewDisabled))) {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.Shield, create.ErrActionCreating, ResNameSubscription, plan.ID.String(), nil),
errors.New("subscription auto_renew flag cannot be changed earlier than 30 days before subscription end and later than 1 day before subscription end").Error(),
)
return
}

in := &shield.CreateSubscriptionInput{}
_, err := conn.CreateSubscription(ctx, in)
if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.Shield, create.ErrActionCreating, ResNameSubscription, plan.ID.String(), err),
err.Error(),
)
return
}

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

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

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

out, err := findSubscriptionByID(ctx, conn)
if errs.IsA[*retry.NotFoundError](err) {
resp.State.RemoveResource(ctx)
return
}
if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.Shield, create.ErrActionReading, ResNameSubscription, 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 *resourceSubscription) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
conn := r.Meta().ShieldClient(ctx)

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

if !plan.AutoRenew.Equal(state.AutoRenew) {
in := &shield.UpdateSubscriptionInput{
AutoRenew: awstypes.AutoRenew(plan.AutoRenew.ValueString()),
}

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

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

func (r *resourceSubscription) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
conn := r.Meta().ShieldClient(ctx)

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

if state.SkipDestroy.ValueBool() {
return
}

in := &shield.UpdateSubscriptionInput{
AutoRenew: awstypes.AutoRenewDisabled,
}

_, err := conn.UpdateSubscription(ctx, in)
if err != nil {
if errs.IsA[*awstypes.ResourceNotFoundException](err) {
return
}
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.Shield, create.ErrActionDeleting, ResNameSubscription, state.ID.String(), err),
err.Error(),
)
return
}
}

func (r *resourceSubscription) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root(names.AttrID), req, resp)
}

func findSubscriptionByID(ctx context.Context, conn *shield.Client) (*awstypes.Subscription, error) {
in := &shield.DescribeSubscriptionInput{}

out, err := conn.DescribeSubscription(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.Subscription == nil {
return nil, tfresource.NewEmptyResultError(in)
}

return out.Subscription, nil
}

type resourceSubscriptionData struct {
AutoRenew fwtypes.StringEnum[awstypes.AutoRenew] `tfsdk:"auto_renew"`
ID types.String `tfsdk:"id"`
SkipDestroy types.Bool `tfsdk:"skip_destroy"`
}
92 changes: 92 additions & 0 deletions internal/service/shield/subscription_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package shield_test

import (
"context"
"errors"
"fmt"
"testing"

"github.com/aws/aws-sdk-go-v2/service/shield"
awstypes "github.com/aws/aws-sdk-go-v2/service/shield/types"
"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"
tfshield "github.com/hashicorp/terraform-provider-aws/internal/service/shield"
"github.com/hashicorp/terraform-provider-aws/names"
)

func TestAccShieldSubscription_basic(t *testing.T) {
ctx := acctest.Context(t) //nolint:staticcheck // will be used when hardcoded skip is commented
if testing.Short() {
t.Skip("skipping long-running test in short mode")
}

// Due to the high cost of this subscription, we hardcode this test to
// skip rather than gating behind an environment variable.
// Run this test be removing the line below.
t.Skipf("running this test requires a yearly commitment to AWS Shield Advanced with a $3000 monthly fee in the associated account")

var subscription shield.DescribeSubscriptionOutput
resourceName := "aws_shield_subscription.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
acctest.PreCheck(ctx, t)
acctest.PreCheckPartitionHasService(t, names.ShieldEndpointID)
},
ErrorCheck: acctest.ErrorCheck(t, names.ShieldServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccSubscriptionConfig_basic(string(awstypes.AutoRenewEnabled)),
Check: resource.ComposeTestCheckFunc(
testAccCheckSubscriptionExists(ctx, resourceName, &subscription),
resource.TestCheckResourceAttr(resourceName, "auto_renew", string(awstypes.AutoRenewEnabled)),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func testAccCheckSubscriptionExists(ctx context.Context, name string, subscription *shield.DescribeSubscriptionOutput) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return create.Error(names.Shield, create.ErrActionCheckingExistence, tfshield.ResNameSubscription, name, errors.New("not found"))
}

if rs.Primary.ID == "" {
return create.Error(names.Shield, create.ErrActionCheckingExistence, tfshield.ResNameSubscription, name, errors.New("not set"))
}

conn := acctest.Provider.Meta().(*conns.AWSClient).ShieldClient(ctx)
resp, err := conn.DescribeSubscription(ctx, &shield.DescribeSubscriptionInput{})

if err != nil {
return create.Error(names.Shield, create.ErrActionCheckingExistence, tfshield.ResNameSubscription, rs.Primary.ID, err)
}

*subscription = *resp

return nil
}
}

func testAccSubscriptionConfig_basic(autoRenew string) string {
return fmt.Sprintf(`
resource "aws_shield_subscription" "test" {
auto_renew = %[1]q
skip_destroy = true
}
`, autoRenew)
}
55 changes: 55 additions & 0 deletions website/docs/r/shield_subscription.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
subcategory: "Shield"
layout: "aws"
page_title: "AWS: aws_shield_subscription"
description: |-
Terraform resource for managing an AWS Shield Subscription.
---

# Resource: aws_shield_subscription

Terraform resource for managing an AWS Shield Subscription.

~> This resource creates a subscription to AWS Shield Advanced, which requires a 1 year subscription commitment with a monthly fee. Refer to the [AWS Shield Pricing](https://aws.amazon.com/shield/pricing/) page for more details.

~> Destruction of this resource will set `auto_renew` to `DISABLED`. Automatic renewal can only be disabled during the last 30 days of a subscription. To unsubscribe outside of this window, you must contact AWS Support. Set `skip_destroy` to `true` to skip modifying the `auto_renew` argument during destruction.

## Example Usage

### Basic Usage

```terraform
resource "aws_shield_subscription" "example" {
auto_renew = "ENABLED"
}
```

## Argument Reference

The following arguments are optional:

* `auto_renew` - (Optional) Toggle for automated renewal of the subscription. Valid values are `ENABLED` or `DISABLED`. Default is `ENABLED`.
* `skip_destroy` - (Optional) Skip attempting to disable automated renewal upon destruction. If set to `true`, the `auto_renew` value will be left as-is and the resource will simply be removed from state.

## Attribute Reference

This resource exports the following attributes in addition to the arguments above:

* `id` - AWS Account ID.

## Import

In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Shield Subscription using the `id`. For example:

```terraform
import {
to = aws_shield_subscription.example
id = "012345678901"
}
```

Using `terraform import`, import Shield Subscription using the `id`. For example:

```console
% terraform import aws_shield_subscription.example 012345678901
```

0 comments on commit fe0ab1d

Please sign in to comment.