From 417b98bafba8b4a528a1b91077e5f1f3e342b242 Mon Sep 17 00:00:00 2001 From: James Nugent Date: Thu, 18 Aug 2016 23:43:11 +0100 Subject: [PATCH] provider/aws: Add aws_alb_listener_rule resource This commit adds the aws_alb_listener_rule resource along with acceptance tests and documentation. --- builtin/providers/aws/provider.go | 1 + .../aws/resource_aws_alb_listener.go | 4 +- .../aws/resource_aws_alb_listener_rule.go | 252 ++++++++++++++++++ .../resource_aws_alb_listener_rule_test.go | 216 +++++++++++++++ .../aws/r/alb_listener_rule.html.markdown | 73 +++++ website/source/layouts/aws.erb | 4 + 6 files changed, 548 insertions(+), 2 deletions(-) create mode 100644 builtin/providers/aws/resource_aws_alb_listener_rule.go create mode 100644 builtin/providers/aws/resource_aws_alb_listener_rule_test.go create mode 100644 website/source/docs/providers/aws/r/alb_listener_rule.html.markdown diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 52382eb03..106ce575d 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -154,6 +154,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "aws_alb": resourceAwsAlb(), "aws_alb_listener": resourceAwsAlbListener(), + "aws_alb_listener_rule": resourceAwsAlbListenerRule(), "aws_alb_target_group": resourceAwsAlbTargetGroup(), "aws_ami": resourceAwsAmi(), "aws_ami_copy": resourceAwsAmiCopy(), diff --git a/builtin/providers/aws/resource_aws_alb_listener.go b/builtin/providers/aws/resource_aws_alb_listener.go index a2bdf2413..5d5baac0d 100644 --- a/builtin/providers/aws/resource_aws_alb_listener.go +++ b/builtin/providers/aws/resource_aws_alb_listener.go @@ -73,7 +73,7 @@ func resourceAwsAlbListener() *schema.Resource { "type": { Type: schema.TypeString, Required: true, - ValidateFunc: validateAwsAlbListenerDefaultActionType, + ValidateFunc: validateAwsAlbListenerActionType, }, }, }, @@ -247,7 +247,7 @@ func validateAwsAlbListenerProtocol(v interface{}, k string) (ws []string, error return } -func validateAwsAlbListenerDefaultActionType(v interface{}, k string) (ws []string, errors []error) { +func validateAwsAlbListenerActionType(v interface{}, k string) (ws []string, errors []error) { value := strings.ToLower(v.(string)) if value != "forward" { errors = append(errors, fmt.Errorf("%q must have the value %q", k, "forward")) diff --git a/builtin/providers/aws/resource_aws_alb_listener_rule.go b/builtin/providers/aws/resource_aws_alb_listener_rule.go new file mode 100644 index 000000000..f1dc944b5 --- /dev/null +++ b/builtin/providers/aws/resource_aws_alb_listener_rule.go @@ -0,0 +1,252 @@ +package aws + +import ( + "errors" + "fmt" + "log" + "strconv" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsAlbListenerRule() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAlbListenerRuleCreate, + Read: resourceAwsAlbListenerRuleRead, + Update: resourceAwsAlbListenerRuleUpdate, + Delete: resourceAwsAlbListenerRuleDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "listener_arn": { + Type: schema.TypeString, + Required: true, + }, + "priority": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validateAwsAlbListenerRulePriority, + }, + "action": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "target_group_arn": { + Type: schema.TypeString, + Required: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateAwsAlbListenerActionType, + }, + }, + }, + }, + "condition": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "field": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateAwsListenerRuleField, + }, + "values": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + }, + }, + }, + }, + } +} + +func resourceAwsAlbListenerRuleCreate(d *schema.ResourceData, meta interface{}) error { + elbconn := meta.(*AWSClient).elbv2conn + + params := &elbv2.CreateRuleInput{ + ListenerArn: aws.String(d.Get("listener_arn").(string)), + Priority: aws.Int64(int64(d.Get("priority").(int))), + } + + actions := d.Get("action").([]interface{}) + params.Actions = make([]*elbv2.Action, len(actions)) + for i, action := range actions { + actionMap := action.(map[string]interface{}) + params.Actions[i] = &elbv2.Action{ + TargetGroupArn: aws.String(actionMap["target_group_arn"].(string)), + Type: aws.String(actionMap["type"].(string)), + } + } + + conditions := d.Get("condition").([]interface{}) + params.Conditions = make([]*elbv2.RuleCondition, len(conditions)) + for i, condition := range conditions { + conditionMap := condition.(map[string]interface{}) + values := conditionMap["values"].([]interface{}) + params.Conditions[i] = &elbv2.RuleCondition{ + Field: aws.String(conditionMap["field"].(string)), + Values: make([]*string, len(values)), + } + for j, value := range values { + params.Conditions[i].Values[j] = aws.String(value.(string)) + } + } + + resp, err := elbconn.CreateRule(params) + if err != nil { + return errwrap.Wrapf("Error creating ALB Listener Rule: {{err}}", err) + } + + if len(resp.Rules) == 0 { + return errors.New("Error creating ALB Listener Rule: no rules returned in response") + } + + d.SetId(*resp.Rules[0].RuleArn) + + return resourceAwsAlbListenerRuleRead(d, meta) +} + +func resourceAwsAlbListenerRuleRead(d *schema.ResourceData, meta interface{}) error { + elbconn := meta.(*AWSClient).elbv2conn + + resp, err := elbconn.DescribeRules(&elbv2.DescribeRulesInput{ + RuleArns: []*string{aws.String(d.Id())}, + }) + if err != nil { + if isRuleNotFound(err) { + log.Printf("[WARN] DescribeRules - removing %s from state", d.Id()) + d.SetId("") + return nil + } + return errwrap.Wrapf(fmt.Sprintf("Error retrieving Rules for listener %s: {{err}}", d.Id()), err) + } + + if len(resp.Rules) != 1 { + return fmt.Errorf("Error retrieving Rule %q", d.Id()) + } + + rule := resp.Rules[0] + + d.Set("arn", rule.RuleArn) + if priority, err := strconv.Atoi(*rule.Priority); err != nil { + return errwrap.Wrapf("Cannot convert rule priority %q to int: {{err}}", err) + } else { + d.Set("priority", priority) + } + + actions := make([]interface{}, len(rule.Actions)) + for i, action := range rule.Actions { + actionMap := make(map[string]interface{}) + actionMap["target_group_arn"] = *action.TargetGroupArn + actionMap["type"] = *action.Type + actions[i] = actionMap + } + d.Set("action", actions) + + conditions := make([]interface{}, len(rule.Conditions)) + for i, condition := range rule.Conditions { + conditionMap := make(map[string]interface{}) + conditionMap["field"] = *condition.Field + conditionValues := make([]string, len(condition.Values)) + for k, value := range condition.Values { + conditionValues[k] = *value + } + conditionMap["values"] = conditionValues + conditions[i] = conditionMap + } + d.Set("condition", conditions) + + return nil +} + +func resourceAwsAlbListenerRuleUpdate(d *schema.ResourceData, meta interface{}) error { + elbconn := meta.(*AWSClient).elbv2conn + + params := &elbv2.ModifyRuleInput{ + RuleArn: aws.String(d.Id()), + } + + actions := d.Get("action").([]interface{}) + params.Actions = make([]*elbv2.Action, len(actions)) + for i, action := range actions { + actionMap := action.(map[string]interface{}) + params.Actions[i] = &elbv2.Action{ + TargetGroupArn: aws.String(actionMap["target_group_arn"].(string)), + Type: aws.String(actionMap["type"].(string)), + } + } + + conditions := d.Get("condition").([]interface{}) + params.Conditions = make([]*elbv2.RuleCondition, len(conditions)) + for i, condition := range conditions { + conditionMap := condition.(map[string]interface{}) + values := conditionMap["values"].([]interface{}) + params.Conditions[i] = &elbv2.RuleCondition{ + Field: aws.String(conditionMap["field"].(string)), + Values: make([]*string, len(values)), + } + for j, value := range values { + params.Conditions[i].Values[j] = aws.String(value.(string)) + } + } + + resp, err := elbconn.ModifyRule(params) + if err != nil { + return errwrap.Wrapf("Error modifying ALB Listener Rule: {{err}}", err) + } + + if len(resp.Rules) == 0 { + return errors.New("Error modifying creating ALB Listener Rule: no rules returned in response") + } + + return resourceAwsAlbListenerRuleRead(d, meta) +} + +func resourceAwsAlbListenerRuleDelete(d *schema.ResourceData, meta interface{}) error { + elbconn := meta.(*AWSClient).elbv2conn + + _, err := elbconn.DeleteRule(&elbv2.DeleteRuleInput{ + RuleArn: aws.String(d.Id()), + }) + if err != nil && !isRuleNotFound(err) { + return errwrap.Wrapf("Error deleting ALB Listener Rule: {{err}}", err) + } + return nil +} + +func validateAwsAlbListenerRulePriority(v interface{}, k string) (ws []string, errors []error) { + value := v.(int) + if value < 1 || value > 99999 { + errors = append(errors, fmt.Errorf("%q must be in the range 1-99999", k)) + } + return +} + +func validateAwsListenerRuleField(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if len(value) > 64 { + errors = append(errors, fmt.Errorf("%q must be a maximum of 64 characters", k)) + } + return +} + +func isRuleNotFound(err error) bool { + elberr, ok := err.(awserr.Error) + return ok && elberr.Code() == "RuleNotFound" +} diff --git a/builtin/providers/aws/resource_aws_alb_listener_rule_test.go b/builtin/providers/aws/resource_aws_alb_listener_rule_test.go new file mode 100644 index 000000000..683fa3535 --- /dev/null +++ b/builtin/providers/aws/resource_aws_alb_listener_rule_test.go @@ -0,0 +1,216 @@ +package aws + +import ( + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSALBListenerRule_basic(t *testing.T) { + var conf elbv2.Rule + albName := fmt.Sprintf("testrule-basic-%s", acctest.RandStringFromCharSet(13, acctest.CharSetAlphaNum)) + targetGroupName := fmt.Sprintf("testtargetgroup-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "aws_alb_listener_rule.static", + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSALBListenerRuleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSALBListenerRuleConfig_basic(albName, targetGroupName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSALBListenerRuleExists("aws_alb_listener_rule.static", &conf), + resource.TestCheckResourceAttrSet("aws_alb_listener_rule.static", "arn"), + resource.TestCheckResourceAttr("aws_alb_listener_rule.static", "priority", "100"), + resource.TestCheckResourceAttr("aws_alb_listener_rule.static", "action.#", "1"), + resource.TestCheckResourceAttr("aws_alb_listener_rule.static", "action.0.type", "forward"), + resource.TestCheckResourceAttrSet("aws_alb_listener_rule.static", "action.0.target_group_arn"), + resource.TestCheckResourceAttr("aws_alb_listener_rule.static", "condition.#", "1"), + resource.TestCheckResourceAttr("aws_alb_listener_rule.static", "condition.0.field", "path-pattern"), + resource.TestCheckResourceAttr("aws_alb_listener_rule.static", "condition.0.values.#", "1"), + resource.TestCheckResourceAttrSet("aws_alb_listener_rule.static", "condition.0.values.0"), + ), + }, + }, + }) +} + +func testAccCheckAWSALBListenerRuleExists(n string, res *elbv2.Rule) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return errors.New("No Listener Rule ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).elbv2conn + + describe, err := conn.DescribeRules(&elbv2.DescribeRulesInput{ + RuleArns: []*string{aws.String(rs.Primary.ID)}, + }) + + if err != nil { + return err + } + + if len(describe.Rules) != 1 || + *describe.Rules[0].RuleArn != rs.Primary.ID { + return errors.New("Listener Rule not found") + } + + *res = *describe.Rules[0] + return nil + } +} + +func testAccCheckAWSALBListenerRuleDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).elbv2conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_alb_listener_rule" { + continue + } + + describe, err := conn.DescribeRules(&elbv2.DescribeRulesInput{ + RuleArns: []*string{aws.String(rs.Primary.ID)}, + }) + + if err == nil { + if len(describe.Rules) != 0 && + *describe.Rules[0].RuleArn == rs.Primary.ID { + return fmt.Errorf("Listener Rule %q still exists", rs.Primary.ID) + } + } + + // Verify the error + if isRuleNotFound(err) { + return nil + } else { + return errwrap.Wrapf("Unexpected error checking ALB Listener Rule destroyed: {{err}}", err) + } + } + + return nil +} + +func testAccAWSALBListenerRuleConfig_basic(albName, targetGroupName string) string { + return fmt.Sprintf(`resource "aws_alb_listener_rule" "static" { + listener_arn = "${aws_alb_listener.front_end.arn}" + priority = 100 + + action { + type = "forward" + target_group_arn = "${aws_alb_target_group.test.arn}" + } + + condition { + field = "path-pattern" + values = ["/static/*"] + } +} + +resource "aws_alb_listener" "front_end" { + load_balancer_arn = "${aws_alb.alb_test.id}" + protocol = "HTTP" + port = "80" + + default_action { + target_group_arn = "${aws_alb_target_group.test.id}" + type = "forward" + } +} + +resource "aws_alb" "alb_test" { + name = "%s" + internal = false + security_groups = ["${aws_security_group.alb_test.id}"] + subnets = ["${aws_subnet.alb_test.*.id}"] + + idle_timeout = 30 + enable_deletion_protection = false + + tags { + TestName = "TestAccAWSALB_basic" + } +} + +resource "aws_alb_target_group" "test" { + name = "%s" + port = 8080 + protocol = "HTTP" + vpc_id = "${aws_vpc.alb_test.id}" + + health_check { + path = "/health" + interval = 60 + port = 8081 + protocol = "HTTP" + timeout = 3 + healthy_threshold = 3 + unhealthy_threshold = 3 + matcher = "200-299" + } +} + +variable "subnets" { + default = ["10.0.1.0/24", "10.0.2.0/24"] + type = "list" +} + +data "aws_availability_zones" "available" {} + +resource "aws_vpc" "alb_test" { + cidr_block = "10.0.0.0/16" + + tags { + TestName = "TestAccAWSALB_basic" + } +} + +resource "aws_subnet" "alb_test" { + count = 2 + vpc_id = "${aws_vpc.alb_test.id}" + cidr_block = "${element(var.subnets, count.index)}" + map_public_ip_on_launch = true + availability_zone = "${element(data.aws_availability_zones.available.names, count.index)}" + + tags { + TestName = "TestAccAWSALB_basic" + } +} + +resource "aws_security_group" "alb_test" { + name = "allow_all_alb_test" + description = "Used for ALB Testing" + vpc_id = "${aws_vpc.alb_test.id}" + + ingress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags { + TestName = "TestAccAWSALB_basic" + } +}`, albName, targetGroupName) +} diff --git a/website/source/docs/providers/aws/r/alb_listener_rule.html.markdown b/website/source/docs/providers/aws/r/alb_listener_rule.html.markdown new file mode 100644 index 000000000..84054ad3c --- /dev/null +++ b/website/source/docs/providers/aws/r/alb_listener_rule.html.markdown @@ -0,0 +1,73 @@ +--- +layout: "aws" +page_title: "AWS: aws_alb_listener_rule" +sidebar_current: "docs-aws-resource-alb-listener-rule" +description: |- + Provides an Application Load Balancer Listener Rule resource. +--- + +# aws\_alb\_listener\_rule + +Provides an Application Load Balancer Listener Rule resource. + +## Example Usage + +``` +# Create a new load balancer +resource "aws_alb" "front_end" { + # Other parameters... +} + +resource "aws_alb_listener" "front_end" { + # Other parameters +} + +resource "aws_alb_listener_rule" "static" { + listener_arn = "${aws_alb_listener.front_end.arn} + priority = 100 + + action { + type = "forward" + target_group_arn = "${aws_alb_target_group.static.arn}" + } + + condition { + field = "path-pattern" + values = ["/static/*"] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `listener_arn` - (Required, Forces New Resource) The ARN of the listener to which to attach the rule. +* `priority` - (Required) The priority for the rule. A listener can't have multiple rules with the same priority. +* `action` - (Required) An Action block. Action blocks are documented below. +* `condition` - (Required) A Condition block. Condition blocks are documented below. + +Action Blocks (for `default_action`) support the following: + +* `target_group_arn` - (Required) The ARN of the Target Group to which to route traffic. +* `type` - (Required) The type of routing action. The only valid value is `forward`. + +Condition Blocks (for `default_action`) support the following: + +* `field` - (Required) The name of the field. The only valid value is `path-pattern`. +* `values` - (Required) The path patterns to match. + +## Attributes Reference + +The following attributes are exported in addition to the arguments listed above: + +* `id` - The ARN of the rule (matches `arn`) +* `arn` - The ARN of the rule (matches `id`) + +## Import + +Rules can be imported using their ARN, e.g. + +``` +$ terraform import aws_alb_listener_rule.front_end arn:aws:elasticloadbalancing:us-west-2:187416307283:listener-rule/app/test/8e4497da625e2d8a/9ab28ade35828f96/67b3d2d36dd7c26b +``` diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 9b7731fd5..b88b462b7 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -218,6 +218,10 @@ aws_alb_listener + > + aws_alb_listener_rule + + > aws_alb_target_group