diff --git a/.changelog/16207.txt b/.changelog/16207.txt new file mode 100644 index 000000000000..3b56c4c99ad5 --- /dev/null +++ b/.changelog/16207.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_datasync_option: Add `private_link_endpoint`, `security_group_arns`, `subnet_arns` and `vpc_endpoint_id` arguments +``` \ No newline at end of file diff --git a/aws/internal/service/datasync/finder/finder.go b/aws/internal/service/datasync/finder/finder.go index abfece626445..c67586365293 100644 --- a/aws/internal/service/datasync/finder/finder.go +++ b/aws/internal/service/datasync/finder/finder.go @@ -7,6 +7,34 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) +func AgentByARN(conn *datasync.DataSync, arn string) (*datasync.DescribeAgentOutput, error) { + input := &datasync.DescribeAgentInput{ + AgentArn: aws.String(arn), + } + + output, err := conn.DescribeAgent(input) + + if tfawserr.ErrMessageContains(err, datasync.ErrCodeInvalidRequestException, "does not exist") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output, nil +} + func TaskByARN(conn *datasync.DataSync, arn string) (*datasync.DescribeTaskOutput, error) { input := &datasync.DescribeTaskInput{ TaskArn: aws.String(arn), diff --git a/aws/internal/service/datasync/waiter/status.go b/aws/internal/service/datasync/waiter/status.go index bbaa85195063..24663dd24d0e 100644 --- a/aws/internal/service/datasync/waiter/status.go +++ b/aws/internal/service/datasync/waiter/status.go @@ -8,6 +8,26 @@ import ( "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) +const ( + agentStatusReady = "ready" +) + +func AgentStatus(conn *datasync.DataSync, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.AgentByARN(conn, arn) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, agentStatusReady, nil + } +} + func TaskStatus(conn *datasync.DataSync, arn string) resource.StateRefreshFunc { return func() (interface{}, string, error) { output, err := finder.TaskByARN(conn, arn) diff --git a/aws/internal/service/datasync/waiter/waiter.go b/aws/internal/service/datasync/waiter/waiter.go index 059dcada10db..1a757a34eaa2 100644 --- a/aws/internal/service/datasync/waiter/waiter.go +++ b/aws/internal/service/datasync/waiter/waiter.go @@ -10,6 +10,23 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) +func AgentReady(conn *datasync.DataSync, arn string, timeout time.Duration) (*datasync.DescribeAgentOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{}, + Target: []string{agentStatusReady}, + Refresh: AgentStatus(conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*datasync.DescribeAgentOutput); ok { + return output, err + } + + return nil, err +} + func TaskAvailable(conn *datasync.DataSync, arn string, timeout time.Duration) (*datasync.DescribeTaskOutput, error) { stateConf := &resource.StateChangeConf{ Pending: []string{datasync.TaskStatusCreating, datasync.TaskStatusUnavailable}, diff --git a/aws/resource_aws_datasync_agent.go b/aws/resource_aws_datasync_agent.go index 90c4b334335f..244b23ea8d45 100644 --- a/aws/resource_aws_datasync_agent.go +++ b/aws/resource_aws_datasync_agent.go @@ -9,9 +9,13 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/datasync" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "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/service/datasync/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/datasync/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func resourceAwsDataSyncAgent() *schema.Resource { @@ -37,12 +41,19 @@ func resourceAwsDataSyncAgent() *schema.Resource { Optional: true, Computed: true, ForceNew: true, - ConflictsWith: []string{"ip_address"}, + ExactlyOneOf: []string{"activation_key", "ip_address"}, + ConflictsWith: []string{"private_link_endpoint"}, }, "ip_address": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ExactlyOneOf: []string{"activation_key", "ip_address"}, + }, + "private_link_endpoint": { Type: schema.TypeString, Optional: true, - Computed: true, ForceNew: true, ConflictsWith: []string{"activation_key"}, }, @@ -50,8 +61,25 @@ func resourceAwsDataSyncAgent() *schema.Resource { 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, @@ -62,67 +90,77 @@ func resourceAwsDataSyncAgentCreate(d *schema.ResourceData, meta interface{}) er conn := meta.(*AWSClient).datasyncconn defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) - region := meta.(*AWSClient).region activationKey := d.Get("activation_key").(string) agentIpAddress := d.Get("ip_address").(string) // Perform one time fetch of activation key from gateway IP address if activationKey == "" { - if agentIpAddress == "" { - return fmt.Errorf("either activation_key or ip_address must be provided") - } - client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Timeout: time.Second * 10, } + region := meta.(*AWSClient).region + + var requestURL string + 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)) + } else { + requestURL = fmt.Sprintf("http://%s/?gatewayType=SYNC&activationRegion=%s", agentIpAddress, region) + } - requestURL := fmt.Sprintf("http://%s/?gatewayType=SYNC&activationRegion=%s", agentIpAddress, region) - log.Printf("[DEBUG] Creating HTTP request: %s", requestURL) request, err := http.NewRequest("GET", requestURL, nil) if err != nil { - return fmt.Errorf("error creating HTTP request: %s", err) + return fmt.Errorf("error creating HTTP request: %w", err) } var response *http.Response err = resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { log.Printf("[DEBUG] Making HTTP request: %s", request.URL.String()) response, err = client.Do(request) + + if err, ok := err.(net.Error); ok { + return resource.RetryableError(fmt.Errorf("error making HTTP request: %w", err)) + } + + if err != nil { + return resource.NonRetryableError(fmt.Errorf("error making HTTP request: %w", err)) + } + + if response == nil { + return resource.NonRetryableError(fmt.Errorf("no response for activation key request")) + } + + log.Printf("[DEBUG] Received HTTP response: %#v", response) + if expected := http.StatusFound; expected != response.StatusCode { + return resource.NonRetryableError(fmt.Errorf("expected HTTP status code %d, received: %d", expected, response.StatusCode)) + } + + redirectURL, err := response.Location() if err != nil { - if err, ok := err.(net.Error); ok { - errMessage := fmt.Errorf("error making HTTP request: %s", err) - log.Printf("[DEBUG] retryable %s", errMessage) - return resource.RetryableError(errMessage) - } - return resource.NonRetryableError(fmt.Errorf("error making HTTP request: %s", err)) + return resource.NonRetryableError(fmt.Errorf("error extracting HTTP Location header: %w", err)) } + + if errorType := redirectURL.Query().Get("errorType"); errorType == "PRIVATE_LINK_ENDPOINT_UNREACHABLE" { + errMessage := fmt.Errorf("got error during activation: %s", errorType) + 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): %w", 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): %w", agentIpAddress, err) } - activationKey = redirectURL.Query().Get("activationKey") - if activationKey == "" { return fmt.Errorf("empty activationKey received from IP Address: %s", agentIpAddress) } @@ -137,35 +175,29 @@ func resourceAwsDataSyncAgentCreate(d *schema.ResourceData, meta interface{}) er input.AgentName = 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)) + } + + if v, ok := d.GetOk("vpc_endpoint_id"); ok { + input.VpcEndpointId = aws.String(v.(string)) + } + log.Printf("[DEBUG] Creating DataSync Agent: %s", input) output, err := conn.CreateAgent(input) + if err != nil { - return fmt.Errorf("error creating DataSync Agent: %s", err) + return fmt.Errorf("error creating DataSync Agent: %w", err) } d.SetId(aws.StringValue(output.AgentArn)) // Agent activations can take a few minutes - descAgentInput := &datasync.DescribeAgentInput{ - AgentArn: aws.String(d.Id()), - } - err = resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { - _, err := conn.DescribeAgent(descAgentInput) - - if isAWSErr(err, "InvalidRequestException", "does not exist") { - return resource.RetryableError(err) - } - - if err != nil { - return resource.NonRetryableError(err) - } - - return nil - }) - if isResourceTimeoutError(err) { - _, err = conn.DescribeAgent(descAgentInput) - } - if err != nil { + if _, err := waiter.AgentReady(conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { return fmt.Errorf("error waiting for DataSync Agent (%s) creation: %s", d.Id(), err) } @@ -177,30 +209,36 @@ func resourceAwsDataSyncAgentRead(d *schema.ResourceData, meta interface{}) erro defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig - input := &datasync.DescribeAgentInput{ - AgentArn: aws.String(d.Id()), - } - - log.Printf("[DEBUG] Reading DataSync Agent: %s", input) - output, err := conn.DescribeAgent(input) + output, err := finder.AgentByARN(conn, d.Id()) - if isAWSErr(err, "InvalidRequestException", "does not exist") { - log.Printf("[WARN] DataSync Agent %q not found - removing from state", d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] DataSync Agent (%s)not found, removing from state", d.Id()) d.SetId("") return nil } if err != nil { - return fmt.Errorf("error reading DataSync Agent (%s): %s", d.Id(), err) + return fmt.Errorf("error reading DataSync Agent (%s): %w", d.Id(), err) } d.Set("arn", output.AgentArn) d.Set("name", output.Name) + if plc := output.PrivateLinkConfig; plc != nil { + 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) + } else { + d.Set("private_link_endpoint", "") + d.Set("security_group_arns", nil) + d.Set("subnet_arns", nil) + d.Set("vpc_endpoint_id", "") + } tags, err := keyvaluetags.DatasyncListTags(conn, d.Id()) if err != nil { - return fmt.Errorf("error listing tags for DataSync Agent (%s): %s", d.Id(), err) + return fmt.Errorf("error listing tags for DataSync Agent (%s): %w", d.Id(), err) } tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) @@ -228,8 +266,9 @@ func resourceAwsDataSyncAgentUpdate(d *schema.ResourceData, meta interface{}) er log.Printf("[DEBUG] Updating DataSync Agent: %s", input) _, err := conn.UpdateAgent(input) + if err != nil { - return fmt.Errorf("error updating DataSync Agent (%s): %s", d.Id(), err) + return fmt.Errorf("error updating DataSync Agent (%s): %w", d.Id(), err) } } @@ -237,7 +276,7 @@ func resourceAwsDataSyncAgentUpdate(d *schema.ResourceData, meta interface{}) er o, n := d.GetChange("tags_all") if err := keyvaluetags.DatasyncUpdateTags(conn, d.Id(), o, n); err != nil { - return fmt.Errorf("error updating DataSync Agent (%s) tags: %s", d.Id(), err) + return fmt.Errorf("error updating DataSync Agent (%s) tags: %w", d.Id(), err) } } @@ -247,19 +286,17 @@ func resourceAwsDataSyncAgentUpdate(d *schema.ResourceData, meta interface{}) er func resourceAwsDataSyncAgentDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).datasyncconn - input := &datasync.DeleteAgentInput{ + log.Printf("[DEBUG] Deleting DataSync Agent: %s", d.Id()) + _, err := conn.DeleteAgent(&datasync.DeleteAgentInput{ AgentArn: aws.String(d.Id()), - } - - log.Printf("[DEBUG] Deleting DataSync Agent: %s", input) - _, err := conn.DeleteAgent(input) + }) - if isAWSErr(err, "InvalidRequestException", "does not exist") { + if tfawserr.ErrMessageContains(err, datasync.ErrCodeInvalidRequestException, "does not exist") { return nil } if err != nil { - return fmt.Errorf("error deleting DataSync Agent (%s): %s", d.Id(), err) + return fmt.Errorf("error deleting DataSync Agent (%s): %w", d.Id(), err) } return nil diff --git a/aws/resource_aws_datasync_agent_test.go b/aws/resource_aws_datasync_agent_test.go index 8ea500980331..9710387cb588 100644 --- a/aws/resource_aws_datasync_agent_test.go +++ b/aws/resource_aws_datasync_agent_test.go @@ -12,6 +12,8 @@ import ( "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/datasync/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func init() { @@ -77,6 +79,7 @@ func testSweepDataSyncAgents(region string) error { func TestAccAWSDataSyncAgent_basic(t *testing.T) { var agent1 datasync.DescribeAgentOutput + rName := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_datasync_agent.test" resource.ParallelTest(t, resource.TestCase{ @@ -86,12 +89,16 @@ func TestAccAWSDataSyncAgent_basic(t *testing.T) { CheckDestroy: testAccCheckAWSDataSyncAgentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSDataSyncAgentConfig(), + Config: testAccAWSDataSyncAgentConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSDataSyncAgentExists(resourceName, &agent1), - resource.TestCheckResourceAttr(resourceName, "name", ""), testAccMatchResourceAttrRegionalARN(resourceName, "arn", "datasync", regexp.MustCompile(`agent/agent-.+`)), + resource.TestCheckResourceAttr(resourceName, "name", ""), + resource.TestCheckResourceAttr(resourceName, "private_link_endpoint", ""), + resource.TestCheckResourceAttr(resourceName, "security_group_arns.#", "0"), + resource.TestCheckResourceAttr(resourceName, "subnet_arns.#", "0"), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "vpc_endpoint_id", ""), ), }, { @@ -106,6 +113,7 @@ func TestAccAWSDataSyncAgent_basic(t *testing.T) { func TestAccAWSDataSyncAgent_disappears(t *testing.T) { var agent1 datasync.DescribeAgentOutput + rName := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_datasync_agent.test" resource.ParallelTest(t, resource.TestCase{ @@ -115,10 +123,10 @@ func TestAccAWSDataSyncAgent_disappears(t *testing.T) { CheckDestroy: testAccCheckAWSDataSyncAgentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSDataSyncAgentConfig(), + Config: testAccAWSDataSyncAgentConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSDataSyncAgentExists(resourceName, &agent1), - testAccCheckAWSDataSyncAgentDisappears(&agent1), + testAccCheckResourceDisappears(testAccProvider, resourceAwsDataSyncAgent(), resourceName), ), ExpectNonEmptyPlan: true, }, @@ -139,14 +147,14 @@ func TestAccAWSDataSyncAgent_AgentName(t *testing.T) { CheckDestroy: testAccCheckAWSDataSyncAgentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSDataSyncAgentConfigName(rName1), + Config: testAccAWSDataSyncAgentConfigName(rName1, rName1), Check: resource.ComposeTestCheckFunc( testAccCheckAWSDataSyncAgentExists(resourceName, &agent1), resource.TestCheckResourceAttr(resourceName, "name", rName1), ), }, { - Config: testAccAWSDataSyncAgentConfigName(rName2), + Config: testAccAWSDataSyncAgentConfigName(rName1, rName2), Check: resource.ComposeTestCheckFunc( testAccCheckAWSDataSyncAgentExists(resourceName, &agent2), resource.TestCheckResourceAttr(resourceName, "name", rName2), @@ -164,6 +172,7 @@ func TestAccAWSDataSyncAgent_AgentName(t *testing.T) { func TestAccAWSDataSyncAgent_Tags(t *testing.T) { var agent1, agent2, agent3 datasync.DescribeAgentOutput + rName := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_datasync_agent.test" resource.ParallelTest(t, resource.TestCase{ @@ -173,7 +182,7 @@ func TestAccAWSDataSyncAgent_Tags(t *testing.T) { CheckDestroy: testAccCheckAWSDataSyncAgentDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSDataSyncAgentConfigTags1("key1", "value1"), + Config: testAccAWSDataSyncAgentConfigTags1(rName, "key1", "value1"), Check: resource.ComposeTestCheckFunc( testAccCheckAWSDataSyncAgentExists(resourceName, &agent1), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), @@ -187,7 +196,7 @@ func TestAccAWSDataSyncAgent_Tags(t *testing.T) { ImportStateVerifyIgnore: []string{"activation_key", "ip_address"}, }, { - Config: testAccAWSDataSyncAgentConfigTags2("key1", "value1updated", "key2", "value2"), + Config: testAccAWSDataSyncAgentConfigTags2(rName, "key1", "value1updated", "key2", "value2"), Check: resource.ComposeTestCheckFunc( testAccCheckAWSDataSyncAgentExists(resourceName, &agent2), testAccCheckAWSDataSyncAgentNotRecreated(&agent1, &agent2), @@ -197,7 +206,7 @@ func TestAccAWSDataSyncAgent_Tags(t *testing.T) { ), }, { - Config: testAccAWSDataSyncAgentConfigTags1("key1", "value1"), + Config: testAccAWSDataSyncAgentConfigTags1(rName, "key1", "value1"), Check: resource.ComposeTestCheckFunc( testAccCheckAWSDataSyncAgentExists(resourceName, &agent3), testAccCheckAWSDataSyncAgentNotRecreated(&agent2, &agent3), @@ -209,6 +218,41 @@ 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" + securityGroupResourceName := "aws_security_group.test" + subnetResourceName := "aws_subnet.test" + vpcEndpointResourceName := "aws_vpc_endpoint.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSDataSync(t) }, + ErrorCheck: testAccErrorCheck(t, datasync.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSDataSyncAgentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSDataSyncAgentConfigVpcEndpointId(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSDataSyncAgentExists(resourceName, &agent), + resource.TestCheckResourceAttr(resourceName, "security_group_arns.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "security_group_arns.*", securityGroupResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "subnet_arns.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "subnet_arns.*", subnetResourceName, "arn"), + 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 @@ -217,19 +261,17 @@ func testAccCheckAWSDataSyncAgentDestroy(s *terraform.State) error { continue } - input := &datasync.DescribeAgentInput{ - AgentArn: aws.String(rs.Primary.ID), - } - - _, err := conn.DescribeAgent(input) + _, err := finder.AgentByARN(conn, rs.Primary.ID) - if isAWSErr(err, "InvalidRequestException", "does not exist") { - return nil + if tfresource.NotFound(err) { + continue } if err != nil { return err } + + return fmt.Errorf("DataSync Agent %s still exists", rs.Primary.ID) } return nil @@ -243,40 +285,19 @@ func testAccCheckAWSDataSyncAgentExists(resourceName string, agent *datasync.Des } conn := testAccProvider.Meta().(*AWSClient).datasyncconn - input := &datasync.DescribeAgentInput{ - AgentArn: aws.String(rs.Primary.ID), - } - output, err := conn.DescribeAgent(input) + output, err := finder.AgentByARN(conn, rs.Primary.ID) if err != nil { return err } - if output == nil { - return fmt.Errorf("Agent %q does not exist", rs.Primary.ID) - } - *agent = *output return nil } } -func testAccCheckAWSDataSyncAgentDisappears(agent *datasync.DescribeAgentOutput) resource.TestCheckFunc { - return func(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).datasyncconn - - input := &datasync.DeleteAgentInput{ - AgentArn: agent.AgentArn, - } - - _, err := conn.DeleteAgent(input) - - return err - } -} - func testAccCheckAWSDataSyncAgentNotRecreated(i, j *datasync.DescribeAgentOutput) resource.TestCheckFunc { return func(s *terraform.State) error { if !aws.TimeValue(i.CreationTime).Equal(aws.TimeValue(j.CreationTime)) { @@ -287,24 +308,18 @@ 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-*"] - } +func testAccAWSDataSyncAgentConfigAgentBase(rName string) string { + return fmt.Sprintf(` +# 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" { cidr_block = "10.0.0.0/16" tags = { - Name = "tf-acc-test-datasync-agent" + Name = %[1]q } } @@ -313,7 +328,7 @@ resource "aws_subnet" "test" { vpc_id = aws_vpc.test.id tags = { - Name = "tf-acc-test-datasync-agent" + Name = %[1]q } } @@ -321,7 +336,7 @@ resource "aws_internet_gateway" "test" { vpc_id = aws_vpc.test.id tags = { - Name = "tf-acc-test-datasync-agent" + Name = %[1]q } } @@ -334,7 +349,7 @@ resource "aws_route_table" "test" { } tags = { - Name = "tf-acc-test-datasync-agent" + Name = %[1]q } } @@ -344,6 +359,7 @@ resource "aws_route_table_association" "test" { } resource "aws_security_group" "test" { + name = %[1]q vpc_id = aws_vpc.test.id egress { @@ -361,14 +377,14 @@ resource "aws_security_group" "test" { } tags = { - Name = "tf-acc-test-datasync-agent" + Name = %[1]q } } 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 @@ -377,50 +393,81 @@ resource "aws_instance" "test" { subnet_id = aws_subnet.test.id tags = { - Name = "tf-acc-test-datasync-agent" + Name = %[1]q } } -` +`, rName) } -func testAccAWSDataSyncAgentConfig() string { - return testAccAWSDataSyncAgentConfigAgentBase() + ` +func testAccAWSDataSyncAgentConfig(rName string) string { + return composeConfig(testAccAWSDataSyncAgentConfigAgentBase(rName), ` resource "aws_datasync_agent" "test" { ip_address = aws_instance.test.public_ip } -` +`) } -func testAccAWSDataSyncAgentConfigName(rName string) string { - return testAccAWSDataSyncAgentConfigAgentBase() + fmt.Sprintf(` +func testAccAWSDataSyncAgentConfigName(rName, agentName string) string { + return composeConfig(testAccAWSDataSyncAgentConfigAgentBase(rName), fmt.Sprintf(` resource "aws_datasync_agent" "test" { ip_address = aws_instance.test.public_ip - name = %q + name = %[1]q } -`, rName) +`, agentName)) } -func testAccAWSDataSyncAgentConfigTags1(key1, value1 string) string { - return testAccAWSDataSyncAgentConfigAgentBase() + fmt.Sprintf(` +func testAccAWSDataSyncAgentConfigTags1(rName, key1, value1 string) string { + return composeConfig(testAccAWSDataSyncAgentConfigAgentBase(rName), fmt.Sprintf(` resource "aws_datasync_agent" "test" { ip_address = aws_instance.test.public_ip tags = { - %q = %q + %[1]q = %[2]q } } -`, key1, value1) +`, key1, value1)) } -func testAccAWSDataSyncAgentConfigTags2(key1, value1, key2, value2 string) string { - return testAccAWSDataSyncAgentConfigAgentBase() + fmt.Sprintf(` +func testAccAWSDataSyncAgentConfigTags2(rName, key1, value1, key2, value2 string) string { + return composeConfig(testAccAWSDataSyncAgentConfigAgentBase(rName), fmt.Sprintf(` resource "aws_datasync_agent" "test" { ip_address = aws_instance.test.public_ip tags = { - %q = %q - %q = %q + %[1]q = %[2]q + %[3]q = %[4]q + } +} +`, key1, value1, key2, value2)) +} + +func testAccAWSDataSyncAgentConfigVpcEndpointId(rName string) string { + return composeConfig(testAccAWSDataSyncAgentConfigAgentBase(rName), fmt.Sprintf(` +resource "aws_datasync_agent" "test" { + name = %[1]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" + + tags = { + Name = %[1]q } } -`, key1, value1, key2, value2) + +data "aws_network_interface" "test" { + id = tolist(aws_vpc_endpoint.test.network_interface_ids)[0] +} +`, rName)) } diff --git a/aws/resource_aws_datasync_task.go b/aws/resource_aws_datasync_task.go index 7f7baa096000..7acec4637cd8 100644 --- a/aws/resource_aws_datasync_task.go +++ b/aws/resource_aws_datasync_task.go @@ -14,6 +14,7 @@ import ( "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/datasync/finder" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/datasync/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func resourceAwsDataSyncTask() *schema.Resource { @@ -198,7 +199,7 @@ func resourceAwsDataSyncTaskRead(d *schema.ResourceData, meta interface{}) error output, err := finder.TaskByARN(conn, d.Id()) - if !d.IsNewResource() && tfawserr.ErrMessageContains(err, datasync.ErrCodeInvalidRequestException, "not found") { + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] DataSync Task (%s) not found, removing from state", d.Id()) d.SetId("") return nil diff --git a/website/docs/r/datasync_agent.html.markdown b/website/docs/r/datasync_agent.html.markdown index 8c501c2b1c65..88d885a06894 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