Skip to content

Commit

Permalink
resource/aws_datasync_agent: Add support for VPC Endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
gazoakley authored and ewbankkit committed May 13, 2021
1 parent ad1103e commit 760488c
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 27 deletions.
92 changes: 75 additions & 17 deletions aws/resource_aws_datasync_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

func resourceAwsDataSyncAgent() *schema.Resource {
Expand All @@ -37,7 +38,7 @@ func resourceAwsDataSyncAgent() *schema.Resource {
Optional: true,
Computed: true,
ForceNew: true,
ConflictsWith: []string{"ip_address"},
ConflictsWith: []string{"ip_address", "private_link_endpoint"},
},
"ip_address": {
Type: schema.TypeString,
Expand All @@ -46,12 +47,35 @@ func resourceAwsDataSyncAgent() *schema.Resource {
ForceNew: true,
ConflictsWith: []string{"activation_key"},
},
"private_link_endpoint": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"activation_key"},
},
"name": {
Type: schema.TypeString,
Optional: true,
},
"security_group_arns": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"subnet_arns": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"tags": tagsSchema(),
"tags_all": tagsSchemaComputed(),
"vpc_endpoint_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
},

CustomizeDiff: SetTagsDiff,
Expand Down Expand Up @@ -81,6 +105,10 @@ func resourceAwsDataSyncAgentCreate(d *schema.ResourceData, meta interface{}) er
}

requestURL := fmt.Sprintf("http://%s/?gatewayType=SYNC&activationRegion=%s", agentIpAddress, region)
if v, ok := d.GetOk("private_link_endpoint"); ok {
requestURL = fmt.Sprintf("http://%s/?gatewayType=SYNC&activationRegion=%s&endpointType=PRIVATE_LINK&privateLinkEndpoint=%s", agentIpAddress, region, v.(string))
}

log.Printf("[DEBUG] Creating HTTP request: %s", requestURL)
request, err := http.NewRequest("GET", requestURL, nil)
if err != nil {
Expand All @@ -99,30 +127,40 @@ func resourceAwsDataSyncAgentCreate(d *schema.ResourceData, meta interface{}) er
}
return resource.NonRetryableError(fmt.Errorf("error making HTTP request: %s", err))
}

if response == nil {
return resource.NonRetryableError(fmt.Errorf("Error retrieving response for activation key request: %s", err))
}

log.Printf("[DEBUG] Received HTTP response: %#v", response)
if response.StatusCode != 302 {
return resource.NonRetryableError(fmt.Errorf("expected HTTP status code 302, received: %d", response.StatusCode))
}

redirectURL, err := response.Location()
if err != nil {
return resource.NonRetryableError(fmt.Errorf("error extracting HTTP Location header: %s", err))
}

errorType := redirectURL.Query().Get("errorType")
if errorType == "PRIVATE_LINK_ENDPOINT_UNREACHABLE" {
errMessage := fmt.Errorf("got error during activation: %s", errorType)
log.Printf("[DEBUG] retryable %s", errMessage)
return resource.RetryableError(errMessage)
}

activationKey = redirectURL.Query().Get("activationKey")
return nil
})
if isResourceTimeoutError(err) {
response, err = client.Do(request)
}
if err != nil {
return fmt.Errorf("error retrieving activation key from IP Address (%s): %s", agentIpAddress, err)
}
if response == nil {
return fmt.Errorf("Error retrieving response for activation key request: %s", err)
}

log.Printf("[DEBUG] Received HTTP response: %#v", response)
if response.StatusCode != 302 {
return fmt.Errorf("expected HTTP status code 302, received: %d", response.StatusCode)
if tfresource.TimedOut(err) {
return fmt.Errorf("timeout retrieving activation key from IP Address (%s): %s", agentIpAddress, err)
}

redirectURL, err := response.Location()
if err != nil {
return fmt.Errorf("error extracting HTTP Location header: %s", err)
return fmt.Errorf("error retrieving activation key from IP Address (%s): %s", agentIpAddress, err)
}

activationKey = redirectURL.Query().Get("activationKey")

if activationKey == "" {
return fmt.Errorf("empty activationKey received from IP Address: %s", agentIpAddress)
}
Expand All @@ -137,6 +175,18 @@ func resourceAwsDataSyncAgentCreate(d *schema.ResourceData, meta interface{}) er
input.AgentName = aws.String(v.(string))
}

if v, ok := d.GetOk("vpc_endpoint_id"); ok {
input.VpcEndpointId = aws.String(v.(string))
}

if v, ok := d.GetOk("security_group_arns"); ok {
input.SecurityGroupArns = expandStringSet(v.(*schema.Set))
}

if v, ok := d.GetOk("subnet_arns"); ok {
input.SubnetArns = expandStringSet(v.(*schema.Set))
}

log.Printf("[DEBUG] Creating DataSync Agent: %s", input)
output, err := conn.CreateAgent(input)
if err != nil {
Expand Down Expand Up @@ -197,6 +247,14 @@ func resourceAwsDataSyncAgentRead(d *schema.ResourceData, meta interface{}) erro
d.Set("arn", output.AgentArn)
d.Set("name", output.Name)

if output.PrivateLinkConfig != nil {
plc := output.PrivateLinkConfig
d.Set("private_link_endpoint", plc.PrivateLinkEndpoint)
d.Set("security_group_arns", flattenStringList(plc.SecurityGroupArns))
d.Set("subnet_arns", flattenStringList(plc.SubnetArns))
d.Set("vpc_endpoint_id", plc.VpcEndpointId)
}

tags, err := keyvaluetags.DatasyncListTags(conn, d.Id())

if err != nil {
Expand Down
69 changes: 59 additions & 10 deletions aws/resource_aws_datasync_agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,34 @@ func TestAccAWSDataSyncAgent_Tags(t *testing.T) {
})
}

func TestAccAWSDataSyncAgent_VpcEndpointId(t *testing.T) {
var agent datasync.DescribeAgentOutput
rName := acctest.RandomWithPrefix("tf-acc-test")
resourceName := "aws_datasync_agent.test"
vpcEndpointResourceName := "aws_vpc_endpoint.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSDataSync(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDataSyncAgentDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSDataSyncAgentConfigVpcEndpointId(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDataSyncAgentExists(resourceName, &agent),
resource.TestCheckResourceAttrPair(resourceName, "vpc_endpoint_id", vpcEndpointResourceName, "id"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"activation_key", "ip_address", "private_link_ip"},
},
},
})
}

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

Expand Down Expand Up @@ -287,17 +315,11 @@ func testAccCheckAWSDataSyncAgentNotRecreated(i, j *datasync.DescribeAgentOutput
}
}

// testAccAWSDataSyncAgentConfigAgentBase uses the "thinstaller" AMI
func testAccAWSDataSyncAgentConfigAgentBase() string {
return `
data "aws_ami" "aws-thinstaller" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["aws-thinstaller-*"]
}
# Reference: https://docs.aws.amazon.com/datasync/latest/userguide/deploy-agents.html
data "aws_ssm_parameter" "aws_service_datasync_ami" {
name = "/aws/service/datasync/ami"
}
resource "aws_vpc" "test" {
Expand Down Expand Up @@ -368,7 +390,7 @@ resource "aws_security_group" "test" {
resource "aws_instance" "test" {
depends_on = [aws_internet_gateway.test]
ami = data.aws_ami.aws-thinstaller.id
ami = data.aws_ssm_parameter.aws_service_datasync_ami.value
associate_public_ip_address = true
# Default instance type from sync.sh
Expand Down Expand Up @@ -424,3 +446,30 @@ resource "aws_datasync_agent" "test" {
}
`, key1, value1, key2, value2)
}

func testAccAWSDataSyncAgentConfigVpcEndpointId(rName string) string {
return testAccAWSDataSyncAgentConfigAgentBase() + fmt.Sprintf(`
resource "aws_datasync_agent" "test" {
name = %q
security_group_arns = [aws_security_group.test.arn]
subnet_arns = [aws_subnet.test.arn]
vpc_endpoint_id = aws_vpc_endpoint.test.id
ip_address = aws_instance.test.public_ip
private_link_endpoint = data.aws_network_interface.test.private_ip
}
data "aws_region" "current" {}
resource "aws_vpc_endpoint" "test" {
service_name = "com.amazonaws.${data.aws_region.current.name}.datasync"
vpc_id = aws_vpc.test.id
security_group_ids = [aws_security_group.test.id]
subnet_ids = [aws_subnet.test.id]
vpc_endpoint_type = "Interface"
}
data "aws_network_interface" "test" {
id = tolist(aws_vpc_endpoint.test.network_interface_ids)[0]
}
`, rName)
}
31 changes: 31 additions & 0 deletions website/docs/r/datasync_agent.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,45 @@ resource "aws_datasync_agent" "example" {
}
```

## Example Usage with VPC Endpoints

```hcl
resource "aws_datasync_agent" "example" {
ip_address = "1.2.3.4"
security_group_arns = [aws_security_group.example.arn]
subnet_arns = [aws_subnet.example.arn]
vpc_endpoint_id = aws_vpc_endpoint.example.id
private_link_endpoint = data.aws_network_interface.example.private_ip
name = "example"
}
data "aws_region" "current" {}
resource "aws_vpc_endpoint" "example" {
service_name = "com.amazonaws.${data.aws_region.current.name}.datasync"
vpc_id = aws_vpc.example.id
security_group_ids = [aws_security_group.example.id]
subnet_ids = [aws_subnet.example.id]
vpc_endpoint_type = "Interface"
}
data "aws_network_interface" "example" {
id = tolist(aws_vpc_endpoint.example.network_interface_ids)[0]
}
```

## Argument Reference

The following arguments are supported:

* `name` - (Required) Name of the DataSync Agent.
* `activation_key` - (Optional) DataSync Agent activation key during resource creation. Conflicts with `ip_address`. If an `ip_address` is provided instead, Terraform will retrieve the `activation_key` as part of the resource creation.
* `ip_address` - (Optional) DataSync Agent IP address to retrieve activation key during resource creation. Conflicts with `activation_key`. DataSync Agent must be accessible on port 80 from where Terraform is running.
* `private_link_endpoint` - (Optional) The IP address of the VPC endpoint the agent should connect to when retrieving an activation key during resource creation. Conflicts with `activation_key`.
* `security_group_arns` - (Optional) The ARNs of the security groups used to protect your data transfer task subnets.
* `subnet_arns` - (Optional) The Amazon Resource Names (ARNs) of the subnets in which DataSync will create elastic network interfaces for each data transfer task.
* `tags` - (Optional) Key-value pairs of resource tags to assign to the DataSync Agent. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level.
* `vpc_endpoint_id` - (Optional) The ID of the VPC (virtual private cloud) endpoint that the agent has access to.

## Attributes Reference

Expand Down

0 comments on commit 760488c

Please sign in to comment.