diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 32932a9b0..e8e6d12bd 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -104,6 +104,7 @@ func Provider() terraform.ResourceProvider { "aws_main_route_table_association": resourceAwsMainRouteTableAssociation(), "aws_network_acl": resourceAwsNetworkAcl(), "aws_network_interface": resourceAwsNetworkInterface(), + "aws_proxy_protocol_policy": resourceAwsProxyProtocolPolicy(), "aws_route53_record": resourceAwsRoute53Record(), "aws_route53_zone": resourceAwsRoute53Zone(), "aws_route_table": resourceAwsRouteTable(), diff --git a/builtin/providers/aws/resource_aws_elb.go b/builtin/providers/aws/resource_aws_elb.go index 21d3b4ebc..c2af95931 100644 --- a/builtin/providers/aws/resource_aws_elb.go +++ b/builtin/providers/aws/resource_aws_elb.go @@ -254,7 +254,7 @@ func resourceAwsElbRead(d *schema.ResourceData, meta interface{}) error { describeResp, err := elbconn.DescribeLoadBalancers(describeElbOpts) if err != nil { - if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "LoadBalancerNotFound" { + if isLoadBalancerNotFound(err) { // The ELB is gone now, so just remove it from the state d.SetId("") return nil @@ -271,7 +271,7 @@ func resourceAwsElbRead(d *schema.ResourceData, meta interface{}) error { } describeAttrsResp, err := elbconn.DescribeLoadBalancerAttributes(describeAttrsOpts) if err != nil { - if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "LoadBalancerNotFound" { + if isLoadBalancerNotFound(err) { // The ELB is gone now, so just remove it from the state d.SetId("") return nil @@ -517,3 +517,8 @@ func resourceAwsElbListenerHash(v interface{}) int { return hashcode.String(buf.String()) } + +func isLoadBalancerNotFound(err error) bool { + elberr, ok := err.(aws.APIError) + return ok && elberr.Code == "LoadBalancerNotFound" +} diff --git a/builtin/providers/aws/resource_aws_proxy_protocol_policy.go b/builtin/providers/aws/resource_aws_proxy_protocol_policy.go new file mode 100644 index 000000000..bdd5a8a5a --- /dev/null +++ b/builtin/providers/aws/resource_aws_proxy_protocol_policy.go @@ -0,0 +1,269 @@ +package aws + +import ( + "fmt" + "log" + "strconv" + "strings" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/elb" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsProxyProtocolPolicy() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsProxyProtocolPolicyCreate, + Read: resourceAwsProxyProtocolPolicyRead, + Update: resourceAwsProxyProtocolPolicyUpdate, + Delete: resourceAwsProxyProtocolPolicyDelete, + + Schema: map[string]*schema.Schema{ + "load_balancer": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "instance_ports": &schema.Schema{ + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + }, + } +} + +func resourceAwsProxyProtocolPolicyCreate(d *schema.ResourceData, meta interface{}) error { + elbconn := meta.(*AWSClient).elbconn + elbname := aws.String(d.Get("load_balancer").(string)) + + input := &elb.CreateLoadBalancerPolicyInput{ + LoadBalancerName: elbname, + PolicyAttributes: []*elb.PolicyAttribute{ + &elb.PolicyAttribute{ + AttributeName: aws.String("ProxyProtocol"), + AttributeValue: aws.String("True"), + }, + }, + PolicyName: aws.String("TFEnableProxyProtocol"), + PolicyTypeName: aws.String("ProxyProtocolPolicyType"), + } + + // Create a policy + log.Printf("[DEBUG] ELB create a policy %s from policy type %s", + *input.PolicyName, *input.PolicyTypeName) + + if _, err := elbconn.CreateLoadBalancerPolicy(input); err != nil { + return fmt.Errorf("Error creating a policy %s: %s", + *input.PolicyName, err) + } + + // Assign the policy name for use later + d.Partial(true) + d.SetId(fmt.Sprintf("%s:%s", *elbname, *input.PolicyName)) + d.SetPartial("load_balancer") + log.Printf("[INFO] ELB PolicyName: %s", *input.PolicyName) + + return resourceAwsProxyProtocolPolicyUpdate(d, meta) +} + +func resourceAwsProxyProtocolPolicyRead(d *schema.ResourceData, meta interface{}) error { + elbconn := meta.(*AWSClient).elbconn + elbname := aws.String(d.Get("load_balancer").(string)) + + // Retrieve the current ELB policies for updating the state + req := &elb.DescribeLoadBalancersInput{ + LoadBalancerNames: []*string{elbname}, + } + resp, err := elbconn.DescribeLoadBalancers(req) + if err != nil { + if isLoadBalancerNotFound(err) { + // The ELB is gone now, so just remove it from the state + d.SetId("") + return nil + } + return fmt.Errorf("Error retrieving ELB attributes: %s", err) + } + + backends := flattenBackendPolicies(resp.LoadBalancerDescriptions[0].BackendServerDescriptions) + + ports := []*string{} + for ip := range backends { + ipstr := strconv.Itoa(int(ip)) + ports = append(ports, &ipstr) + } + d.Set("instance_ports", ports) + d.Set("load_balancer", *elbname) + return nil +} + +func resourceAwsProxyProtocolPolicyUpdate(d *schema.ResourceData, meta interface{}) error { + elbconn := meta.(*AWSClient).elbconn + elbname := aws.String(d.Get("load_balancer").(string)) + + // Retrieve the current ELB policies for updating the state + req := &elb.DescribeLoadBalancersInput{ + LoadBalancerNames: []*string{elbname}, + } + resp, err := elbconn.DescribeLoadBalancers(req) + if err != nil { + if isLoadBalancerNotFound(err) { + // The ELB is gone now, so just remove it from the state + d.SetId("") + return nil + } + return fmt.Errorf("Error retrieving ELB attributes: %s", err) + } + + backends := flattenBackendPolicies(resp.LoadBalancerDescriptions[0].BackendServerDescriptions) + _, policyName := resourceAwsProxyProtocolPolicyParseId(d.Id()) + + d.Partial(true) + if d.HasChange("instance_ports") { + o, n := d.GetChange("instance_ports") + os := o.(*schema.Set) + ns := n.(*schema.Set) + remove := os.Difference(ns).List() + add := ns.Difference(os).List() + + inputs := []*elb.SetLoadBalancerPoliciesForBackendServerInput{} + + i, err := resourceAwsProxyProtocolPolicyRemove(policyName, remove, backends) + if err != nil { + return err + } + inputs = append(inputs, i...) + + i, err = resourceAwsProxyProtocolPolicyAdd(policyName, add, backends) + if err != nil { + return err + } + inputs = append(inputs, i...) + + for _, input := range inputs { + input.LoadBalancerName = elbname + if _, err := elbconn.SetLoadBalancerPoliciesForBackendServer(input); err != nil { + return fmt.Errorf("Error setting policy for backend: %s", err) + } + } + + d.SetPartial("instance_ports") + } + + return resourceAwsProxyProtocolPolicyRead(d, meta) +} + +func resourceAwsProxyProtocolPolicyDelete(d *schema.ResourceData, meta interface{}) error { + elbconn := meta.(*AWSClient).elbconn + elbname := aws.String(d.Get("load_balancer").(string)) + + // Retrieve the current ELB policies for updating the state + req := &elb.DescribeLoadBalancersInput{ + LoadBalancerNames: []*string{elbname}, + } + resp, err := elbconn.DescribeLoadBalancers(req) + if err != nil { + if isLoadBalancerNotFound(err) { + // The ELB is gone now, so just remove it from the state + d.SetId("") + return nil + } + return fmt.Errorf("Error retrieving ELB attributes: %s", err) + } + + backends := flattenBackendPolicies(resp.LoadBalancerDescriptions[0].BackendServerDescriptions) + ports := d.Get("instance_ports").(*schema.Set).List() + _, policyName := resourceAwsProxyProtocolPolicyParseId(d.Id()) + + inputs, err := resourceAwsProxyProtocolPolicyRemove(policyName, ports, backends) + if err != nil { + return fmt.Errorf("Error detaching a policy from backend: %s", err) + } + for _, input := range inputs { + input.LoadBalancerName = elbname + if _, err := elbconn.SetLoadBalancerPoliciesForBackendServer(input); err != nil { + return fmt.Errorf("Error setting policy for backend: %s", err) + } + } + + pOpt := &elb.DeleteLoadBalancerPolicyInput{ + LoadBalancerName: elbname, + PolicyName: aws.String(policyName), + } + if _, err := elbconn.DeleteLoadBalancerPolicy(pOpt); err != nil { + return fmt.Errorf("Error removing a policy from load balancer: %s", err) + } + + return nil +} + +func resourceAwsProxyProtocolPolicyRemove(policyName string, ports []interface{}, backends map[int64][]string) ([]*elb.SetLoadBalancerPoliciesForBackendServerInput, error) { + inputs := make([]*elb.SetLoadBalancerPoliciesForBackendServerInput, 0, len(ports)) + for _, p := range ports { + ip, err := strconv.ParseInt(p.(string), 10, 64) + if err != nil { + return nil, fmt.Errorf("Error detaching the policy: %s", err) + } + + newPolicies := []*string{} + curPolicies, found := backends[ip] + if !found { + // No policy for this instance port found, just skip it. + continue + } + + for _, policy := range curPolicies { + if policy == policyName { + // remove the policy + continue + } + newPolicies = append(newPolicies, &policy) + } + + inputs = append(inputs, &elb.SetLoadBalancerPoliciesForBackendServerInput{ + InstancePort: &ip, + PolicyNames: newPolicies, + }) + } + return inputs, nil +} + +func resourceAwsProxyProtocolPolicyAdd(policyName string, ports []interface{}, backends map[int64][]string) ([]*elb.SetLoadBalancerPoliciesForBackendServerInput, error) { + inputs := make([]*elb.SetLoadBalancerPoliciesForBackendServerInput, 0, len(ports)) + for _, p := range ports { + ip, err := strconv.ParseInt(p.(string), 10, 64) + if err != nil { + return nil, fmt.Errorf("Error attaching the policy: %s", err) + } + + newPolicies := []*string{} + curPolicies := backends[ip] + for _, p := range curPolicies { + if p == policyName { + // Just remove it for now. It will be back later. + continue + } else { + newPolicies = append(newPolicies, &p) + } + } + newPolicies = append(newPolicies, aws.String(policyName)) + + inputs = append(inputs, &elb.SetLoadBalancerPoliciesForBackendServerInput{ + InstancePort: &ip, + PolicyNames: newPolicies, + }) + } + return inputs, nil +} + +// resourceAwsProxyProtocolPolicyParseId takes an ID and parses it into +// it's constituent parts. You need two axes (LB name, policy name) +// to create or identify a proxy protocol policy in AWS's API. +func resourceAwsProxyProtocolPolicyParseId(id string) (string, string) { + parts := strings.SplitN(id, ":", 2) + return parts[0], parts[1] +} diff --git a/builtin/providers/aws/resource_aws_proxy_protocol_policy_test.go b/builtin/providers/aws/resource_aws_proxy_protocol_policy_test.go new file mode 100644 index 000000000..d03cfd7eb --- /dev/null +++ b/builtin/providers/aws/resource_aws_proxy_protocol_policy_test.go @@ -0,0 +1,103 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSProxyProtocolPolicy(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckProxyProtocolPolicyDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccProxyProtocolPolicyConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "aws_proxy_protocol_policy.smtp", "load_balancer", "test-lb"), + resource.TestCheckResourceAttr( + "aws_proxy_protocol_policy.smtp", "instance_ports.#", "1"), + resource.TestCheckResourceAttr( + "aws_proxy_protocol_policy.smtp", "instance_ports.4196041389", "25"), + ), + }, + resource.TestStep{ + Config: testAccProxyProtocolPolicyConfigUpdate, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "aws_proxy_protocol_policy.smtp", "load_balancer", "test-lb"), + resource.TestCheckResourceAttr( + "aws_proxy_protocol_policy.smtp", "instance_ports.#", "2"), + resource.TestCheckResourceAttr( + "aws_proxy_protocol_policy.smtp", "instance_ports.4196041389", "25"), + resource.TestCheckResourceAttr( + "aws_proxy_protocol_policy.smtp", "instance_ports.1925441437", "587"), + ), + }, + }, + }) +} + +func testAccCheckProxyProtocolPolicyDestroy(s *terraform.State) error { + if len(s.RootModule().Resources) > 0 { + return fmt.Errorf("Expected all resources to be gone, but found: %#v", s.RootModule().Resources) + } + + return nil +} + +const testAccProxyProtocolPolicyConfig = ` +resource "aws_elb" "lb" { + name = "test-lb" + availability_zones = ["us-west-2a"] + + listener { + instance_port = 25 + instance_protocol = "tcp" + lb_port = 25 + lb_protocol = "tcp" + } + + listener { + instance_port = 587 + instance_protocol = "tcp" + lb_port = 587 + lb_protocol = "tcp" + } +} + +resource "aws_proxy_protocol_policy" "smtp" { + load_balancer = "${aws_elb.lb.name}" + instance_ports = ["25"] +} +` + +const testAccProxyProtocolPolicyConfigUpdate = ` +resource "aws_elb" "lb" { + name = "test-lb" + availability_zones = ["us-west-2a"] + + listener { + instance_port = 25 + instance_protocol = "tcp" + lb_port = 25 + lb_protocol = "tcp" + } + + listener { + instance_port = 587 + instance_protocol = "tcp" + lb_port = 587 + lb_protocol = "tcp" + } +} + +resource "aws_proxy_protocol_policy" "smtp" { + load_balancer = "${aws_elb.lb.name}" + instance_ports = ["25", "587"] +} +` diff --git a/builtin/providers/aws/structure.go b/builtin/providers/aws/structure.go index 2f22d6e3a..533780037 100644 --- a/builtin/providers/aws/structure.go +++ b/builtin/providers/aws/structure.go @@ -2,6 +2,7 @@ package aws import ( "fmt" + "sort" "strings" "github.com/awslabs/aws-sdk-go/aws" @@ -170,6 +171,18 @@ func expandInstanceString(list []interface{}) []*elb.Instance { return result } +// Flattens an array of Backend Descriptions into a a map of instance_port to policy names. +func flattenBackendPolicies(backends []*elb.BackendServerDescription) map[int64][]string { + policies := make(map[int64][]string) + for _, i := range backends { + for _, p := range i.PolicyNames { + policies[*i.InstancePort] = append(policies[*i.InstancePort], *p) + } + sort.Strings(policies[*i.InstancePort]) + } + return policies +} + // Flattens an array of Listeners into a []map[string]interface{} func flattenListeners(list []*elb.ListenerDescription) []map[string]interface{} { result := make([]map[string]interface{}, 0, len(list)) diff --git a/website/source/docs/providers/aws/r/proxy_protocol_policy.html.markdown b/website/source/docs/providers/aws/r/proxy_protocol_policy.html.markdown new file mode 100644 index 000000000..8781f8ca6 --- /dev/null +++ b/website/source/docs/providers/aws/r/proxy_protocol_policy.html.markdown @@ -0,0 +1,55 @@ +--- +layout: "aws" +page_title: "AWS: aws_proxy_protocol_policy" +sidebar_current: "docs-aws-proxy-protocol-policy" +description: |- + Provides a proxy protocol policy, which allows an ELB to carry a client connection information to a backend. +--- + +# aws\_proxy\_protocol\_policy + +Provides a proxy protocol policy, which allows an ELB to carry a client connection information to a backend. + +## Example Usage + +``` +resource "aws_elb" "lb" { + name = "test-lb" + availability_zones = ["us-east-1a"] + + listener { + instance_port = 25 + instance_protocol = "tcp" + lb_port = 25 + lb_protocol = "tcp" + } + + listener { + instance_port = 587 + instance_protocol = "tcp" + lb_port = 587 + lb_protocol = "tcp" + } +} + +resource "aws_proxy_protocol_policy" "smtp" { + load_balancer = "${aws_elb.lb.name}" + instance_ports = ["25", "587"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `load_balancer` - (Required) The load balancer to which the policy + should be attached. +* `instance_ports` - (Required) List of instance ports to which the policy + should be applied. This can be specified if the protocol is SSL or TCP. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the policy. +* `load_balancer` - The load balancer to which the policy is attached. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 083453649..294019069 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -76,6 +76,10 @@ aws_key_pair + > + aws_proxy_protocol_policy + + > aws_route_table