Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Resource aws_sesv2_dedicated_ip_assignment #27361

Merged
merged 6 commits into from
Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/27361.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
aws_sesv2_dedicated_ip_assignment
```
1 change: 1 addition & 0 deletions docs/acc-test-environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Environment variables (beyond standard AWS Go SDK ones) used by acceptance testi
| `SERVICEQUOTAS_INCREASE_ON_CREATE_SERVICE_CODE` | Service Code for Service Quotas testing (submits support case). |
| `SERVICEQUOTAS_INCREASE_ON_CREATE_VALUE` | Value of quota increase for Service Quotas testing (submits support case). |
| `SES_DOMAIN_IDENTITY_ROOT_DOMAIN` | Root domain name of publicly accessible and Route 53 configurable domain for SES Domain Identity testing. |
| `SES_DEDICATED_IP` | Dedicated IP address for testing IP assignment with a "Standard" (non-managed) SES dedicated IP pool. |
| `SWF_DOMAIN_TESTING_ENABLED` | Enables SWF Domain testing (API does not support deletions). |
| `TEST_AWS_ORGANIZATION_ACCOUNT_EMAIL_DOMAIN` | Email address for Organizations Account testing. |
| `TEST_AWS_SES_VERIFIED_EMAIL_ARN` | Verified SES Email Identity for use in Cognito User Pool testing. |
Expand Down
7 changes: 4 additions & 3 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2055,9 +2055,10 @@ func New(_ context.Context) (*schema.Provider, error) {
"aws_ses_receipt_rule_set": ses.ResourceReceiptRuleSet(),
"aws_ses_template": ses.ResourceTemplate(),

"aws_sesv2_configuration_set": sesv2.ResourceConfigurationSet(),
"aws_sesv2_dedicated_ip_pool": sesv2.ResourceDedicatedIPPool(),
"aws_sesv2_email_identity": sesv2.ResourceEmailIdentity(),
"aws_sesv2_configuration_set": sesv2.ResourceConfigurationSet(),
"aws_sesv2_dedicated_ip_assignment": sesv2.ResourceDedicatedIPAssignment(),
"aws_sesv2_dedicated_ip_pool": sesv2.ResourceDedicatedIPPool(),
"aws_sesv2_email_identity": sesv2.ResourceEmailIdentity(),

"aws_sfn_activity": sfn.ResourceActivity(),
"aws_sfn_state_machine": sfn.ResourceStateMachine(),
Expand Down
174 changes: 174 additions & 0 deletions internal/service/sesv2/dedicated_ip_assignment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package sesv2

import (
"context"
"errors"
"fmt"
"log"
"strings"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sesv2"
"github.com/aws/aws-sdk-go-v2/service/sesv2/types"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"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"
)

func ResourceDedicatedIPAssignment() *schema.Resource {
return &schema.Resource{
CreateWithoutTimeout: resourceDedicatedIPAssignmentCreate,
ReadWithoutTimeout: resourceDedicatedIPAssignmentRead,
DeleteWithoutTimeout: resourceDedicatedIPAssignmentDelete,

Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(30 * time.Minute),
Delete: schema.DefaultTimeout(30 * time.Minute),
},

Schema: map[string]*schema.Schema{
"destination_pool_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Copy link
Member Author

@jar-b jar-b Oct 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opted for ForceNew here, even though technically the IP could be moved via an update. This felt cleaner for the case where a config like the following has a pool_name change:

resource "aws_sesv2_dedicated_ip_pool" "test" {
  pool_name = "my-pool" # ex. change to my-pool2
}

resource "aws_sesv2_dedicated_ip_assignment" "test" {
  ip                    = "0.0.0.0"
  destination_pool_name = aws_sesv2_dedicated_ip_pool.test.pool_name
}

Without ForceNew:

  • apply attempts to delete aws_sesv2_dedicated_ip_pool.test first, but fails because the pool isn't empty. This is resolved by a two-step apply. First, removing the assignment resource and applying to destroy it. Then the pool name can be updated and the assignment safely added back.
    • Adding logic to the dedicated IP pool destroy function to empty it first can't be done safely because it will cause a state change for assignment resources mid-apply.

With ForceNew:

  • Both resources are destroyed and re-created with no side effects.

},
"ip": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.IsIPAddress,
},
},
}
}

const (
ResNameDedicatedIPAssignment = "Dedicated IP Assignment"
)

func resourceDedicatedIPAssignmentCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).SESV2Conn

in := &sesv2.PutDedicatedIpInPoolInput{
Ip: aws.String(d.Get("ip").(string)),
DestinationPoolName: aws.String(d.Get("destination_pool_name").(string)),
}

_, err := conn.PutDedicatedIpInPool(ctx, in)
if err != nil {
return create.DiagError(names.SESV2, create.ErrActionCreating, ResNameDedicatedIPAssignment, d.Get("ip").(string), err)
}

id := toID(d.Get("ip").(string), d.Get("destination_pool_name").(string))
d.SetId(id)

return resourceDedicatedIPAssignmentRead(ctx, d, meta)
}

func resourceDedicatedIPAssignmentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).SESV2Conn

out, err := FindDedicatedIPAssignmentByID(ctx, conn, d.Id())
if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] SESV2 DedicatedIPAssignment (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}
if err != nil {
return create.DiagError(names.SESV2, create.ErrActionReading, ResNameDedicatedIPAssignment, d.Id(), err)
}

d.Set("ip", aws.ToString(out.Ip))
d.Set("destination_pool_name", aws.ToString(out.PoolName))

return nil
}

func resourceDedicatedIPAssignmentDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).SESV2Conn
ip, _ := splitID(d.Id())

log.Printf("[INFO] Deleting SESV2 DedicatedIPAssignment %s", d.Id())
_, err := conn.PutDedicatedIpInPool(ctx, &sesv2.PutDedicatedIpInPoolInput{
Ip: aws.String(ip),
DestinationPoolName: aws.String(defaultDedicatedPoolName),
})

if err != nil {
var nfe *types.NotFoundException
if errors.As(err, &nfe) {
return nil
}

return create.DiagError(names.SESV2, create.ErrActionDeleting, ResNameDedicatedIPAssignment, d.Id(), err)
}

return nil
}

const (
// defaultDedicatedPoolName contains the name of the standard pool managed by AWS
// where dedicated IP addresses with an assignment are stored
//
// When an assignment resource is removed from state, the delete function will re-assign
// the relevant IP to this pool.
defaultDedicatedPoolName = "ses-default-dedicated-pool"
)

// ErrIncorrectPoolAssignment is returned when an IP is assigned to a pool different
// from what is specified in state
var ErrIncorrectPoolAssignment = errors.New("incorrect pool assignment")

func FindDedicatedIPAssignmentByID(ctx context.Context, conn *sesv2.Client, id string) (*types.DedicatedIp, error) {
ip, destinationPoolName := splitID(id)

in := &sesv2.GetDedicatedIpInput{
Ip: aws.String(ip),
}
out, err := conn.GetDedicatedIp(ctx, in)
if err != nil {
var nfe *types.NotFoundException
if errors.As(err, &nfe) {
return nil, &resource.NotFoundError{
LastError: err,
LastRequest: in,
}
}

return nil, err
}

if out == nil || out.DedicatedIp == nil {
return nil, tfresource.NewEmptyResultError(in)
}
if out.DedicatedIp.PoolName == nil || aws.ToString(out.DedicatedIp.PoolName) != destinationPoolName {
return nil, &resource.NotFoundError{
LastError: ErrIncorrectPoolAssignment,
LastRequest: in,
}
}

return out.DedicatedIp, nil
}

func toID(ip, destinationPoolName string) string {
return fmt.Sprintf("%s,%s", ip, destinationPoolName)
}

func splitID(id string) (string, string) {
items := strings.Split(id, ",")
if len(items) == 2 {
return items[0], items[1]
}
return "", ""
}
154 changes: 154 additions & 0 deletions internal/service/sesv2/dedicated_ip_assignment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package sesv2_test

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

"github.com/aws/aws-sdk-go-v2/service/sesv2/types"
sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/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"
tfsesv2 "github.com/hashicorp/terraform-provider-aws/internal/service/sesv2"
"github.com/hashicorp/terraform-provider-aws/names"
)

func TestAccSESV2DedicatedIPAssignment_serial(t *testing.T) {
testCases := map[string]func(t *testing.T){
"basic": testAccSESV2DedicatedIPAssignment_basic,
"disappears": testAccSESV2DedicatedIPAssignment_disappears,
}

for name, tc := range testCases {
tc := tc
t.Run(name, func(t *testing.T) {
tc(t)
})
}
}

func testAccSESV2DedicatedIPAssignment_basic(t *testing.T) { // nosemgrep:ci.sesv2-in-func-name
if os.Getenv("SES_DEDICATED_IP") == "" {
jar-b marked this conversation as resolved.
Show resolved Hide resolved
t.Skip("Environment variable SES_DEDICATED_IP is not set")
}

ip := os.Getenv("SES_DEDICATED_IP")
poolName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_sesv2_dedicated_ip_assignment.test"

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ErrorCheck: acctest.ErrorCheck(t, names.SESV2EndpointID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckDedicatedIPAssignmentDestroy,
Steps: []resource.TestStep{
{
Config: testAccDedicatedIPAssignmentConfig_basic(ip, poolName),
Check: resource.ComposeTestCheckFunc(
testAccCheckDedicatedIPAssignmentExists(resourceName),
resource.TestCheckResourceAttr(resourceName, "ip", ip),
resource.TestCheckResourceAttr(resourceName, "destination_pool_name", poolName),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func testAccSESV2DedicatedIPAssignment_disappears(t *testing.T) { // nosemgrep:ci.sesv2-in-func-name
if os.Getenv("SES_DEDICATED_IP") == "" {
t.Skip("Environment variable SES_DEDICATED_IP is not set")
}

ip := os.Getenv("SES_DEDICATED_IP")
poolName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_sesv2_dedicated_ip_assignment.test"

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ErrorCheck: acctest.ErrorCheck(t, names.SESV2EndpointID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckDedicatedIPAssignmentDestroy,
Steps: []resource.TestStep{
{
Config: testAccDedicatedIPAssignmentConfig_basic(ip, poolName),
Check: resource.ComposeTestCheckFunc(
testAccCheckDedicatedIPAssignmentExists(resourceName),
acctest.CheckResourceDisappears(acctest.Provider, tfsesv2.ResourceDedicatedIPAssignment(), resourceName),
),
ExpectNonEmptyPlan: true,
},
},
})
}

func testAccCheckDedicatedIPAssignmentDestroy(s *terraform.State) error {
conn := acctest.Provider.Meta().(*conns.AWSClient).SESV2Conn
ctx := context.Background()

for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_sesv2_dedicated_ip_assignment" {
continue
}

_, err := tfsesv2.FindDedicatedIPAssignmentByID(ctx, conn, rs.Primary.ID)
if err != nil {
var nfe *types.NotFoundException
if errors.As(err, &nfe) {
return nil
}
if errors.Is(err, tfsesv2.ErrIncorrectPoolAssignment) {
return nil
}
return err
}

return create.Error(names.SESV2, create.ErrActionCheckingDestroyed, tfsesv2.ResNameDedicatedIPAssignment, rs.Primary.ID, errors.New("not destroyed"))
}

return nil
}

func testAccCheckDedicatedIPAssignmentExists(name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return create.Error(names.SESV2, create.ErrActionCheckingExistence, tfsesv2.ResNameDedicatedIPAssignment, name, errors.New("not found"))
}

if rs.Primary.ID == "" {
return create.Error(names.SESV2, create.ErrActionCheckingExistence, tfsesv2.ResNameDedicatedIPAssignment, name, errors.New("not set"))
}

conn := acctest.Provider.Meta().(*conns.AWSClient).SESV2Conn
ctx := context.Background()
_, err := tfsesv2.FindDedicatedIPAssignmentByID(ctx, conn, rs.Primary.ID)
if err != nil {
return create.Error(names.SESV2, create.ErrActionCheckingExistence, tfsesv2.ResNameDedicatedIPAssignment, rs.Primary.ID, err)
}

return nil
}
}

func testAccDedicatedIPAssignmentConfig_basic(ip, poolName string) string {
return fmt.Sprintf(`
resource "aws_sesv2_dedicated_ip_pool" "test" {
pool_name = %[2]q
}

resource "aws_sesv2_dedicated_ip_assignment" "test" {
ip = %[1]q
destination_pool_name = aws_sesv2_dedicated_ip_pool.test.pool_name
}
`, ip, poolName)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestAccSESV2DedicatedIPPoolDataSource_basic(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
acctest.PreCheck(t)
testAccPreCheck(t)
testAccPreCheckDedicatedIPPool(t)
},
ErrorCheck: acctest.ErrorCheck(t, names.SESV2EndpointID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Expand Down
Loading