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_s3outposts_endpoint #15585

Merged
merged 2 commits into from
Oct 27, 2020
Merged
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
29 changes: 29 additions & 0 deletions aws/internal/service/s3outposts/finder/finder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package finder

import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/s3outposts"
)

// Endpoint returns matching Endpoint by ARN.
func Endpoint(conn *s3outposts.S3Outposts, endpointArn string) (*s3outposts.Endpoint, error) {
input := &s3outposts.ListEndpointsInput{}
var result *s3outposts.Endpoint

err := conn.ListEndpointsPages(input, func(page *s3outposts.ListEndpointsOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}

for _, endpoint := range page.Endpoints {
if aws.StringValue(endpoint.EndpointArn) == endpointArn {
result = endpoint
return false
}
}

return !lastPage
})

return result, err
}
30 changes: 30 additions & 0 deletions aws/internal/service/s3outposts/waiter/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package waiter

import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/s3outposts"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/s3outposts/finder"
)

const (
EndpointStatusNotFound = "NotFound"
EndpointStatusUnknown = "Unknown"
)

// EndpointStatus fetches the Endpoint and its Status
func EndpointStatus(conn *s3outposts.S3Outposts, endpointArn string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
endpoint, err := finder.Endpoint(conn, endpointArn)

if err != nil {
return nil, EndpointStatusUnknown, err
}

if endpoint == nil {
return nil, EndpointStatusNotFound, nil
}

return endpoint, aws.StringValue(endpoint.Status), nil
}
}
37 changes: 37 additions & 0 deletions aws/internal/service/s3outposts/waiter/waiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package waiter

import (
"time"

"github.com/aws/aws-sdk-go/service/s3outposts"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

const (
// API model constant is incorrectly AVAILABLE
EndpointStatusAvailable = "Available"

// API model constant is incorrectly PENDING
EndpointStatusPending = "Pending"

// Maximum amount of time to wait for Endpoint to return Available on creation
EndpointStatusCreatedTimeout = 5 * time.Minute
)

// EndpointStatusCreated waits for Endpoint to return Available
func EndpointStatusCreated(conn *s3outposts.S3Outposts, endpointArn string) (*s3outposts.Endpoint, error) {
stateConf := &resource.StateChangeConf{
Pending: []string{EndpointStatusPending, EndpointStatusNotFound},
Target: []string{EndpointStatusAvailable},
Refresh: EndpointStatus(conn, endpointArn),
Timeout: EndpointStatusCreatedTimeout,
}

outputRaw, err := stateConf.WaitForState()

if v, ok := outputRaw.(*s3outposts.Endpoint); ok {
return v, err
}

return nil, err
}
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
@@ -864,6 +864,7 @@ func Provider() *schema.Provider {
"aws_s3_bucket_inventory": resourceAwsS3BucketInventory(),
"aws_s3control_bucket": resourceAwsS3ControlBucket(),
"aws_s3control_bucket_policy": resourceAwsS3ControlBucketPolicy(),
"aws_s3outposts_endpoint": resourceAwsS3OutpostsEndpoint(),
"aws_security_group": resourceAwsSecurityGroup(),
"aws_network_interface_sg_attachment": resourceAwsNetworkInterfaceSGAttachment(),
"aws_default_security_group": resourceAwsDefaultSecurityGroup(),
212 changes: 212 additions & 0 deletions aws/resource_aws_s3outposts_endpoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package aws

import (
"fmt"
"log"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/service/s3outposts"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/s3outposts/finder"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/s3outposts/waiter"
)

func resourceAwsS3OutpostsEndpoint() *schema.Resource {
return &schema.Resource{
Create: resourceAwsS3OutpostsEndpointCreate,
Read: resourceAwsS3OutpostsEndpointRead,
Delete: resourceAwsS3OutpostsEndpointDelete,

Importer: &schema.ResourceImporter{
State: resourceAwsS3OutpostsEndpointImportState,
},

Schema: map[string]*schema.Schema{
"arn": {
Type: schema.TypeString,
Computed: true,
},
"cidr_block": {
Type: schema.TypeString,
Computed: true,
},
"creation_time": {
Type: schema.TypeString,
Computed: true,
},
"network_interfaces": {
Type: schema.TypeSet,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"network_interface_id": {
Type: schema.TypeString,
Computed: true,
},
},
},
},
"outpost_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringIsNotEmpty,
},
"security_group_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringIsNotEmpty,
},
"subnet_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringIsNotEmpty,
},
},
}
}

func resourceAwsS3OutpostsEndpointCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).s3outpostsconn

input := &s3outposts.CreateEndpointInput{
OutpostId: aws.String(d.Get("outpost_id").(string)),
SecurityGroupId: aws.String(d.Get("security_group_id").(string)),
SubnetId: aws.String(d.Get("subnet_id").(string)),
}

output, err := conn.CreateEndpoint(input)

if err != nil {
return fmt.Errorf("error creating S3 Outposts Endpoint: %w", err)
}

if output == nil {
return fmt.Errorf("error creating S3 Outposts Endpoint: empty response")
}

d.SetId(aws.StringValue(output.EndpointArn))

if _, err := waiter.EndpointStatusCreated(conn, d.Id()); err != nil {
return fmt.Errorf("error waiting for S3 Outposts Endpoint (%s) to become available: %w", d.Id(), err)
}

return resourceAwsS3OutpostsEndpointRead(d, meta)
}

func resourceAwsS3OutpostsEndpointRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).s3outpostsconn

endpoint, err := finder.Endpoint(conn, d.Id())

if err != nil {
return fmt.Errorf("error reading S3 Outposts Endpoint (%s): %w", d.Id(), err)
}

if endpoint == nil {
if d.IsNewResource() {
return fmt.Errorf("error reading S3 Outposts Endpoint (%s): not found after creation", d.Id())
}

log.Printf("[WARN] S3 Outposts Endpoint (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}

d.Set("arn", endpoint.EndpointArn)
d.Set("cidr_block", endpoint.CidrBlock)

if endpoint.CreationTime != nil {
d.Set("creation_time", aws.TimeValue(endpoint.CreationTime).Format(time.RFC3339))
}

if err := d.Set("network_interfaces", flattenS3outpostsNetworkInterfaces(endpoint.NetworkInterfaces)); err != nil {
return fmt.Errorf("error setting network_interfaces: %w", err)
}

d.Set("outpost_id", endpoint.OutpostsId)
Copy link
Contributor

Choose a reason for hiding this comment

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

Outposts vs Outpost 🤷‍♀️

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Create and Delete functions refer to it in the singular and recent ELBv2 support also refers to it in the singular: https://github.com/terraform-providers/terraform-provider-aws/pull/15170/files 🙁


return nil
}

func resourceAwsS3OutpostsEndpointDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).s3outpostsconn

parsedArn, err := arn.Parse(d.Id())

if err != nil {
return fmt.Errorf("error parsing S3 Outposts Endpoint ARN (%s): %w", d.Id(), err)
}

// ARN resource format: outpost/<outpost-id>/endpoint/<endpoint-id>
arnResourceParts := strings.Split(parsedArn.Resource, "/")

if parsedArn.AccountID == "" || len(arnResourceParts) != 4 {
return fmt.Errorf("error parsing S3 Outposts Endpoint ARN (%s): unknown format", d.Id())
}

input := &s3outposts.DeleteEndpointInput{
EndpointId: aws.String(arnResourceParts[3]),
OutpostId: aws.String(arnResourceParts[1]),
}

_, err = conn.DeleteEndpoint(input)

if err != nil {
return fmt.Errorf("error deleting S3 Outposts Endpoint (%s): %w", d.Id(), err)
}

return nil
}

func resourceAwsS3OutpostsEndpointImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
idParts := strings.Split(d.Id(), ",")

if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
return nil, fmt.Errorf("unexpected format of ID (%s), expected ENDPOINT-ARN,SECURITY-GROUP-ID,SUBNET-ID", d.Id())
}

endpointArn := idParts[0]
securityGroupId := idParts[1]
subnetId := idParts[2]

d.SetId(endpointArn)
d.Set("security_group_id", securityGroupId)
d.Set("subnet_id", subnetId)

return []*schema.ResourceData{d}, nil
}

func flattenS3outpostsNetworkInterfaces(apiObjects []*s3outposts.NetworkInterface) []interface{} {
var tfList []interface{}

for _, apiObject := range apiObjects {
if apiObject == nil {
continue
}

tfList = append(tfList, flattenS3outpostsNetworkInterface(apiObject))
}

return tfList
}

func flattenS3outpostsNetworkInterface(apiObject *s3outposts.NetworkInterface) map[string]interface{} {
if apiObject == nil {
return nil
}

tfMap := map[string]interface{}{}

if v := apiObject.NetworkInterfaceId; v != nil {
tfMap["network_interface_id"] = aws.StringValue(v)
}

return tfMap
}
156 changes: 156 additions & 0 deletions aws/resource_aws_s3outposts_endpoint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package aws

import (
"fmt"
"regexp"
"testing"

"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/terraform-providers/terraform-provider-aws/aws/internal/service/s3outposts/finder"
)

func TestAccAWSS3OutpostsEndpoint_basic(t *testing.T) {
resourceName := "aws_s3outposts_endpoint.test"
rInt := acctest.RandIntRange(0, 255)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSS3OutpostsEndpointDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSS3OutpostsEndpointConfig(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSS3OutpostsEndpointExists(resourceName),
testAccMatchResourceAttrRegionalARN(resourceName, "arn", "s3-outposts", regexp.MustCompile(`outpost/[^/]+/endpoint/[a-z0-9]+`)),
resource.TestCheckResourceAttrSet(resourceName, "creation_time"),
resource.TestCheckResourceAttrPair(resourceName, "cidr_block", "aws_vpc.test", "cidr_block"),
resource.TestCheckResourceAttr(resourceName, "network_interfaces.#", "4"),
resource.TestCheckResourceAttrPair(resourceName, "outpost_id", "data.aws_outposts_outpost.test", "id"),
resource.TestCheckResourceAttrPair(resourceName, "security_group_id", "aws_security_group.test", "id"),
resource.TestCheckResourceAttrPair(resourceName, "subnet_id", "aws_subnet.test", "id"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateIdFunc: testAccAWSS3OutpostsEndpointImportStateIdFunc(resourceName),
ImportStateVerify: true,
},
},
})
}

func TestAccAWSS3OutpostsEndpoint_disappears(t *testing.T) {
resourceName := "aws_s3outposts_endpoint.test"
rInt := acctest.RandIntRange(0, 255)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSS3OutpostsEndpointDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSS3OutpostsEndpointConfig(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSS3OutpostsEndpointExists(resourceName),
testAccCheckResourceDisappears(testAccProvider, resourceAwsS3OutpostsEndpoint(), resourceName),
),
ExpectNonEmptyPlan: true,
},
},
})
}

func testAccCheckAWSS3OutpostsEndpointDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).s3outpostsconn

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

endpoint, err := finder.Endpoint(conn, rs.Primary.ID)

if err != nil {
return err
}

if endpoint != nil {
return fmt.Errorf("S3 Outposts Endpoint (%s) still exists", rs.Primary.ID)
}
}

return nil
}

func testAccCheckAWSS3OutpostsEndpointExists(resourceName string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[resourceName]
if !ok {
return fmt.Errorf("not found: %s", resourceName)
}

if rs.Primary.ID == "" {
return fmt.Errorf("no resource ID is set")
}

conn := testAccProvider.Meta().(*AWSClient).s3outpostsconn

endpoint, err := finder.Endpoint(conn, rs.Primary.ID)

if err != nil {
return err
}

if endpoint == nil {
return fmt.Errorf("S3 Outposts Endpoint (%s) not found", rs.Primary.ID)
}

return nil
}
}

func testAccAWSS3OutpostsEndpointImportStateIdFunc(resourceName string) resource.ImportStateIdFunc {
return func(s *terraform.State) (string, error) {
rs, ok := s.RootModule().Resources[resourceName]
if !ok {
return "", fmt.Errorf("Not found: %s", resourceName)
}

return fmt.Sprintf("%s,%s,%s", rs.Primary.ID, rs.Primary.Attributes["security_group_id"], rs.Primary.Attributes["subnet_id"]), nil
}
}

func testAccAWSS3OutpostsEndpointConfig(rInt int) string {
return fmt.Sprintf(`
data "aws_outposts_outposts" "test" {}
data "aws_outposts_outpost" "test" {
id = tolist(data.aws_outposts_outposts.test.ids)[0]
}
resource "aws_vpc" "test" {
cidr_block = "10.%[1]d.0.0/16"
}
resource "aws_security_group" "test" {
vpc_id = aws_vpc.test.id
}
resource "aws_subnet" "test" {
availability_zone = data.aws_outposts_outpost.test.availability_zone
cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, 0)
outpost_arn = data.aws_outposts_outpost.test.arn
vpc_id = aws_vpc.test.id
}
resource "aws_s3outposts_endpoint" "test" {
outpost_id = data.aws_outposts_outpost.test.id
security_group_id = aws_security_group.test.id
subnet_id = aws_subnet.test.id
}
`, rInt)
}
48 changes: 48 additions & 0 deletions website/docs/r/s3outposts_endpoint.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
subcategory: "S3 Outposts"
layout: "aws"
page_title: "AWS: aws_s3outposts_endpoint"
description: |-
Manages an S3 Outposts Endpoint.
---

# Resource: aws_s3outposts_endpoint

Provides a resource to manage an S3 Outposts Endpoint.

## Example Usage

```hcl
resource "aws_s3outposts_endpoint" "example" {
outpost_id = data.aws_outposts_outpost.example.id
security_group_id = aws_security_group.example.id
subnet_id = aws_subnet.example.id
}
```

## Argument Reference

The following arguments are required:

* `outpost_id` - (Required) Identifier of the Outpost to contain this endpoint.
* `security_group_id` - (Required) Identifier of the EC2 Security Group.
* `subnet_id` - (Required) Identifier of the EC2 Subnet.

## Attributes Reference

In addition to all arguments above, the following attributes are exported:

* `arn` - Amazon Resource Name (ARN) of the endpoint.
* `cidr_block` - VPC CIDR block of the endpoint.
* `creation_time` - UTC creation time in [RFC3339 format](https://tools.ietf.org/html/rfc3339#section-5.8).
* `id` - Amazon Resource Name (ARN) of the endpoint.
* `network_interfaces` - Set of nested attributes for associated Elastic Network Interfaces (ENIs).
* `network_interface_id` - Identifier of the Elastic Network Interface (ENI).

## Import

S3 Outposts Endpoints can be imported using Amazon Resource Name (ARN), EC2 Security Group identifier, and EC2 Subnet identifier, separated by commas (`,`) e.g.

```
$ terraform import aws_s3outposts_endpoint.example arn:aws:s3-outposts:us-east-1:123456789012:outpost/op-12345678/endpoint/0123456789abcdef,sg-12345678,subnet-12345678
```