diff --git a/aws/resource_aws_transfer_server.go b/aws/resource_aws_transfer_server.go index dca3640015e6..efd6ed34d462 100644 --- a/aws/resource_aws_transfer_server.go +++ b/aws/resource_aws_transfer_server.go @@ -41,6 +41,7 @@ func resourceAwsTransferServer() *schema.Resource { Default: transfer.EndpointTypePublic, ValidateFunc: validation.StringInSlice([]string{ transfer.EndpointTypePublic, + transfer.EndpointTypeVpc, transfer.EndpointTypeVpcEndpoint, }, false), }, @@ -53,7 +54,7 @@ func resourceAwsTransferServer() *schema.Resource { Schema: map[string]*schema.Schema{ "vpc_endpoint_id": { Type: schema.TypeString, - Required: true, + Optional: true, ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { value := v.(string) validNamePattern := "^vpce-[0-9a-f]{17}$" @@ -64,6 +65,28 @@ func resourceAwsTransferServer() *schema.Resource { } return }, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if new == "" && d.Get("endpoint_type").(string) == transfer.EndpointTypeVpc { + return true + } + return false + }, + }, + "vpc_id": { + Type: schema.TypeString, + Optional: true, + }, + "subnet_ids": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "address_allocation_ids": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, }, }, }, @@ -155,7 +178,7 @@ func resourceAwsTransferServerCreate(d *schema.ResourceData, meta interface{}) e } if attr, ok := d.GetOk("endpoint_details"); ok { - createOpts.EndpointDetails = expandTransferServerEndpointDetails(attr.([]interface{})) + createOpts.EndpointDetails = expandTransferServerEndpointDetails(attr.([]interface{}), false) } if attr, ok := d.GetOk("host_key"); ok { @@ -171,6 +194,48 @@ func resourceAwsTransferServerCreate(d *schema.ResourceData, meta interface{}) e d.SetId(*resp.ServerId) + if err := transferServerWaitForServerOnline(conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return fmt.Errorf("error waiting for Trasfer Server (%s) creation: %s", d.Id(), err) + } + + if attr, ok := d.GetOk("endpoint_details"); ok { + ed := expandTransferServerEndpointDetails(attr.([]interface{}), true) + if len(ed.AddressAllocationIds) > 0 { + if err := stopTransferServer(d, conn); err != nil { + return err + } + if err := transferServerWaitForServerOffline(conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return fmt.Errorf("error waiting for Trasfer Server (%s) to stop: %s", d.Id(), err) + } + updateOpts := &transfer.UpdateServerInput{ + ServerId: aws.String(d.Id()), + } + updateOpts.EndpointDetails = ed + err := resource.Retry(10*time.Minute, func() *resource.RetryError { + _, err := conn.UpdateServer(updateOpts) + + if isAWSErr(err, transfer.ErrCodeConflictException, "VPC Endpoint state is not yet available") { + return nil + } + + if err != nil { + return resource.NonRetryableError(err) + } + + return resource.RetryableError(fmt.Errorf("Transfer Server (%s) in conflicted state", d.Id())) + }) + if err != nil { + return fmt.Errorf("error updating Transfer Server (%s): %s", d.Id(), err) + } + if err := startTransferServer(d, conn); err != nil { + return err + } + if err := transferServerWaitForServerOnline(conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return fmt.Errorf("error waiting for Trasfer Server (%s) to start: %s", d.Id(), err) + } + } + } + return resourceAwsTransferServerRead(d, meta) } @@ -193,6 +258,7 @@ func resourceAwsTransferServerRead(d *schema.ResourceData, meta interface{}) err } return err } + log.Printf("[DEBUG] Reading Transfer Server %#v", resp.Server) endpoint := meta.(*AWSClient).RegionalHostname(fmt.Sprintf("%s.server.transfer", d.Id())) @@ -219,6 +285,7 @@ func resourceAwsTransferServerRead(d *schema.ResourceData, meta interface{}) err func resourceAwsTransferServerUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).transferconn updateFlag := false + stopFlag := false updateOpts := &transfer.UpdateServerInput{ ServerId: aws.String(d.Id()), } @@ -251,7 +318,10 @@ func resourceAwsTransferServerUpdate(d *schema.ResourceData, meta interface{}) e if d.HasChange("endpoint_details") { updateFlag = true if attr, ok := d.GetOk("endpoint_details"); ok { - updateOpts.EndpointDetails = expandTransferServerEndpointDetails(attr.([]interface{})) + if d.HasChange("endpoint_details.0.address_allocation_ids") { + stopFlag = true + } + updateOpts.EndpointDetails = expandTransferServerEndpointDetails(attr.([]interface{}), updateFlag) } } @@ -263,6 +333,15 @@ func resourceAwsTransferServerUpdate(d *schema.ResourceData, meta interface{}) e } if updateFlag { + if stopFlag { + if err := stopTransferServer(d, conn); err != nil { + return err + } + if err := transferServerWaitForServerOffline(conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return fmt.Errorf("error waiting for Trasfer Server (%s) to stop: %s", d.Id(), err) + } + } + log.Printf("[DEBUG] Updating Transfer Server %#v", updateOpts) _, err := conn.UpdateServer(updateOpts) if err != nil { if isAWSErr(err, transfer.ErrCodeResourceNotFoundException, "") { @@ -272,6 +351,14 @@ func resourceAwsTransferServerUpdate(d *schema.ResourceData, meta interface{}) e } return fmt.Errorf("error updating Transfer Server (%s): %s", d.Id(), err) } + if stopFlag { + if err := startTransferServer(d, conn); err != nil { + return err + } + if err := transferServerWaitForServerOnline(conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return fmt.Errorf("error waiting for Trasfer Server (%s) to start: %s", d.Id(), err) + } + } } if d.HasChange("tags") { @@ -386,15 +473,30 @@ func deleteTransferUsers(conn *transfer.Transfer, serverID string, nextToken *st return nil } -func expandTransferServerEndpointDetails(l []interface{}) *transfer.EndpointDetails { +func expandTransferServerEndpointDetails(l []interface{}, isUpdate bool) *transfer.EndpointDetails { if len(l) == 0 || l[0] == nil { return nil } e := l[0].(map[string]interface{}) + ed := &transfer.EndpointDetails{} + + if v, ok := e["vpc_endpoint_id"].(string); ok && v != "" { + ed.VpcEndpointId = aws.String(v) + } - return &transfer.EndpointDetails{ - VpcEndpointId: aws.String(e["vpc_endpoint_id"].(string)), + if v, ok := e["vpc_id"].(string); ok && v != "" { + ed.VpcId = aws.String(v) } + + if v, ok := e["subnet_ids"].(*schema.Set); ok && v.Len() > 0 { + ed.SubnetIds = expandStringSet(v) + } + + if v, ok := e["address_allocation_ids"].(*schema.Set); ok && v.Len() > 0 && isUpdate { + ed.AddressAllocationIds = expandStringSet(v) + } + + return ed } func flattenTransferServerEndpointDetails(endpointDetails *transfer.EndpointDetails) []interface{} { @@ -403,8 +505,88 @@ func flattenTransferServerEndpointDetails(endpointDetails *transfer.EndpointDeta } e := map[string]interface{}{ - "vpc_endpoint_id": aws.StringValue(endpointDetails.VpcEndpointId), + "vpc_endpoint_id": aws.StringValue(endpointDetails.VpcEndpointId), + "vpc_id": aws.StringValue(endpointDetails.VpcId), + "subnet_ids": flattenStringSet(endpointDetails.SubnetIds), + "address_allocation_ids": flattenStringSet(endpointDetails.AddressAllocationIds), } return []interface{}{e} } + +func stopTransferServer(d *schema.ResourceData, conn *transfer.Transfer) error { + stopReq := &transfer.StopServerInput{ + ServerId: aws.String(d.Id()), + } + if _, err := conn.StopServer(stopReq); err != nil { + return fmt.Errorf("error stopping Transfer Server (%s): %s", d.Id(), err) + } + return nil +} + +func startTransferServer(d *schema.ResourceData, conn *transfer.Transfer) error { + stopReq := &transfer.StartServerInput{ + ServerId: aws.String(d.Id()), + } + if _, err := conn.StartServer(stopReq); err != nil { + return fmt.Errorf("error starting Transfer Server (%s): %s", d.Id(), err) + } + return nil +} + +func transferServerWaitForServerOnline(conn *transfer.Transfer, serverId string, timeout time.Duration) error { + stateChangeConf := &resource.StateChangeConf{ + Pending: []string{transfer.StateStarting, transfer.StateOffline, transfer.StateStopping}, + Target: []string{transfer.StateOnline}, + Refresh: transferRefreshServerStatus(conn, serverId), + Timeout: timeout, + Delay: 10 * time.Second, + } + + _, err := stateChangeConf.WaitForState() + + return err +} + +func transferServerWaitForServerOffline(conn *transfer.Transfer, serverId string, timeout time.Duration) error { + stateChangeConf := &resource.StateChangeConf{ + Pending: []string{transfer.StateStarting, transfer.StateOnline, transfer.StateStopping}, + Target: []string{transfer.StateOffline}, + Refresh: transferRefreshServerStatus(conn, serverId), + Timeout: timeout, + Delay: 10 * time.Second, + } + + _, err := stateChangeConf.WaitForState() + + return err +} + +func transferRefreshServerStatus(conn *transfer.Transfer, serverId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + server, err := describeTransferServer(conn, serverId) + + if server == nil { + return 42, "destroyed", nil + } + + if server.State != nil { + log.Printf("[DEBUG] Transfer Server status (%s): %s", serverId, *server.State) + } + + return server, aws.StringValue(server.State), err + } +} + +func describeTransferServer(conn *transfer.Transfer, serverId string) (*transfer.DescribedServer, error) { + params := &transfer.DescribeServerInput{ + ServerId: aws.String(serverId), + } + + resp, err := conn.DescribeServer(params) + if err != nil { + log.Printf("[WARN] Error on descibing Transfer Server: %s", err) + return nil, err + } + return resp.Server, err +} diff --git a/aws/resource_aws_transfer_server_test.go b/aws/resource_aws_transfer_server_test.go index bfa5eb18c4e8..8744a0cc39eb 100644 --- a/aws/resource_aws_transfer_server_test.go +++ b/aws/resource_aws_transfer_server_test.go @@ -68,27 +68,26 @@ func testSweepTransferServers(region string) error { func TestAccAWSTransferServer_basic(t *testing.T) { var conf transfer.DescribedServer rName := acctest.RandString(5) + resourceName := "aws_transfer_server.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t) }, - IDRefreshName: "aws_transfer_server.foo", + IDRefreshName: resourceName, Providers: testAccProviders, CheckDestroy: testAccCheckAWSTransferServerDestroy, Steps: []resource.TestStep{ { Config: testAccAWSTransferServerConfig_basic, Check: resource.ComposeTestCheckFunc( - testAccCheckAWSTransferServerExists("aws_transfer_server.foo", &conf), - testAccMatchResourceAttrRegionalARN("aws_transfer_server.foo", "arn", "transfer", regexp.MustCompile(`server/.+`)), - resource.TestMatchResourceAttr( - "aws_transfer_server.foo", "endpoint", regexp.MustCompile(fmt.Sprintf("^s-[a-z0-9]+.server.transfer.%s.amazonaws.com$", testAccGetRegion()))), - resource.TestCheckResourceAttr( - "aws_transfer_server.foo", "identity_provider_type", "SERVICE_MANAGED"), - resource.TestCheckResourceAttr("aws_transfer_server.foo", "tags.%", "0"), + testAccCheckAWSTransferServerExists(resourceName, &conf), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "transfer", regexp.MustCompile(`server/.+`)), + resource.TestMatchResourceAttr(resourceName, "endpoint", regexp.MustCompile(fmt.Sprintf("^s-[a-z0-9]+.server.transfer.%s.amazonaws.com$", testAccGetRegion()))), + resource.TestCheckResourceAttr(resourceName, "identity_provider_type", "SERVICE_MANAGED"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), ), }, { - ResourceName: "aws_transfer_server.foo", + ResourceName: resourceName, ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"force_destroy"}, @@ -96,15 +95,11 @@ func TestAccAWSTransferServer_basic(t *testing.T) { { Config: testAccAWSTransferServerConfig_basicUpdate(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSTransferServerExists("aws_transfer_server.foo", &conf), - resource.TestCheckResourceAttr( - "aws_transfer_server.foo", "tags.%", "2"), - resource.TestCheckResourceAttr( - "aws_transfer_server.foo", "tags.NAME", "tf-acc-test-transfer-server"), - resource.TestCheckResourceAttr( - "aws_transfer_server.foo", "tags.ENV", "test"), - resource.TestCheckResourceAttrPair( - "aws_transfer_server.foo", "logging_role", "aws_iam_role.foo", "arn"), + testAccCheckAWSTransferServerExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.NAME", "tf-acc-test-transfer-server"), + resource.TestCheckResourceAttr(resourceName, "tags.ENV", "test"), + resource.TestCheckResourceAttrPair(resourceName, "logging_role", "aws_iam_role.test", "arn"), ), }, }, @@ -114,27 +109,23 @@ func TestAccAWSTransferServer_basic(t *testing.T) { func TestAccAWSTransferServer_apigateway(t *testing.T) { var conf transfer.DescribedServer rName := acctest.RandString(5) + resourceName := "aws_transfer_server.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t) }, - IDRefreshName: "aws_transfer_server.foo", + IDRefreshName: resourceName, Providers: testAccProviders, CheckDestroy: testAccCheckAWSTransferServerDestroy, Steps: []resource.TestStep{ { Config: testAccAWSTransferServerConfig_apigateway(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSTransferServerExists("aws_transfer_server.foo", &conf), - resource.TestCheckResourceAttr( - "aws_transfer_server.foo", "identity_provider_type", "API_GATEWAY"), - resource.TestCheckResourceAttrSet( - "aws_transfer_server.foo", "invocation_role"), - resource.TestCheckResourceAttr( - "aws_transfer_server.foo", "tags.%", "2"), - resource.TestCheckResourceAttr( - "aws_transfer_server.foo", "tags.NAME", "tf-acc-test-transfer-server"), - resource.TestCheckResourceAttr( - "aws_transfer_server.foo", "tags.TYPE", "apigateway"), + testAccCheckAWSTransferServerExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "identity_provider_type", "API_GATEWAY"), + resource.TestCheckResourceAttrSet(resourceName, "invocation_role"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.NAME", "tf-acc-test-transfer-server"), + resource.TestCheckResourceAttr(resourceName, "tags.TYPE", "apigateway"), ), }, }, @@ -143,6 +134,7 @@ func TestAccAWSTransferServer_apigateway(t *testing.T) { func TestAccAWSTransferServer_disappears(t *testing.T) { var conf transfer.DescribedServer + resourceName := "aws_transfer_server.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t) }, @@ -152,7 +144,7 @@ func TestAccAWSTransferServer_disappears(t *testing.T) { { Config: testAccAWSTransferServerConfig_basic, Check: resource.ComposeTestCheckFunc( - testAccCheckAWSTransferServerExists("aws_transfer_server.foo", &conf), + testAccCheckAWSTransferServerExists(resourceName, &conf), testAccCheckAWSTransferServerDisappears(&conf), ), ExpectNonEmptyPlan: true, @@ -165,28 +157,27 @@ func TestAccAWSTransferServer_forcedestroy(t *testing.T) { var conf transfer.DescribedServer var roleConf iam.GetRoleOutput rName := acctest.RandString(5) + resourceName := "aws_transfer_server.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t) }, - IDRefreshName: "aws_transfer_server.foo", + IDRefreshName: resourceName, Providers: testAccProviders, CheckDestroy: testAccCheckAWSTransferServerDestroy, Steps: []resource.TestStep{ { Config: testAccAWSTransferServerConfig_forcedestroy(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSTransferServerExists("aws_transfer_server.foo", &conf), - testAccCheckAWSRoleExists("aws_iam_role.foo", &roleConf), - resource.TestCheckResourceAttr( - "aws_transfer_server.foo", "identity_provider_type", "SERVICE_MANAGED"), - resource.TestCheckResourceAttr( - "aws_transfer_server.foo", "force_destroy", "true"), + testAccCheckAWSTransferServerExists(resourceName, &conf), + testAccCheckAWSRoleExists("aws_iam_role.test", &roleConf), + resource.TestCheckResourceAttr(resourceName, "identity_provider_type", "SERVICE_MANAGED"), + resource.TestCheckResourceAttr(resourceName, "force_destroy", "true"), testAccCheckAWSTransferCreateUser(&conf, &roleConf, rName), testAccCheckAWSTransferCreateSshKey(&conf, rName), ), }, { - ResourceName: "aws_transfer_server.foo", + ResourceName: resourceName, ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"force_destroy", "host_key"}, @@ -197,7 +188,7 @@ func TestAccAWSTransferServer_forcedestroy(t *testing.T) { func TestAccAWSTransferServer_vpcEndpointId(t *testing.T) { var conf transfer.DescribedServer - resourceName := "aws_transfer_server.default" + resourceName := "aws_transfer_server.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t) }, @@ -209,8 +200,36 @@ func TestAccAWSTransferServer_vpcEndpointId(t *testing.T) { Config: testAccAWSTransferServerConfig_VpcEndPoint, Check: resource.ComposeTestCheckFunc( testAccCheckAWSTransferServerExists(resourceName, &conf), - resource.TestCheckResourceAttr( - resourceName, "endpoint_type", "VPC_ENDPOINT"), + resource.TestCheckResourceAttr(resourceName, "endpoint_type", "VPC_ENDPOINT"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"force_destroy"}, + }, + }, + }) +} + +func TestAccAWSTransferServer_vpcOnly(t *testing.T) { + var conf transfer.DescribedServer + resourceName := "aws_transfer_server.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t) }, + IDRefreshName: resourceName, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSTransferServerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSTransferServerConfig_vpc, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSTransferServerExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "endpoint_type", "VPC"), + resource.TestCheckResourceAttr(resourceName, "endpoint_details.0.subnet_ids.#", "2"), + resource.TestCheckResourceAttr(resourceName, "endpoint_details.0.address_allocation_ids.#", "2"), ), }, { @@ -373,14 +392,14 @@ func testAccPreCheckAWSTransfer(t *testing.T) { } const testAccAWSTransferServerConfig_basic = ` -resource "aws_transfer_server" "foo" {} +resource "aws_transfer_server" "test" {} ` func testAccAWSTransferServerConfig_basicUpdate(rName string) string { return fmt.Sprintf(` -resource "aws_iam_role" "foo" { - name = "tf-test-transfer-server-iam-role-%s" +resource "aws_iam_role" "test" { + name = "tf-test-transfer-server-iam-role-%[1]s" assume_role_policy = <