diff --git a/aws/provider.go b/aws/provider.go index 30586781738b..6adf2138b6d5 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -466,6 +466,7 @@ func Provider() terraform.ResourceProvider { "aws_glacier_vault": resourceAwsGlacierVault(), "aws_glacier_vault_lock": resourceAwsGlacierVaultLock(), "aws_globalaccelerator_accelerator": resourceAwsGlobalAcceleratorAccelerator(), + "aws_globalaccelerator_listener": resourceAwsGlobalAcceleratorListener(), "aws_glue_catalog_database": resourceAwsGlueCatalogDatabase(), "aws_glue_catalog_table": resourceAwsGlueCatalogTable(), "aws_glue_classifier": resourceAwsGlueClassifier(), diff --git a/aws/resource_aws_globalaccelerator_listener.go b/aws/resource_aws_globalaccelerator_listener.go new file mode 100644 index 000000000000..a329faa76013 --- /dev/null +++ b/aws/resource_aws_globalaccelerator_listener.go @@ -0,0 +1,261 @@ +package aws + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/globalaccelerator" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +func resourceAwsGlobalAcceleratorListener() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsGlobalAcceleratorListenerCreate, + Read: resourceAwsGlobalAcceleratorListenerRead, + Update: resourceAwsGlobalAcceleratorListenerUpdate, + Delete: resourceAwsGlobalAcceleratorListenerDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "accelerator_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "client_affinity": { + Type: schema.TypeString, + Optional: true, + Default: globalaccelerator.ClientAffinityNone, + ValidateFunc: validation.StringInSlice([]string{ + globalaccelerator.ClientAffinityNone, + globalaccelerator.ClientAffinitySourceIp, + }, false), + }, + "protocol": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + globalaccelerator.ProtocolTcp, + globalaccelerator.ProtocolUdp, + }, false), + }, + "port_range": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + MaxItems: 10, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "from_port": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(0, 65535), + }, + "to_port": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(0, 65535), + }, + }, + }, + }, + }, + } +} + +func resourceAwsGlobalAcceleratorListenerCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).globalacceleratorconn + + opts := &globalaccelerator.CreateListenerInput{ + AcceleratorArn: aws.String(d.Get("accelerator_arn").(string)), + ClientAffinity: aws.String(d.Get("client_affinity").(string)), + IdempotencyToken: aws.String(resource.UniqueId()), + Protocol: aws.String(d.Get("protocol").(string)), + PortRanges: resourceAwsGlobalAcceleratorListenerExpandPortRanges(d.Get("port_range").(*schema.Set).List()), + } + + log.Printf("[DEBUG] Create Global Accelerator listener: %s", opts) + + resp, err := conn.CreateListener(opts) + if err != nil { + return fmt.Errorf("Error creating Global Accelerator listener: %s", err) + } + + d.SetId(*resp.Listener.ListenerArn) + + // Creating a listener triggers the accelerator to change status to InPending + stateConf := &resource.StateChangeConf{ + Pending: []string{globalaccelerator.AcceleratorStatusInProgress}, + Target: []string{globalaccelerator.AcceleratorStatusDeployed}, + Refresh: resourceAwsGlobalAcceleratorAcceleratorStateRefreshFunc(conn, d.Get("accelerator_arn").(string)), + Timeout: 5 * time.Minute, + } + + log.Printf("[DEBUG] Waiting for Global Accelerator listener (%s) availability", d.Id()) + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for Global Accelerator listener (%s) availability: %s", d.Id(), err) + } + + return resourceAwsGlobalAcceleratorListenerRead(d, meta) +} + +func resourceAwsGlobalAcceleratorListenerRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).globalacceleratorconn + + listener, err := resourceAwsGlobalAcceleratorListenerRetrieve(conn, d.Id()) + + if err != nil { + return fmt.Errorf("Error reading Global Accelerator listener: %s", err) + } + + if listener == nil { + log.Printf("[WARN] Global Accelerator listener (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + acceleratorArn, err := resourceAwsGlobalAcceleratorListenerParseAcceleratorArn(d.Id()) + + if err != nil { + return err + } + + d.Set("accelerator_arn", acceleratorArn) + d.Set("client_affinity", listener.ClientAffinity) + d.Set("protocol", listener.Protocol) + if err := d.Set("port_range", resourceAwsGlobalAcceleratorListenerFlattenPortRanges(listener.PortRanges)); err != nil { + return fmt.Errorf("error setting port_range: %s", err) + } + + return nil +} + +func resourceAwsGlobalAcceleratorListenerParseAcceleratorArn(listenerArn string) (string, error) { + parts := strings.Split(listenerArn, "/") + if len(parts) < 4 { + return "", fmt.Errorf("Unable to parse accelerator ARN from %s", listenerArn) + } + return strings.Join(parts[0:2], "/"), nil +} + +func resourceAwsGlobalAcceleratorListenerExpandPortRanges(portRanges []interface{}) []*globalaccelerator.PortRange { + out := make([]*globalaccelerator.PortRange, len(portRanges)) + + for i, raw := range portRanges { + portRange := raw.(map[string]interface{}) + m := globalaccelerator.PortRange{} + + m.FromPort = aws.Int64(int64(portRange["from_port"].(int))) + m.ToPort = aws.Int64(int64(portRange["to_port"].(int))) + + out[i] = &m + } + + return out +} + +func resourceAwsGlobalAcceleratorListenerFlattenPortRanges(portRanges []*globalaccelerator.PortRange) []interface{} { + out := make([]interface{}, len(portRanges)) + + for i, portRange := range portRanges { + m := make(map[string]interface{}) + + m["from_port"] = aws.Int64Value(portRange.FromPort) + m["to_port"] = aws.Int64Value(portRange.ToPort) + + out[i] = m + } + + return out +} + +func resourceAwsGlobalAcceleratorListenerRetrieve(conn *globalaccelerator.GlobalAccelerator, listenerArn string) (*globalaccelerator.Listener, error) { + resp, err := conn.DescribeListener(&globalaccelerator.DescribeListenerInput{ + ListenerArn: aws.String(listenerArn), + }) + + if err != nil { + if isAWSErr(err, globalaccelerator.ErrCodeListenerNotFoundException, "") { + return nil, nil + } + return nil, err + } + + return resp.Listener, nil +} + +func resourceAwsGlobalAcceleratorListenerUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).globalacceleratorconn + + opts := &globalaccelerator.UpdateListenerInput{ + ClientAffinity: aws.String(d.Get("client_affinity").(string)), + ListenerArn: aws.String(d.Id()), + Protocol: aws.String(d.Get("protocol").(string)), + PortRanges: resourceAwsGlobalAcceleratorListenerExpandPortRanges(d.Get("port_range").(*schema.Set).List()), + } + + log.Printf("[DEBUG] Update Global Accelerator listener: %s", opts) + + _, err := conn.UpdateListener(opts) + if err != nil { + return fmt.Errorf("Error updating Global Accelerator listener: %s", err) + } + + // Creating a listener triggers the accelerator to change status to InPending + stateConf := &resource.StateChangeConf{ + Pending: []string{globalaccelerator.AcceleratorStatusInProgress}, + Target: []string{globalaccelerator.AcceleratorStatusDeployed}, + Refresh: resourceAwsGlobalAcceleratorAcceleratorStateRefreshFunc(conn, d.Get("accelerator_arn").(string)), + Timeout: 5 * time.Minute, + } + + log.Printf("[DEBUG] Waiting for Global Accelerator listener (%s) availability", d.Id()) + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for Global Accelerator listener (%s) availability: %s", d.Id(), err) + } + + return resourceAwsGlobalAcceleratorListenerRead(d, meta) +} + +func resourceAwsGlobalAcceleratorListenerDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).globalacceleratorconn + + opts := &globalaccelerator.DeleteListenerInput{ + ListenerArn: aws.String(d.Id()), + } + + _, err := conn.DeleteListener(opts) + if err != nil { + if isAWSErr(err, globalaccelerator.ErrCodeListenerNotFoundException, "") { + return nil + } + return fmt.Errorf("Error deleting Global Accelerator listener: %s", err) + } + + // Deleting a listener triggers the accelerator to change status to InPending + stateConf := &resource.StateChangeConf{ + Pending: []string{globalaccelerator.AcceleratorStatusInProgress}, + Target: []string{globalaccelerator.AcceleratorStatusDeployed}, + Refresh: resourceAwsGlobalAcceleratorAcceleratorStateRefreshFunc(conn, d.Get("accelerator_arn").(string)), + Timeout: 5 * time.Minute, + } + + log.Printf("[DEBUG] Waiting for Global Accelerator listener (%s) deletion", d.Id()) + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for Global Accelerator listener (%s) deletion: %s", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_globalaccelerator_listener_test.go b/aws/resource_aws_globalaccelerator_listener_test.go new file mode 100644 index 000000000000..8ab842cc7429 --- /dev/null +++ b/aws/resource_aws_globalaccelerator_listener_test.go @@ -0,0 +1,159 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAwsGlobalAcceleratorListener_basic(t *testing.T) { + resourceName := "aws_globalaccelerator_listener.example" + rInt := acctest.RandInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGlobalAcceleratorListenerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGlobalAcceleratorListener_basic(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckGlobalAcceleratorListenerExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "client_affinity", "NONE"), + resource.TestCheckResourceAttr(resourceName, "protocol", "TCP"), + resource.TestCheckResourceAttr(resourceName, "port_range.#", "1"), + resource.TestCheckResourceAttr(resourceName, "port_range.3309144275.from_port", "80"), + resource.TestCheckResourceAttr(resourceName, "port_range.3309144275.to_port", "81"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsGlobalAcceleratorListener_update(t *testing.T) { + resourceName := "aws_globalaccelerator_listener.example" + rInt := acctest.RandInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGlobalAcceleratorListenerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGlobalAcceleratorListener_basic(rInt), + }, + { + Config: testAccGlobalAcceleratorListener_update(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckGlobalAcceleratorListenerExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "client_affinity", "SOURCE_IP"), + resource.TestCheckResourceAttr(resourceName, "protocol", "UDP"), + resource.TestCheckResourceAttr(resourceName, "port_range.#", "1"), + resource.TestCheckResourceAttr(resourceName, "port_range.3922064764.from_port", "443"), + resource.TestCheckResourceAttr(resourceName, "port_range.3922064764.to_port", "444"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckGlobalAcceleratorListenerExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).globalacceleratorconn + + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + accelerator, err := resourceAwsGlobalAcceleratorListenerRetrieve(conn, rs.Primary.ID) + if err != nil { + return err + } + + if accelerator == nil { + return fmt.Errorf("Global Accelerator listener not found") + } + + return nil + } +} + +func testAccCheckGlobalAcceleratorListenerDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).globalacceleratorconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_globalaccelerator_listener" { + continue + } + + accelerator, err := resourceAwsGlobalAcceleratorListenerRetrieve(conn, rs.Primary.ID) + if err != nil { + return err + } + + if accelerator != nil { + return fmt.Errorf("Global Accelerator listener still exists") + } + } + return nil +} + +func testAccGlobalAcceleratorListener_basic(rInt int) string { + return fmt.Sprintf(` +resource "aws_globalaccelerator_accelerator" "example" { + name = "tf-%d" + ip_address_type = "IPV4" + enabled = false +} + +resource "aws_globalaccelerator_listener" "example" { + accelerator_arn = "${aws_globalaccelerator_accelerator.example.id}" + protocol = "TCP" + + port_range { + from_port = 80 + to_port = 81 + } +} +`, rInt) +} + +func testAccGlobalAcceleratorListener_update(rInt int) string { + return fmt.Sprintf(` +resource "aws_globalaccelerator_accelerator" "example" { + name = "tf-%d" + ip_address_type = "IPV4" + enabled = false +} + +resource "aws_globalaccelerator_listener" "example" { + accelerator_arn = "${aws_globalaccelerator_accelerator.example.id}" + client_affinity = "SOURCE_IP" + protocol = "UDP" + + port_range { + from_port = 443 + to_port = 444 + } + } + +`, rInt) +} diff --git a/website/aws.erb b/website/aws.erb index 6f4803bf0b5b..3766ccfdef4a 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1463,6 +1463,11 @@ aws_globalaccelerator_accelerator + > diff --git a/website/docs/r/globalaccelerator_listener.markdown b/website/docs/r/globalaccelerator_listener.markdown new file mode 100644 index 000000000000..58c7dab068e6 --- /dev/null +++ b/website/docs/r/globalaccelerator_listener.markdown @@ -0,0 +1,66 @@ +--- +layout: "aws" +page_title: "AWS: aws_globalaccelerator_listener" +sidebar_current: "docs-aws-resource-globalaccelerator-listener" +description: |- + Provides a Global Accelerator listener. +--- + +# aws_globalaccelerator_listener + +Provides a Global Accelerator listener. + +## Example Usage + +```hcl +resource "aws_globalaccelerator_accelerator" "example" { + name = "Example" + ip_address_type = "IPV4" + enabled = true + + attributes { + flow_logs_enabled = true + flow_logs_s3_bucket = "example-bucket" + flow_logs_s3_prefix = "flow-logs/" + } +} + +resource "aws_globalaccelerator_listener" "example" { + accelerator_arn = "${aws_globalaccelerator_accelerator.example.id}" + client_affinity = "SOURCE_IP" + protocol = "TCP" + + port_range { + from_port = 80 + to_port = 80 + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `accelerator_arn` - (Required) The Amazon Resource Name (ARN) of your accelerator. +* `client_affinity` - (Optional) Direct all requests from a user to the same endpoint. Valid values are `NONE`, `SOURCE_IP`. Default: `NONE`. If `NONE`, Global Accelerator uses the "five-tuple" properties of source IP address, source port, destination IP address, destination port, and protocol to select the hash value. If `SOURCE_IP`, Global Accelerator uses the "two-tuple" properties of source (client) IP address and destination IP address to select the hash value. +* `protocol` - (Optional) The protocol for the connections from clients to the accelerator. Valid values are `TCP`, `UDP`. +* `port_range` - (Optional) The list of port ranges for the connections from clients to the accelerator. Fields documented below. + +**port_range** supports the following attributes: + +* `from_port` - (Optional) The first port in the range of ports, inclusive. +* `to_port` - (Optional) The last port in the range of ports, inclusive. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The Amazon Resource Name (ARN) of the listener. + +## Import + +Global Accelerator listeners can be imported using the `id`, e.g. + +``` +$ terraform import aws_globalaccelerator_listener.example arn:aws:globalaccelerator::111111111111:accelerator/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/listener/xxxxxxxx +```