From 760488cc0fe28b87dcbce61d86c3b15ecb35eff5 Mon Sep 17 00:00:00 2001 From: Gareth Oakley Date: Sun, 15 Nov 2020 19:42:40 +0000 Subject: [PATCH] resource/aws_datasync_agent: Add support for VPC Endpoints --- aws/resource_aws_datasync_agent.go | 92 +++++++++++++++++---- aws/resource_aws_datasync_agent_test.go | 69 +++++++++++++--- website/docs/r/datasync_agent.html.markdown | 31 +++++++ 3 files changed, 165 insertions(+), 27 deletions(-) diff --git a/aws/resource_aws_datasync_agent.go b/aws/resource_aws_datasync_agent.go index 90c4b334335..b46309fa8bc 100644 --- a/aws/resource_aws_datasync_agent.go +++ b/aws/resource_aws_datasync_agent.go @@ -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 { @@ -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, @@ -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, @@ -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 { @@ -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) } @@ -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 { @@ -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 { diff --git a/aws/resource_aws_datasync_agent_test.go b/aws/resource_aws_datasync_agent_test.go index 8ea50098033..6df4aa59cb7 100644 --- a/aws/resource_aws_datasync_agent_test.go +++ b/aws/resource_aws_datasync_agent_test.go @@ -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 @@ -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" { @@ -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 @@ -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) +} diff --git a/website/docs/r/datasync_agent.html.markdown b/website/docs/r/datasync_agent.html.markdown index 8c501c2b1c6..88d885a0689 100644 --- a/website/docs/r/datasync_agent.html.markdown +++ b/website/docs/r/datasync_agent.html.markdown @@ -21,6 +21,33 @@ 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: @@ -28,7 +55,11 @@ 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