Skip to content

Commit

Permalink
Merge pull request #27361 from hashicorp/f-aws_sesv2_dedicated_ip
Browse files Browse the repository at this point in the history
New Resource `aws_sesv2_dedicated_ip_assignment`
  • Loading branch information
jar-b authored Oct 24, 2022
2 parents c3324e9 + c7729d6 commit 447ab4a
Show file tree
Hide file tree
Showing 8 changed files with 394 additions and 9 deletions.
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,
},
"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") == "" {
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

0 comments on commit 447ab4a

Please sign in to comment.