diff --git a/.changelog/13371.txt b/.changelog/13371.txt new file mode 100644 index 00000000000..2725cd73be0 --- /dev/null +++ b/.changelog/13371.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_transfer_server: Add `protocols` argument +``` + +```release-note:enhancement +data-source/aws_transfer_server: Add `certificate`, `endpoint_type`, `protocols` and `security_policy_name` attributes +``` \ No newline at end of file diff --git a/aws/data_source_aws_transfer_server.go b/aws/data_source_aws_transfer_server.go index e239bca22cf..69047839a1a 100644 --- a/aws/data_source_aws_transfer_server.go +++ b/aws/data_source_aws_transfer_server.go @@ -2,46 +2,75 @@ package aws import ( "fmt" - "log" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/transfer" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/transfer/finder" ) func dataSourceAwsTransferServer() *schema.Resource { return &schema.Resource{ - Read: dataSourceAwsTransferServerRead, Schema: map[string]*schema.Schema{ - "server_id": { + "arn": { Type: schema.TypeString, - Required: true, + Computed: true, }, - "arn": { + + "certificate": { Type: schema.TypeString, Computed: true, }, + "endpoint": { Type: schema.TypeString, Computed: true, }, - "invocation_role": { + + "endpoint_type": { Type: schema.TypeString, Computed: true, }, - "url": { + + "identity_provider_type": { Type: schema.TypeString, Computed: true, }, - "identity_provider_type": { + + "invocation_role": { Type: schema.TypeString, Computed: true, }, + "logging_role": { Type: schema.TypeString, Computed: true, }, + + "protocols": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "security_policy_name": { + Type: schema.TypeString, + Computed: true, + }, + + "server_id": { + Type: schema.TypeString, + Required: true, + }, + + "url": { + Type: schema.TypeString, + Computed: true, + }, }, + + Read: dataSourceAwsTransferServerRead, } } @@ -49,28 +78,32 @@ func dataSourceAwsTransferServerRead(d *schema.ResourceData, meta interface{}) e conn := meta.(*AWSClient).transferconn serverID := d.Get("server_id").(string) - input := &transfer.DescribeServerInput{ - ServerId: aws.String(serverID), - } - log.Printf("[DEBUG] Describe Transfer Server Option: %#v", input) + output, err := finder.ServerByID(conn, serverID) - resp, err := conn.DescribeServer(input) if err != nil { - return fmt.Errorf("error describing Transfer Server (%s): %w", serverID, err) + return fmt.Errorf("error reading Transfer Server (%s): %w", serverID, err) } - endpoint := meta.(*AWSClient).RegionalHostname(fmt.Sprintf("%s.server.transfer", serverID)) - - d.SetId(serverID) - d.Set("arn", resp.Server.Arn) - d.Set("endpoint", endpoint) - if resp.Server.IdentityProviderDetails != nil { - d.Set("invocation_role", resp.Server.IdentityProviderDetails.InvocationRole) - d.Set("url", resp.Server.IdentityProviderDetails.Url) + d.SetId(aws.StringValue(output.ServerId)) + d.Set("arn", output.Arn) + d.Set("certificate", output.Certificate) + d.Set("endpoint", meta.(*AWSClient).RegionalHostname(fmt.Sprintf("%s.server.transfer", serverID))) + d.Set("endpoint_type", output.EndpointType) + d.Set("identity_provider_type", output.IdentityProviderType) + if output.IdentityProviderDetails != nil { + d.Set("invocation_role", output.IdentityProviderDetails.InvocationRole) + } else { + d.Set("invocation_role", "") + } + d.Set("logging_role", output.LoggingRole) + d.Set("protocols", aws.StringValueSlice(output.Protocols)) + d.Set("security_policy_name", output.SecurityPolicyName) + if output.IdentityProviderDetails != nil { + d.Set("url", output.IdentityProviderDetails.Url) + } else { + d.Set("url", "") } - d.Set("identity_provider_type", resp.Server.IdentityProviderType) - d.Set("logging_role", resp.Server.LoggingRole) return nil } diff --git a/aws/data_source_aws_transfer_server_test.go b/aws/data_source_aws_transfer_server_test.go index ad55309eb06..5a0e22104d3 100644 --- a/aws/data_source_aws_transfer_server_test.go +++ b/aws/data_source_aws_transfer_server_test.go @@ -45,9 +45,15 @@ func TestAccDataSourceAwsTransferServer_service_managed(t *testing.T) { Config: testAccDataSourceAwsTransferServerConfig_service_managed(rName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "certificate", resourceName, "certificate"), resource.TestCheckResourceAttrPair(datasourceName, "endpoint", resourceName, "endpoint"), + resource.TestCheckResourceAttrPair(datasourceName, "endpoint_type", resourceName, "endpoint_type"), resource.TestCheckResourceAttrPair(datasourceName, "identity_provider_type", resourceName, "identity_provider_type"), + resource.TestCheckResourceAttrPair(datasourceName, "invocation_role", resourceName, "invocation_role"), resource.TestCheckResourceAttrPair(datasourceName, "logging_role", resourceName, "logging_role"), + resource.TestCheckResourceAttrPair(datasourceName, "protocols.#", resourceName, "protocols.#"), + resource.TestCheckResourceAttrPair(datasourceName, "security_policy_name", resourceName, "security_policy_name"), + resource.TestCheckResourceAttrPair(datasourceName, "url", resourceName, "url"), ), }, }, diff --git a/aws/internal/service/transfer/finder/finder.go b/aws/internal/service/transfer/finder/finder.go new file mode 100644 index 00000000000..5680e032ca2 --- /dev/null +++ b/aws/internal/service/transfer/finder/finder.go @@ -0,0 +1,36 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/transfer" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func ServerByID(conn *transfer.Transfer, id string) (*transfer.DescribedServer, error) { + input := &transfer.DescribeServerInput{ + ServerId: aws.String(id), + } + + output, err := conn.DescribeServer(input) + + if tfawserr.ErrCodeEquals(err, transfer.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Server == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Server, nil +} diff --git a/aws/internal/service/transfer/id.go b/aws/internal/service/transfer/id.go new file mode 100644 index 00000000000..13e3143770a --- /dev/null +++ b/aws/internal/service/transfer/id.go @@ -0,0 +1,25 @@ +package transfer + +import ( + "fmt" + "strings" +) + +const userResourceIDSeparator = "/" + +func UserCreateResourceID(serverID, userName string) string { + parts := []string{serverID, userName} + id := strings.Join(parts, userResourceIDSeparator) + + return id +} + +func UserParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, userResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected SERVERID%[2]sUSERNAME", id, userResourceIDSeparator) +} diff --git a/aws/internal/service/transfer/waiter/status.go b/aws/internal/service/transfer/waiter/status.go new file mode 100644 index 00000000000..b142972008d --- /dev/null +++ b/aws/internal/service/transfer/waiter/status.go @@ -0,0 +1,25 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/transfer" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/transfer/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func ServerState(conn *transfer.Transfer, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.ServerByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.State), nil + } +} diff --git a/aws/internal/service/transfer/waiter/waiter.go b/aws/internal/service/transfer/waiter/waiter.go new file mode 100644 index 00000000000..a6d5000e2cf --- /dev/null +++ b/aws/internal/service/transfer/waiter/waiter.go @@ -0,0 +1,80 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/transfer" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + ServerDeletedTimeout = 10 * time.Minute +) + +func ServerCreated(conn *transfer.Transfer, id string, timeout time.Duration) (*transfer.DescribedServer, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{transfer.StateStarting}, + Target: []string{transfer.StateOnline}, + Refresh: ServerState(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*transfer.DescribedServer); ok { + return output, err + } + + return nil, err +} + +func ServerDeleted(conn *transfer.Transfer, id string) (*transfer.DescribedServer, error) { + stateConf := &resource.StateChangeConf{ + Pending: transfer.State_Values(), + Target: []string{}, + Refresh: ServerState(conn, id), + Timeout: ServerDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*transfer.DescribedServer); ok { + return output, err + } + + return nil, err +} + +func ServerStarted(conn *transfer.Transfer, id string, timeout time.Duration) (*transfer.DescribedServer, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{transfer.StateStarting, transfer.StateOffline, transfer.StateStopping}, + Target: []string{transfer.StateOnline}, + Refresh: ServerState(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*transfer.DescribedServer); ok { + return output, err + } + + return nil, err +} + +func ServerStopped(conn *transfer.Transfer, id string, timeout time.Duration) (*transfer.DescribedServer, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{transfer.StateStarting, transfer.StateOnline, transfer.StateStopping}, + Target: []string{transfer.StateOffline}, + Refresh: ServerState(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*transfer.DescribedServer); ok { + return output, err + } + + return nil, err +} diff --git a/aws/resource_aws_transfer_server.go b/aws/resource_aws_transfer_server.go index e7eff78df71..86a0bd6b24a 100644 --- a/aws/resource_aws_transfer_server.go +++ b/aws/resource_aws_transfer_server.go @@ -8,10 +8,15 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/transfer" "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "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/keyvaluetags" + tftransfer "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/transfer" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/transfer/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/transfer/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func resourceAwsTransferServer() *schema.Resource { @@ -30,30 +35,23 @@ func resourceAwsTransferServer() *schema.Resource { Computed: true, }, + "certificate": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateArn, + }, + "endpoint": { Type: schema.TypeString, Computed: true, }, - "endpoint_type": { - Type: schema.TypeString, - Optional: true, - Default: transfer.EndpointTypePublic, - ValidateFunc: validation.StringInSlice(transfer.EndpointType_Values(), false), - }, - "endpoint_details": { Type: schema.TypeList, Optional: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "vpc_endpoint_id": { - Type: schema.TypeString, - Optional: true, - ConflictsWith: []string{"endpoint_details.0.address_allocation_ids", "endpoint_details.0.subnet_ids", "endpoint_details.0.vpc_id"}, - Computed: true, - }, "address_allocation_ids": { Type: schema.TypeSet, Optional: true, @@ -68,6 +66,12 @@ func resourceAwsTransferServer() *schema.Resource { Set: schema.HashString, ConflictsWith: []string{"endpoint_details.0.vpc_endpoint_id"}, }, + "vpc_endpoint_id": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"endpoint_details.0.address_allocation_ids", "endpoint_details.0.subnet_ids", "endpoint_details.0.vpc_id"}, + Computed: true, + }, "vpc_id": { Type: schema.TypeString, Optional: true, @@ -78,27 +82,29 @@ func resourceAwsTransferServer() *schema.Resource { }, }, - "host_key": { + "endpoint_type": { Type: schema.TypeString, Optional: true, - Sensitive: true, - ValidateFunc: validation.StringLenBetween(0, 4096), + Default: transfer.EndpointTypePublic, + ValidateFunc: validation.StringInSlice(transfer.EndpointType_Values(), false), }, - "host_key_fingerprint": { - Type: schema.TypeString, - Computed: true, + "force_destroy": { + Type: schema.TypeBool, + Optional: true, + Default: false, }, - "invocation_role": { + "host_key": { Type: schema.TypeString, Optional: true, - ValidateFunc: validateArn, + Sensitive: true, + ValidateFunc: validation.StringLenBetween(0, 4096), }, - "url": { + "host_key_fingerprint": { Type: schema.TypeString, - Optional: true, + Computed: true, }, "identity_provider_type": { @@ -109,17 +115,30 @@ func resourceAwsTransferServer() *schema.Resource { ValidateFunc: validation.StringInSlice(transfer.IdentityProviderType_Values(), false), }, + "invocation_role": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateArn, + }, + "logging_role": { Type: schema.TypeString, Optional: true, ValidateFunc: validateArn, }, - "force_destroy": { - Type: schema.TypeBool, + "protocols": { + Type: schema.TypeSet, + MinItems: 1, + MaxItems: 3, Optional: true, - Default: false, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice(transfer.Protocol_Values(), false), + }, }, + "security_policy_name": { Type: schema.TypeString, Optional: true, @@ -131,9 +150,13 @@ func resourceAwsTransferServer() *schema.Resource { }, false), }, - "tags": tagsSchema(), - + "tags": tagsSchema(), "tags_all": tagsSchemaComputed(), + + "url": { + Type: schema.TypeString, + Optional: true, + }, }, CustomizeDiff: SetTagsDiff, @@ -145,119 +168,98 @@ func resourceAwsTransferServerCreate(d *schema.ResourceData, meta interface{}) e conn := meta.(*AWSClient).transferconn defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) - createOpts := &transfer.CreateServerInput{} - if len(tags) > 0 { - createOpts.Tags = tags.IgnoreAws().TransferTags() + input := &transfer.CreateServerInput{} + + if v, ok := d.GetOk("certificate"); ok { + input.Certificate = aws.String(v.(string)) } - identityProviderDetails := &transfer.IdentityProviderDetails{} - if attr, ok := d.GetOk("invocation_role"); ok { - identityProviderDetails.InvocationRole = aws.String(attr.(string)) + if v, ok := d.GetOk("endpoint_details"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.EndpointDetails = expandTransferEndpointDetails(v.([]interface{})[0].(map[string]interface{})) + + // Prevent the following error: InvalidRequestException: AddressAllocationIds cannot be set in CreateServer + // Reference: https://docs.aws.amazon.com/transfer/latest/userguide/API_EndpointDetails.html#TransferFamily-Type-EndpointDetails-AddressAllocationIds + if input.EndpointDetails != nil && len(input.EndpointDetails.AddressAllocationIds) > 0 { + input.EndpointDetails.AddressAllocationIds = nil + updateAfterCreate = true + } } - if attr, ok := d.GetOk("url"); ok { - identityProviderDetails.Url = aws.String(attr.(string)) + if v, ok := d.GetOk("endpoint_type"); ok { + input.EndpointType = aws.String(v.(string)) } - if identityProviderDetails.Url != nil || identityProviderDetails.InvocationRole != nil { - createOpts.IdentityProviderDetails = identityProviderDetails + if v, ok := d.GetOk("host_key"); ok { + input.HostKey = aws.String(v.(string)) } - if attr, ok := d.GetOk("identity_provider_type"); ok { - createOpts.IdentityProviderType = aws.String(attr.(string)) + if v, ok := d.GetOk("identity_provider_type"); ok { + input.IdentityProviderType = aws.String(v.(string)) } - if attr, ok := d.GetOk("logging_role"); ok { - createOpts.LoggingRole = aws.String(attr.(string)) + if v, ok := d.GetOk("invocation_role"); ok { + if input.IdentityProviderDetails == nil { + input.IdentityProviderDetails = &transfer.IdentityProviderDetails{} + } + + input.IdentityProviderDetails.InvocationRole = aws.String(v.(string)) } - if attr, ok := d.GetOk("endpoint_type"); ok { - createOpts.EndpointType = aws.String(attr.(string)) + if v, ok := d.GetOk("logging_role"); ok { + input.LoggingRole = aws.String(v.(string)) } - if attr, ok := d.GetOk("security_policy_name"); ok { - createOpts.SecurityPolicyName = aws.String(attr.(string)) + if v, ok := d.GetOk("protocols"); ok && v.(*schema.Set).Len() > 0 { + input.Protocols = expandStringSet(v.(*schema.Set)) } - if attr, ok := d.GetOk("endpoint_details"); ok { - createOpts.EndpointDetails = expandTransferServerEndpointDetails(attr.([]interface{})) + if v, ok := d.GetOk("security_policy_name"); ok { + input.SecurityPolicyName = aws.String(v.(string)) + } - // Prevent the following error: InvalidRequestException: AddressAllocationIds cannot be set in CreateServer - // Reference: https://docs.aws.amazon.com/transfer/latest/userguide/API_EndpointDetails.html#TransferFamily-Type-EndpointDetails-AddressAllocationIds - if createOpts.EndpointDetails.AddressAllocationIds != nil { - createOpts.EndpointDetails.AddressAllocationIds = nil - updateAfterCreate = true + if v, ok := d.GetOk("url"); ok { + if input.IdentityProviderDetails == nil { + input.IdentityProviderDetails = &transfer.IdentityProviderDetails{} } + + input.IdentityProviderDetails.Url = aws.String(v.(string)) } - if attr, ok := d.GetOk("host_key"); ok { - createOpts.HostKey = aws.String(attr.(string)) + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().TransferTags() } - log.Printf("[DEBUG] Create Transfer Server Option: %#v", createOpts) + log.Printf("[DEBUG] Creating Transfer Server: %s", input) + output, err := conn.CreateServer(input) - resp, err := conn.CreateServer(createOpts) if err != nil { - return fmt.Errorf("Error creating Transfer Server: %s", err) + return fmt.Errorf("error creating Transfer Server: %w", err) } - d.SetId(aws.StringValue(resp.ServerId)) + d.SetId(aws.StringValue(output.ServerId)) - stateChangeConf := &resource.StateChangeConf{ - Pending: []string{transfer.StateStarting}, - Target: []string{transfer.StateOnline}, - Refresh: refreshTransferServerStatus(conn, d.Id()), - Timeout: d.Timeout(schema.TimeoutCreate), - Delay: 10 * time.Second, - } + _, err = waiter.ServerCreated(conn, d.Id(), d.Timeout(schema.TimeoutCreate)) - if _, err := stateChangeConf.WaitForState(); err != nil { - return fmt.Errorf("error waiting for Transfer Server (%s) to start: %w", d.Id(), err) + if err != nil { + return fmt.Errorf("error waiting for Transfer Server (%s) to create: %w", d.Id(), err) } if updateAfterCreate { - if err := stopAndWaitForTransferServer(d.Id(), conn, d.Timeout(schema.TimeoutCreate)); err != nil { + if err := stopTransferServer(conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { return err } - updateOpts := &transfer.UpdateServerInput{ + input := &transfer.UpdateServerInput{ ServerId: aws.String(d.Id()), - EndpointDetails: expandTransferServerEndpointDetails(d.Get("endpoint_details").([]interface{})), + EndpointDetails: expandTransferEndpointDetails(d.Get("endpoint_details").([]interface{})[0].(map[string]interface{})), } - // EIPs cannot be assigned directly on server creation, so the server must - // be created, stopped, and updated. The Transfer API will return a state - // of ONLINE before the underlying VPC Endpoint is available and attempting - // to assign the EIPs will return an error until that EC2 API process is - // complete: - // ConflictException: VPC Endpoint state is not yet available - // To prevent accessing the EC2 API directly to check the VPC Endpoint - // state, which can require confusing IAM permissions and have other - // eventual consistency consideration, we retry only via the Transfer API. - err := resource.Retry(Ec2VpcEndpointCreationTimeout, func() *resource.RetryError { - _, err := conn.UpdateServer(updateOpts) - - if tfawserr.ErrMessageContains(err, transfer.ErrCodeConflictException, "VPC Endpoint state is not yet available") { - return resource.RetryableError(err) - } - - if err != nil { - return resource.NonRetryableError(err) - } - - return nil - }) - - if isResourceTimeoutError(err) { - _, err = conn.UpdateServer(updateOpts) - } - - if err != nil { - return fmt.Errorf("error updating Transfer Server (%s): %w", d.Id(), err) + if err := updateTransferServer(conn, input); err != nil { + return err } - if err := startAndWaitForTransferServer(d.Id(), conn, d.Timeout(schema.TimeoutCreate)); err != nil { + if err := startTransferServer(conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { return err } } @@ -270,40 +272,46 @@ func resourceAwsTransferServerRead(d *schema.ResourceData, meta interface{}) err defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig - descOpts := &transfer.DescribeServerInput{ - ServerId: aws.String(d.Id()), - } + output, err := finder.ServerByID(conn, d.Id()) - log.Printf("[DEBUG] Describe Transfer Server Option: %#v", descOpts) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Transfer Server (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } - resp, err := conn.DescribeServer(descOpts) if err != nil { - if isAWSErr(err, transfer.ErrCodeResourceNotFoundException, "") { - log.Printf("[WARN] Transfer Server (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil - } - return err + return fmt.Errorf("error reading Transfer Server (%s): %w", d.Id(), err) } - endpoint := meta.(*AWSClient).RegionalHostname(fmt.Sprintf("%s.server.transfer", d.Id())) - - d.Set("arn", resp.Server.Arn) - d.Set("endpoint", endpoint) - d.Set("invocation_role", "") - d.Set("url", "") - if resp.Server.IdentityProviderDetails != nil { - d.Set("invocation_role", resp.Server.IdentityProviderDetails.InvocationRole) - d.Set("url", resp.Server.IdentityProviderDetails.Url) + d.Set("arn", output.Arn) + d.Set("certificate", output.Certificate) + d.Set("endpoint", meta.(*AWSClient).RegionalHostname(fmt.Sprintf("%s.server.transfer", d.Id()))) + if output.EndpointDetails != nil { + if err := d.Set("endpoint_details", []interface{}{flattenTransferEndpointDetails(output.EndpointDetails)}); err != nil { + return fmt.Errorf("error setting endpoint_details: %w", err) + } + } else { + d.Set("endpoint_details", nil) + } + d.Set("endpoint_type", output.EndpointType) + d.Set("host_key_fingerprint", output.HostKeyFingerprint) + d.Set("identity_provider_type", output.IdentityProviderType) + if output.IdentityProviderDetails != nil { + d.Set("invocation_role", output.IdentityProviderDetails.InvocationRole) + } else { + d.Set("invocation_role", "") + } + d.Set("logging_role", output.LoggingRole) + d.Set("protocols", aws.StringValueSlice(output.Protocols)) + d.Set("security_policy_name", output.SecurityPolicyName) + if output.IdentityProviderDetails != nil { + d.Set("url", output.IdentityProviderDetails.Url) + } else { + d.Set("url", "") } - d.Set("endpoint_type", resp.Server.EndpointType) - d.Set("endpoint_details", flattenTransferServerEndpointDetails(resp.Server.EndpointDetails)) - d.Set("identity_provider_type", resp.Server.IdentityProviderType) - d.Set("logging_role", resp.Server.LoggingRole) - d.Set("host_key_fingerprint", resp.Server.HostKeyFingerprint) - d.Set("security_policy_name", resp.Server.SecurityPolicyName) - tags := keyvaluetags.TransferKeyValueTags(resp.Server.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + tags := keyvaluetags.TransferKeyValueTags(output.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) //lintignore:AWSR002 if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { @@ -313,79 +321,84 @@ func resourceAwsTransferServerRead(d *schema.ResourceData, meta interface{}) err if err := d.Set("tags_all", tags.Map()); err != nil { return fmt.Errorf("error setting tags_all: %w", err) } + return nil } func resourceAwsTransferServerUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).transferconn - updateFlag := false - stopFlag := false - updateOpts := &transfer.UpdateServerInput{ - ServerId: aws.String(d.Id()), - } - if d.HasChange("logging_role") { - updateFlag = true - updateOpts.LoggingRole = aws.String(d.Get("logging_role").(string)) - } + if d.HasChangesExcept("tags", "tags_all") { + stopFlag := false - if d.HasChange("security_policy_name") { - updateFlag = true - updateOpts.SecurityPolicyName = aws.String(d.Get("security_policy_name").(string)) - } + input := &transfer.UpdateServerInput{ + ServerId: aws.String(d.Id()), + } - if d.HasChanges("invocation_role", "url") { - identityProviderDetails := &transfer.IdentityProviderDetails{} - updateFlag = true - if attr, ok := d.GetOk("invocation_role"); ok { - identityProviderDetails.InvocationRole = aws.String(attr.(string)) + if d.HasChange("logging_role") { + input.LoggingRole = aws.String(d.Get("logging_role").(string)) } - if attr, ok := d.GetOk("url"); ok { - identityProviderDetails.Url = aws.String(attr.(string)) + if d.HasChange("security_policy_name") { + input.SecurityPolicyName = aws.String(d.Get("security_policy_name").(string)) } - updateOpts.IdentityProviderDetails = identityProviderDetails - } - if d.HasChange("endpoint_type") { - updateFlag = true - if attr, ok := d.GetOk("endpoint_type"); ok { - updateOpts.EndpointType = aws.String(attr.(string)) + if d.HasChanges("invocation_role", "url") { + identityProviderDetails := &transfer.IdentityProviderDetails{} + + if attr, ok := d.GetOk("invocation_role"); ok { + identityProviderDetails.InvocationRole = aws.String(attr.(string)) + } + + if attr, ok := d.GetOk("url"); ok { + identityProviderDetails.Url = aws.String(attr.(string)) + } + + input.IdentityProviderDetails = identityProviderDetails } - } - if d.HasChange("endpoint_details") { - updateFlag = true - if attr, ok := d.GetOk("endpoint_details"); ok { - updateOpts.EndpointDetails = expandTransferServerEndpointDetails(attr.([]interface{})) + if d.HasChange("endpoint_type") { + input.EndpointType = aws.String(d.Get("endpoint_type").(string)) } - // Prevent the following error: InvalidRequestException: Server must be OFFLINE to change AddressAllocationIds - if d.HasChange("endpoint_details.0.address_allocation_ids") { - stopFlag = true + if d.HasChange("certificate") { + input.Certificate = aws.String(d.Get("certificate").(string)) } - } - if d.HasChange("host_key") { - updateFlag = true - if attr, ok := d.GetOk("host_key"); ok { - updateOpts.HostKey = aws.String(attr.(string)) + if d.HasChange("protocols") { + input.Protocols = expandStringSet(d.Get("protocols").(*schema.Set)) + } + + if d.HasChange("endpoint_details") { + if v, ok := d.GetOk("endpoint_details"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.EndpointDetails = expandTransferEndpointDetails(v.([]interface{})[0].(map[string]interface{})) + } + + // Prevent the following error: InvalidRequestException: Server must be OFFLINE to change AddressAllocationIds + if d.HasChange("endpoint_details.0.address_allocation_ids") { + stopFlag = true + } + } + + if d.HasChange("host_key") { + if attr, ok := d.GetOk("host_key"); ok { + input.HostKey = aws.String(attr.(string)) + } } - } - if updateFlag { if stopFlag { - if err := stopAndWaitForTransferServer(d.Id(), conn, d.Timeout(schema.TimeoutUpdate)); err != nil { + if err := stopTransferServer(conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil { return err } } - if _, err := conn.UpdateServer(updateOpts); err != nil { - return fmt.Errorf("error updating Transfer Server (%s): %w", d.Id(), err) + log.Printf("[DEBUG] Updating Transfer Server: %s", input) + if err := updateTransferServer(conn, input); err != nil { + return err } if stopFlag { - if err := startAndWaitForTransferServer(d.Id(), conn, d.Timeout(schema.TimeoutUpdate)); err != nil { + if err := startTransferServer(conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil { return err } } @@ -394,7 +407,7 @@ func resourceAwsTransferServerUpdate(d *schema.ResourceData, meta interface{}) e if d.HasChange("tags_all") { o, n := d.GetChange("tags_all") if err := keyvaluetags.TransferUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { - return fmt.Errorf("error updating tags: %s", err) + return fmt.Errorf("error updating tags: %w", err) } } @@ -405,222 +418,179 @@ func resourceAwsTransferServerDelete(d *schema.ResourceData, meta interface{}) e conn := meta.(*AWSClient).transferconn if d.Get("force_destroy").(bool) { - log.Printf("[DEBUG] Transfer Server (%s) attempting to forceDestroy", d.Id()) - if err := deleteTransferUsers(conn, d.Id(), nil); err != nil { - return err + input := &transfer.ListUsersInput{ + ServerId: aws.String(d.Id()), } - } + var deletionErrs *multierror.Error - delOpts := &transfer.DeleteServerInput{ - ServerId: aws.String(d.Id()), - } - - log.Printf("[DEBUG] Delete Transfer Server Option: %#v", delOpts) + err := conn.ListUsersPages(input, func(page *transfer.ListUsersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } - _, err := conn.DeleteServer(delOpts) - if err != nil { - if isAWSErr(err, transfer.ErrCodeResourceNotFoundException, "") { - return nil - } - return fmt.Errorf("error deleting Transfer Server (%s): %s", d.Id(), err) - } + for _, user := range page.Users { + resourceID := tftransfer.UserCreateResourceID(d.Id(), aws.StringValue(user.UserName)) - if err := waitForTransferServerDeletion(conn, d.Id()); err != nil { - return fmt.Errorf("error waiting for Transfer Server (%s): %s", d.Id(), err) - } + r := resourceAwsTransferUser() + d := r.Data(nil) + d.SetId(resourceID) + err := r.Delete(d, meta) - return nil -} - -func waitForTransferServerDeletion(conn *transfer.Transfer, serverID string) error { - params := &transfer.DescribeServerInput{ - ServerId: aws.String(serverID), - } + if err != nil { + deletionErrs = multierror.Append(deletionErrs, fmt.Errorf("error deleting Transfer User (%s): %w", resourceID, err)) + continue + } + } - err := resource.Retry(10*time.Minute, func() *resource.RetryError { - _, err := conn.DescribeServer(params) + return !lastPage + }) - if isAWSErr(err, transfer.ErrCodeResourceNotFoundException, "") { - return nil + if err != nil { + deletionErrs = multierror.Append(deletionErrs, fmt.Errorf("error listing Transfer Users: %w", err)) } + err = deletionErrs.ErrorOrNil() + if err != nil { - return resource.NonRetryableError(err) + return err } + } - return resource.RetryableError(fmt.Errorf("Transfer Server (%s) still exists", serverID)) + log.Printf("[DEBUG] Deleting Transfer Server (%s)", d.Id()) + _, err := conn.DeleteServer(&transfer.DeleteServerInput{ + ServerId: aws.String(d.Id()), }) - if isResourceTimeoutError(err) { - _, err = conn.DescribeServer(params) - if isAWSErr(err, transfer.ErrCodeResourceNotFoundException, "") { - return nil - } - if err == nil { - return fmt.Errorf("Transfer server (%s) still exists", serverID) - } - } - if err != nil { - return fmt.Errorf("Error waiting for transfer server deletion: %s", err) - } - return nil -} -func deleteTransferUsers(conn *transfer.Transfer, serverID string, nextToken *string) error { - listOpts := &transfer.ListUsersInput{ - ServerId: aws.String(serverID), - NextToken: nextToken, + if tfawserr.ErrCodeEquals(err, transfer.ErrCodeResourceNotFoundException) { + return nil } - log.Printf("[DEBUG] List Transfer User Option: %#v", listOpts) - - resp, err := conn.ListUsers(listOpts) if err != nil { - return err + return fmt.Errorf("error deleting Transfer Server (%s): %w", d.Id(), err) } - for _, user := range resp.Users { - - delOpts := &transfer.DeleteUserInput{ - ServerId: aws.String(serverID), - UserName: user.UserName, - } - - log.Printf("[DEBUG] Delete Transfer User Option: %#v", delOpts) - - _, err = conn.DeleteUser(delOpts) - if err != nil { - if isAWSErr(err, transfer.ErrCodeResourceNotFoundException, "") { - continue - } - return fmt.Errorf("error deleting Transfer User (%s) for Server(%s): %s", *user.UserName, serverID, err) - } - } + _, err = waiter.ServerDeleted(conn, d.Id()) - if resp.NextToken != nil { - return deleteTransferUsers(conn, serverID, resp.NextToken) + if err != nil { + return fmt.Errorf("error waiting for Transfer Server (%s) delete: %w", d.Id(), err) } return nil } -func expandTransferServerEndpointDetails(l []interface{}) *transfer.EndpointDetails { - if len(l) == 0 || l[0] == nil { +func expandTransferEndpointDetails(tfMap map[string]interface{}) *transfer.EndpointDetails { + if tfMap == nil { return nil } - e := l[0].(map[string]interface{}) - out := &transfer.EndpointDetails{} + apiObject := &transfer.EndpointDetails{} - if v, ok := e["vpc_endpoint_id"].(string); ok && v != "" { - out.VpcEndpointId = aws.String(v) + if v, ok := tfMap["address_allocation_ids"].(*schema.Set); ok && v.Len() > 0 { + apiObject.AddressAllocationIds = expandStringSet(v) } - if v, ok := e["address_allocation_ids"].(*schema.Set); ok && v.Len() > 0 { - out.AddressAllocationIds = expandStringSet(v) + if v, ok := tfMap["subnet_ids"].(*schema.Set); ok && v.Len() > 0 { + apiObject.SubnetIds = expandStringSet(v) } - if v, ok := e["subnet_ids"].(*schema.Set); ok && v.Len() > 0 { - out.SubnetIds = expandStringSet(v) + if v, ok := tfMap["vpc_endpoint_id"].(string); ok && v != "" { + apiObject.VpcEndpointId = aws.String(v) } - if v, ok := e["vpc_id"].(string); ok && v != "" { - out.VpcId = aws.String(v) + if v, ok := tfMap["vpc_id"].(string); ok && v != "" { + apiObject.VpcId = aws.String(v) } - return out + return apiObject } -func flattenTransferServerEndpointDetails(endpointDetails *transfer.EndpointDetails) []interface{} { - if endpointDetails == nil { - return []interface{}{} +func flattenTransferEndpointDetails(apiObject *transfer.EndpointDetails) map[string]interface{} { + if apiObject == nil { + return nil } - e := make(map[string]interface{}) - if endpointDetails.VpcEndpointId != nil { - e["vpc_endpoint_id"] = aws.StringValue(endpointDetails.VpcEndpointId) + tfMap := map[string]interface{}{} + + if v := apiObject.AddressAllocationIds; v != nil { + tfMap["address_allocation_ids"] = aws.StringValueSlice(v) } - if endpointDetails.AddressAllocationIds != nil { - e["address_allocation_ids"] = flattenStringSet(endpointDetails.AddressAllocationIds) + + if v := apiObject.SubnetIds; v != nil { + tfMap["subnet_ids"] = aws.StringValueSlice(v) } - if endpointDetails.SubnetIds != nil { - e["subnet_ids"] = flattenStringSet(endpointDetails.SubnetIds) + + if v := apiObject.VpcEndpointId; v != nil { + tfMap["vpc_endpoint_id"] = aws.StringValue(v) } - if endpointDetails.VpcId != nil { - e["vpc_id"] = aws.StringValue(endpointDetails.VpcId) + + if v := apiObject.VpcId; v != nil { + tfMap["vpc_id"] = aws.StringValue(v) } - return []interface{}{e} + return tfMap } -func stopAndWaitForTransferServer(serverId string, conn *transfer.Transfer, timeout time.Duration) error { - stopReq := &transfer.StopServerInput{ - ServerId: aws.String(serverId), - } - if _, err := conn.StopServer(stopReq); err != nil { - return fmt.Errorf("error stopping Transfer Server (%s): %s", serverId, err) +func stopTransferServer(conn *transfer.Transfer, serverID string, timeout time.Duration) error { + input := &transfer.StopServerInput{ + ServerId: aws.String(serverID), } - stateChangeConf := &resource.StateChangeConf{ - Pending: []string{transfer.StateStarting, transfer.StateOnline, transfer.StateStopping}, - Target: []string{transfer.StateOffline}, - Refresh: refreshTransferServerStatus(conn, serverId), - Timeout: timeout, - Delay: 10 * time.Second, + if _, err := conn.StopServer(input); err != nil { + return fmt.Errorf("error stopping Transfer Server (%s): %w", serverID, err) } - if _, err := stateChangeConf.WaitForState(); err != nil { - return fmt.Errorf("error waiting for Transfer Server (%s) to stop: %s", serverId, err) + if _, err := waiter.ServerStopped(conn, serverID, timeout); err != nil { + return fmt.Errorf("error waiting for Transfer Server (%s) to stop: %w", serverID, err) } return nil } -func startAndWaitForTransferServer(serverId string, conn *transfer.Transfer, timeout time.Duration) error { - stopReq := &transfer.StartServerInput{ - ServerId: aws.String(serverId), - } - - if _, err := conn.StartServer(stopReq); err != nil { - return fmt.Errorf("error starting Transfer Server (%s): %s", serverId, err) +func startTransferServer(conn *transfer.Transfer, serverID string, timeout time.Duration) error { + input := &transfer.StartServerInput{ + ServerId: aws.String(serverID), } - stateChangeConf := &resource.StateChangeConf{ - Pending: []string{transfer.StateStarting, transfer.StateOffline, transfer.StateStopping}, - Target: []string{transfer.StateOnline}, - Refresh: refreshTransferServerStatus(conn, serverId), - Timeout: timeout, - Delay: 10 * time.Second, + if _, err := conn.StartServer(input); err != nil { + return fmt.Errorf("error starting Transfer Server (%s): %w", serverID, err) } - if _, err := stateChangeConf.WaitForState(); err != nil { - return fmt.Errorf("error waiting for Transfer Server (%s) to start: %s", serverId, err) + if _, err := waiter.ServerStarted(conn, serverID, timeout); err != nil { + return fmt.Errorf("error waiting for Transfer Server (%s) to start: %w", serverID, err) } return nil } -func refreshTransferServerStatus(conn *transfer.Transfer, serverId string) resource.StateRefreshFunc { - return func() (interface{}, string, error) { - server, err := describeTransferServer(conn, serverId) +func updateTransferServer(conn *transfer.Transfer, input *transfer.UpdateServerInput) error { + // The Transfer API will return a state of ONLINE for a server before the + // underlying VPC Endpoint is available and attempting to update the server + // will return an error until that EC2 API process is complete: + // ConflictException: VPC Endpoint state is not yet available + // To prevent accessing the EC2 API directly to check the VPC Endpoint + // state, which can require confusing IAM permissions and have other + // eventual consistency consideration, we retry only via the Transfer API. + err := resource.Retry(Ec2VpcEndpointCreationTimeout, func() *resource.RetryError { + _, err := conn.UpdateServer(input) + + if tfawserr.ErrMessageContains(err, transfer.ErrCodeConflictException, "VPC Endpoint state is not yet available") { + return resource.RetryableError(err) + } - if server == nil { - return 42, "destroyed", nil + if err != nil { + return resource.NonRetryableError(err) } - return server, aws.StringValue(server.State), err - } -} + return nil + }) -func describeTransferServer(conn *transfer.Transfer, serverId string) (*transfer.DescribedServer, error) { - params := &transfer.DescribeServerInput{ - ServerId: aws.String(serverId), + if tfresource.TimedOut(err) { + _, err = conn.UpdateServer(input) } - resp, err := conn.DescribeServer(params) - - if resp == nil { - return nil, err + if err != nil { + return fmt.Errorf("error updating Transfer Server (%s): %w", aws.StringValue(input.ServerId), err) } - return resp.Server, err + return nil } diff --git a/aws/resource_aws_transfer_server_test.go b/aws/resource_aws_transfer_server_test.go index 4315dfd5893..c369117b9a2 100644 --- a/aws/resource_aws_transfer_server_test.go +++ b/aws/resource_aws_transfer_server_test.go @@ -7,11 +7,14 @@ import ( "testing" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/service/acmpca" "github.com/aws/aws-sdk-go/service/transfer" + "github.com/hashicorp/go-multierror" "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/transfer/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func init() { @@ -30,25 +33,25 @@ func testSweepTransferServers(region string) error { } conn := client.(*AWSClient).transferconn input := &transfer.ListServersInput{} + var sweeperErrs *multierror.Error err = conn.ListServersPages(input, func(page *transfer.ListServersOutput, lastPage bool) bool { - for _, server := range page.Servers { - id := aws.StringValue(server.ServerId) - input := &transfer.DeleteServerInput{ - ServerId: server.ServerId, - } + if page == nil { + return !lastPage + } - log.Printf("[INFO] Deleting Transfer Server: %s", id) - _, err := conn.DeleteServer(input) + for _, server := range page.Servers { + r := resourceAwsTransferServer() + d := r.Data(nil) + d.SetId(aws.StringValue(server.ServerId)) + d.Set("force_destroy", true) // In lieu of an aws_transfer_user sweeper. + err = r.Delete(d, client) if err != nil { - log.Printf("[ERROR] Error deleting Transfer Server (%s): %s", id, err) + log.Printf("[ERROR] %s", err) + sweeperErrs = multierror.Append(sweeperErrs, err) continue } - - if err := waitForTransferServerDeletion(conn, id); err != nil { - log.Printf("[ERROR] Error waiting for Transfer Server (%s) deletion: %s", id, err) - } } return !lastPage @@ -56,14 +59,14 @@ func testSweepTransferServers(region string) error { if testSweepSkipSweepError(err) { log.Printf("[WARN] Skipping Transfer Server sweep for %s: %s", region, err) - return nil + return sweeperErrs.ErrorOrNil() // In case we have completed some pages, but had errors } if err != nil { - return fmt.Errorf("error listing Transfer Servers: %s", err) + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing Transfer Servers: %w", err)) } - return nil + return sweeperErrs.ErrorOrNil() } func testAccErrorCheckSkipTransfer(t *testing.T) resource.ErrorCheckFunc { @@ -75,6 +78,7 @@ func testAccErrorCheckSkipTransfer(t *testing.T) resource.ErrorCheckFunc { func TestAccAWSTransferServer_basic(t *testing.T) { var conf transfer.DescribedServer resourceName := "aws_transfer_server.test" + iamRoleResourceName := "aws_iam_role.test" rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ @@ -88,11 +92,21 @@ func TestAccAWSTransferServer_basic(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckAWSTransferServerExists(resourceName, &conf), testAccMatchResourceAttrRegionalARN(resourceName, "arn", "transfer", regexp.MustCompile(`server/.+`)), + resource.TestCheckResourceAttr(resourceName, "certificate", ""), testAccMatchResourceAttrRegionalHostname(resourceName, "endpoint", "server.transfer", regexp.MustCompile(`s-[a-z0-9]+`)), - resource.TestCheckResourceAttr(resourceName, "identity_provider_type", "SERVICE_MANAGED"), - resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "endpoint_details.#", "0"), resource.TestCheckResourceAttr(resourceName, "endpoint_type", "PUBLIC"), + resource.TestCheckResourceAttr(resourceName, "force_destroy", "false"), + resource.TestCheckNoResourceAttr(resourceName, "host_key"), + resource.TestCheckResourceAttrSet(resourceName, "host_key_fingerprint"), + resource.TestCheckResourceAttr(resourceName, "identity_provider_type", "SERVICE_MANAGED"), + resource.TestCheckResourceAttr(resourceName, "invocation_role", ""), + resource.TestCheckResourceAttr(resourceName, "logging_role", ""), + resource.TestCheckResourceAttr(resourceName, "protocols.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "protocols.*", "SFTP"), resource.TestCheckResourceAttr(resourceName, "security_policy_name", "TransferSecurityPolicy-2018-11"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "url", ""), ), }, { @@ -105,9 +119,45 @@ func TestAccAWSTransferServer_basic(t *testing.T) { Config: testAccAWSTransferServerUpdatedConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSTransferServerExists(resourceName, &conf), - resource.TestCheckResourceAttrPair(resourceName, "logging_role", "aws_iam_role.test", "arn"), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "transfer", regexp.MustCompile(`server/.+`)), + resource.TestCheckResourceAttr(resourceName, "certificate", ""), + testAccMatchResourceAttrRegionalHostname(resourceName, "endpoint", "server.transfer", regexp.MustCompile(`s-[a-z0-9]+`)), + resource.TestCheckResourceAttr(resourceName, "endpoint_details.#", "0"), + resource.TestCheckResourceAttr(resourceName, "endpoint_type", "PUBLIC"), + resource.TestCheckResourceAttr(resourceName, "force_destroy", "false"), + resource.TestCheckNoResourceAttr(resourceName, "host_key"), + resource.TestCheckResourceAttrSet(resourceName, "host_key_fingerprint"), resource.TestCheckResourceAttr(resourceName, "identity_provider_type", "SERVICE_MANAGED"), + resource.TestCheckResourceAttr(resourceName, "invocation_role", ""), + resource.TestCheckResourceAttrPair(resourceName, "logging_role", iamRoleResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "protocols.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "protocols.*", "SFTP"), + resource.TestCheckResourceAttr(resourceName, "security_policy_name", "TransferSecurityPolicy-2018-11"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "url", ""), + ), + }, + }, + }) +} + +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) }, + ErrorCheck: testAccErrorCheck(t, transfer.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSTransferServerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSTransferServerBasicConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSTransferServerExists(resourceName, &conf), + testAccCheckResourceDisappears(testAccProvider, resourceAwsTransferServer(), resourceName), ), + ExpectNonEmptyPlan: true, }, }, }) @@ -147,7 +197,7 @@ func TestAccAWSTransferServer_securityPolicy(t *testing.T) { }) } -func TestAccAWSTransferServer_Vpc(t *testing.T) { +func TestAccAWSTransferServer_vpc(t *testing.T) { var conf transfer.DescribedServer resourceName := "aws_transfer_server.test" rName := acctest.RandomWithPrefix("tf-acc-test") @@ -159,7 +209,7 @@ func TestAccAWSTransferServer_Vpc(t *testing.T) { CheckDestroy: testAccCheckAWSTransferServerDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSTransferServerConfig_Vpc(rName), + Config: testAccAWSTransferServerVpcConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSTransferServerExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "endpoint_type", "VPC"), @@ -174,7 +224,7 @@ func TestAccAWSTransferServer_Vpc(t *testing.T) { ImportStateVerifyIgnore: []string{"force_destroy"}, }, { - Config: testAccAWSTransferServerConfig_VpcUpdate(rName), + Config: testAccAWSTransferServerVpcUpdateConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSTransferServerExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "endpoint_type", "VPC"), @@ -185,55 +235,140 @@ func TestAccAWSTransferServer_Vpc(t *testing.T) { }) } -func TestAccAWSTransferServer_apigateway(t *testing.T) { +// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/16556 +/* +func TestAccAWSTransferServer_updateEndpointType(t *testing.T) { var conf transfer.DescribedServer resourceName := "aws_transfer_server.test" rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t); testAccPreCheckAWSTransfer(t) }, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t) }, ErrorCheck: testAccErrorCheck(t, transfer.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSTransferServerDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSTransferServerConfig_apigateway(rName), + Config: testAccAWSTransferServerBasicConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSTransferServerExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "endpoint_details.#", "0"), + resource.TestCheckResourceAttr(resourceName, "endpoint_type", "PUBLIC"), + ), + }, + { + Config: testAccAWSTransferServerVpcConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSTransferServerExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "endpoint_type", "VPC"), + resource.TestCheckResourceAttr(resourceName, "endpoint_details.0.subnet_ids.#", "1"), + resource.TestCheckResourceAttr(resourceName, "endpoint_details.0.address_allocation_ids.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"force_destroy"}, + }, + }, + }) +} +*/ + +func TestAccAWSTransferServer_protocols(t *testing.T) { + var s transfer.DescribedServer + var ca acmpca.CertificateAuthority + resourceName := "aws_transfer_server.test" + acmCAResourceName := "aws_acmpca_certificate_authority.test" + acmCertificateResourceName := "aws_acm_certificate.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t); testAccPreCheckAWSTransfer(t) }, + ErrorCheck: testAccErrorCheck(t, transfer.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSTransferServerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSTransferServerProtocolsConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSTransferServerExists(resourceName, &s), + resource.TestCheckResourceAttr(resourceName, "certificate", ""), + resource.TestCheckResourceAttr(resourceName, "endpoint_type", "VPC"), resource.TestCheckResourceAttr(resourceName, "identity_provider_type", "API_GATEWAY"), - resource.TestCheckResourceAttrPair(resourceName, "invocation_role", "aws_iam_role.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "protocols.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "protocols.*", "FTP"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"force_destroy"}, + }, + // We need to create and activate the CA before issuing a certificate. + { + Config: testAccAWSTransferServerConfigRootCA(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(acmCAResourceName, &ca), + testAccCheckAwsAcmpcaCertificateAuthorityActivateCA(&ca), + ), + }, + { + Config: testAccAWSTransferServerProtocolsUpdateConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSTransferServerExists(resourceName, &s), + resource.TestCheckResourceAttrPair(resourceName, "certificate", acmCertificateResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "endpoint_type", "VPC"), + resource.TestCheckResourceAttr(resourceName, "identity_provider_type", "API_GATEWAY"), + resource.TestCheckResourceAttr(resourceName, "protocols.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "protocols.*", "FTP"), + resource.TestCheckTypeSetElemAttr(resourceName, "protocols.*", "FTPS"), + ), + }, + { + Config: testAccAWSTransferServerProtocolsUpdateConfig(rName), + Check: resource.ComposeTestCheckFunc( + // CA must be DISABLED for deletion. + testAccCheckAwsAcmpcaCertificateAuthorityDisableCA(&ca), ), + ExpectNonEmptyPlan: true, }, }, }) } -func TestAccAWSTransferServer_disappears(t *testing.T) { +func TestAccAWSTransferServer_apiGateway(t *testing.T) { var conf transfer.DescribedServer resourceName := "aws_transfer_server.test" + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t) }, + PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t); testAccPreCheckAWSTransfer(t) }, ErrorCheck: testAccErrorCheck(t, transfer.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSTransferServerDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSTransferServerBasicConfig(), + Config: testAccAWSTransferServerApiGatewayIdentityProviderTypeConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSTransferServerExists(resourceName, &conf), - testAccCheckResourceDisappears(testAccProvider, resourceAwsTransferServer(), resourceName), + resource.TestCheckResourceAttr(resourceName, "identity_provider_type", "API_GATEWAY"), + resource.TestCheckResourceAttrPair(resourceName, "invocation_role", "aws_iam_role.test", "arn"), ), - ExpectNonEmptyPlan: true, }, }, }) } -func TestAccAWSTransferServer_forcedestroy(t *testing.T) { - var conf transfer.DescribedServer - var roleConf iam.GetRoleOutput +func TestAccAWSTransferServer_forceDestroy(t *testing.T) { + var s transfer.DescribedServer + var u transfer.DescribedUser + var k transfer.SshPublicKey resourceName := "aws_transfer_server.test" + userResourceName := "aws_transfer_user.test" + sshKeyResourceName := "aws_transfer_ssh_key.test" rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ @@ -243,14 +378,12 @@ func TestAccAWSTransferServer_forcedestroy(t *testing.T) { CheckDestroy: testAccCheckAWSTransferServerDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSTransferServerConfig_forcedestroy(rName), + Config: testAccAWSTransferServerForceDestroyConfig(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSTransferServerExists(resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "identity_provider_type", "SERVICE_MANAGED"), + testAccCheckAWSTransferServerExists(resourceName, &s), + testAccCheckAWSTransferUserExists(userResourceName, &u), + testAccCheckAWSTransferSshKeyExists(sshKeyResourceName, &k), resource.TestCheckResourceAttr(resourceName, "force_destroy", "true"), - testAccCheckAWSRoleExists("aws_iam_role.test", &roleConf), - testAccCheckAWSTransferCreateUser(&conf, &roleConf, rName), - testAccCheckAWSTransferCreateSshKey(&conf, rName), ), }, { @@ -263,26 +396,22 @@ func TestAccAWSTransferServer_forcedestroy(t *testing.T) { }) } -func TestAccAWSTransferServer_vpcEndpointId(t *testing.T) { +func TestAccAWSTransferServer_hostKey(t *testing.T) { var conf transfer.DescribedServer resourceName := "aws_transfer_server.test" - rName := acctest.RandomWithPrefix("tf-acc-test") - - if testAccGetPartition() == "aws-us-gov" { - t.Skip("Transfer Server VPC_ENDPOINT endpoint type is not supported in GovCloud partition") - } + hostKey := "test-fixtures/transfer-ssh-rsa-key" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t) }, + PreCheck: func() { testAccPreCheck(t) }, ErrorCheck: testAccErrorCheck(t, transfer.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSTransferServerDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSTransferServerConfig_VpcEndPoint(rName), + Config: testAccAWSTransferServerHostKeyConfig(hostKey), Check: resource.ComposeTestCheckFunc( testAccCheckAWSTransferServerExists(resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "endpoint_type", "VPC_ENDPOINT"), + resource.TestCheckResourceAttr(resourceName, "host_key_fingerprint", "SHA256:Z2pW9sPKDD/T34tVfCoolsRcECNTlekgaKvDn9t+9sg="), ), }, { @@ -295,22 +424,26 @@ func TestAccAWSTransferServer_vpcEndpointId(t *testing.T) { }) } -func TestAccAWSTransferServer_hostKey(t *testing.T) { +func TestAccAWSTransferServer_vpcEndpointId(t *testing.T) { var conf transfer.DescribedServer resourceName := "aws_transfer_server.test" - hostKey := "test-fixtures/transfer-ssh-rsa-key" + rName := acctest.RandomWithPrefix("tf-acc-test") + + if testAccGetPartition() == "aws-us-gov" { + t.Skip("Transfer Server VPC_ENDPOINT endpoint type is not supported in GovCloud partition") + } resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSTransfer(t) }, ErrorCheck: testAccErrorCheck(t, transfer.EndpointsID), Providers: testAccProviders, CheckDestroy: testAccCheckAWSTransferServerDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSTransferServerConfig_hostKey(hostKey), + Config: testAccAWSTransferServerVpcEndPointConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSTransferServerExists(resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "host_key_fingerprint", "SHA256:Z2pW9sPKDD/T34tVfCoolsRcECNTlekgaKvDn9t+9sg="), + resource.TestCheckResourceAttr(resourceName, "endpoint_type", "VPC_ENDPOINT"), ), }, { @@ -323,7 +456,7 @@ func TestAccAWSTransferServer_hostKey(t *testing.T) { }) } -func testAccCheckAWSTransferServerExists(n string, res *transfer.DescribedServer) resource.TestCheckFunc { +func testAccCheckAWSTransferServerExists(n string, v *transfer.DescribedServer) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -336,15 +469,13 @@ func testAccCheckAWSTransferServerExists(n string, res *transfer.DescribedServer conn := testAccProvider.Meta().(*AWSClient).transferconn - describe, err := conn.DescribeServer(&transfer.DescribeServerInput{ - ServerId: aws.String(rs.Primary.ID), - }) + output, err := finder.ServerByID(conn, rs.Primary.ID) if err != nil { return err } - *res = *describe.Server + *v = *output return nil } @@ -358,56 +489,20 @@ func testAccCheckAWSTransferServerDestroy(s *terraform.State) error { continue } - _, err := conn.DescribeServer(&transfer.DescribeServerInput{ - ServerId: aws.String(rs.Primary.ID), - }) + _, err := finder.ServerByID(conn, rs.Primary.ID) - if isAWSErr(err, transfer.ErrCodeResourceNotFoundException, "") { + if tfresource.NotFound(err) { continue } - if err == nil { - return fmt.Errorf("Transfer Server (%s) still exists", rs.Primary.ID) - } - } - - return nil -} - -func testAccCheckAWSTransferCreateUser(describedServer *transfer.DescribedServer, getRoleOutput *iam.GetRoleOutput, userName string) resource.TestCheckFunc { - return func(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).transferconn - - input := &transfer.CreateUserInput{ - ServerId: describedServer.ServerId, - UserName: aws.String(userName), - Role: getRoleOutput.Role.Arn, - } - - if _, err := conn.CreateUser(input); err != nil { - return fmt.Errorf("error creating Transfer User (%s) on Server (%s): %s", userName, aws.StringValue(describedServer.ServerId), err) + if err != nil { + return err } - return nil + return fmt.Errorf("Transfer Server %s still exists", rs.Primary.ID) } -} -func testAccCheckAWSTransferCreateSshKey(describedServer *transfer.DescribedServer, userName string) resource.TestCheckFunc { - return func(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).transferconn - - input := &transfer.ImportSshPublicKeyInput{ - ServerId: describedServer.ServerId, - UserName: aws.String(userName), - SshPublicKeyBody: aws.String("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3F6tyPEFEzV0LX3X8BsXdMsQz1x2cEikKDEY0aIj41qgxMCP/iteneqXSIFZBp5vizPvaoIR3Um9xK7PGoW8giupGn+EPuxIA4cDM4vzOqOkiMPhz5XK0whEjkVzTo4+S0puvDZuwIsdiW9mxhJc7tgBNL0cYlWSYVkz4G/fslNfRPW5mYAM49f4fhtxPb5ok4Q2Lg9dPKVHO/Bgeu5woMc7RY0p1ej6D4CKFE6lymSDJpW0YHX/wqE9+cfEauh7xZcG0q9t2ta6F6fmX0agvpFyZo8aFbXeUBr7osSCJNgvavWbM/06niWrOvYX2xwWdhXmXSrbX8ZbabVohBK41 phodgson@thoughtworks.com"), - } - - if _, err := conn.ImportSshPublicKey(input); err != nil { - return fmt.Errorf("error creating Transfer SSH Public Key for (%s/%s): %s", userName, aws.StringValue(describedServer.ServerId), err) - } - - return nil - } + return nil } func testAccPreCheckAWSTransfer(t *testing.T) { @@ -426,21 +521,61 @@ func testAccPreCheckAWSTransfer(t *testing.T) { } } -func testAccAWSTransferServerBasicConfig() string { - return ` -resource "aws_transfer_server" "test" {} -` +func testAccAWSTransferServerConfigBaseVpc(rName string) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + + tags = { + Name = %[1]q + } } -func testAccAWSTransferServerSecurityPolicyConfig(policy string) string { - return fmt.Sprintf(` -resource "aws_transfer_server" "test" { - security_policy_name = %[1]q +resource "aws_internet_gateway" "test" { + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } } -`, policy) + +resource "aws_subnet" "test" { + vpc_id = aws_vpc.test.id + cidr_block = "10.0.0.0/24" + map_public_ip_on_launch = true + + tags = { + Name = %[1]q + } + + depends_on = [aws_internet_gateway.test] } -func testAccAWSTransferServerUpdatedConfig(rName string) string { +resource "aws_default_route_table" "test" { + default_route_table_id = aws_vpc.test.default_route_table_id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.test.id + } + + tags = { + Name = %[1]q + } +} + +resource "aws_security_group" "test" { + name = %[1]q + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } +} +`, rName) +} + +func testAccAWSTransferServerConfigBaseLoggingRole(rName string) string { return fmt.Sprintf(` resource "aws_iam_role" "test" { name = %[1]q @@ -477,15 +612,10 @@ resource "aws_iam_role_policy" "test" { } POLICY } - -resource "aws_transfer_server" "test" { - identity_provider_type = "SERVICE_MANAGED" - logging_role = aws_iam_role.test.arn -} `, rName) } -func testAccAWSTransferServerConfig_apigateway(rName string) string { +func testAccAWSTransferServerConfigBaseApiGateway(rName string) string { return fmt.Sprintf(` resource "aws_api_gateway_rest_api" "test" { name = %[1]q @@ -540,53 +670,49 @@ resource "aws_api_gateway_deployment" "test" { "a" = "2" } } +`, rName) +} -resource "aws_iam_role" "test" { - name = %[1]q +func testAccAWSTransferServerBasicConfig() string { + return ` +resource "aws_transfer_server" "test" {} +` +} - assume_role_policy = <