diff --git a/builtin/providers/aws/resource_aws_elb.go b/builtin/providers/aws/resource_aws_elb.go index 79dc5fe7a..84323a698 100644 --- a/builtin/providers/aws/resource_aws_elb.go +++ b/builtin/providers/aws/resource_aws_elb.go @@ -4,6 +4,8 @@ import ( "bytes" "fmt" "log" + "regexp" + "strconv" "strings" "time" @@ -92,9 +94,10 @@ func resourceAwsElb() *schema.Resource { }, "idle_timeout": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - Default: 60, + Type: schema.TypeInt, + Optional: true, + Default: 60, + ValidateFunc: validateIntegerInRange(1, 3600), }, "connection_draining": &schema.Schema{ @@ -115,9 +118,10 @@ func resourceAwsElb() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "interval": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - Default: 60, + Type: schema.TypeInt, + Optional: true, + Default: 60, + ValidateFunc: validateAccessLogsInterval, }, "bucket": &schema.Schema{ Type: schema.TypeString, @@ -142,23 +146,27 @@ func resourceAwsElb() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "instance_port": &schema.Schema{ - Type: schema.TypeInt, - Required: true, + Type: schema.TypeInt, + Required: true, + ValidateFunc: validateIntegerInRange(1, 65535), }, "instance_protocol": &schema.Schema{ - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: validateListenerProtocol, }, "lb_port": &schema.Schema{ - Type: schema.TypeInt, - Required: true, + Type: schema.TypeInt, + Required: true, + ValidateFunc: validateIntegerInRange(1, 65535), }, "lb_protocol": &schema.Schema{ - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: validateListenerProtocol, }, "ssl_certificate_id": &schema.Schema{ @@ -178,28 +186,33 @@ func resourceAwsElb() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "healthy_threshold": &schema.Schema{ - Type: schema.TypeInt, - Required: true, + Type: schema.TypeInt, + Required: true, + ValidateFunc: validateIntegerInRange(2, 10), }, "unhealthy_threshold": &schema.Schema{ - Type: schema.TypeInt, - Required: true, + Type: schema.TypeInt, + Required: true, + ValidateFunc: validateIntegerInRange(2, 10), }, "target": &schema.Schema{ - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: validateHeathCheckTarget, }, "interval": &schema.Schema{ - Type: schema.TypeInt, - Required: true, + Type: schema.TypeInt, + Required: true, + ValidateFunc: validateIntegerInRange(5, 300), }, "timeout": &schema.Schema{ - Type: schema.TypeInt, - Required: true, + Type: schema.TypeInt, + Required: true, + ValidateFunc: validateIntegerInRange(2, 60), }, }, }, @@ -807,3 +820,112 @@ func sourceSGIdByName(meta interface{}, sg, vpcId string) (string, error) { group := resp.SecurityGroups[0] return *group.GroupId, nil } + +func validateAccessLogsInterval(v interface{}, k string) (ws []string, errors []error) { + value := v.(int) + + // Check if the value is either 5 or 60 (minutes). + if value != 5 && value != 60 { + errors = append(errors, fmt.Errorf( + "%q contains an invalid Access Logs interval \"%d\". "+ + "Valid intervals are either 5 or 60 (minutes).", + k, value)) + } + return +} + +func validateHeathCheckTarget(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + // Parse the Health Check target value. + matches := regexp.MustCompile(`\A(\w+):(\d+)(.+)?\z`).FindStringSubmatch(value) + + // Check if the value contains a valid target. + if matches == nil || len(matches) < 1 { + errors = append(errors, fmt.Errorf( + "%q contains an invalid Health Check: %s", + k, value)) + + // Invalid target? Return immediately, + // there is no need to collect other + // errors. + return + } + + // Check if the value contains a valid protocol. + if !isValidProtocol(matches[1]) { + errors = append(errors, fmt.Errorf( + "%q contains an invalid Health Check protocol %q. "+ + "Valid protocols are either %q, %q, %q, or %q.", + k, matches[1], "TCP", "SSL", "HTTP", "HTTPS")) + } + + // Check if the value contains a valid port range. + port, _ := strconv.Atoi(matches[2]) + if port < 1 || port > 65535 { + errors = append(errors, fmt.Errorf( + "%q contains an invalid Health Check target port \"%d\". "+ + "Valid port is in the range from 1 to 65535 inclusive.", + k, port)) + } + + switch strings.ToLower(matches[1]) { + case "tcp", "ssl": + // Check if value is in the form : for TCP and/or SSL. + if matches[3] != "" { + errors = append(errors, fmt.Errorf( + "%q cannot contain a path in the Health Check target: %s", + k, value)) + } + break + case "http", "https": + // Check if value is in the form :/ for HTTP and/or HTTPS. + if matches[3] == "" { + errors = append(errors, fmt.Errorf( + "%q must contain a path in the Health Check target: %s", + k, value)) + } + + // Cannot be longer than 1024 multibyte characters. + if len([]rune(matches[3])) > 1024 { + errors = append(errors, fmt.Errorf("%q cannot contain a path longer "+ + "than 1024 characters in the Health Check target: %s", + k, value)) + } + break + } + + return +} + +func validateListenerProtocol(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + if !isValidProtocol(value) { + errors = append(errors, fmt.Errorf( + "%q contains an invalid Listener protocol %q. "+ + "Valid protocols are either %q, %q, %q, or %q.", + k, value, "TCP", "SSL", "HTTP", "HTTPS")) + } + return +} + +func isValidProtocol(s string) bool { + if s == "" { + return false + } + s = strings.ToLower(s) + + validProtocols := map[string]bool{ + "http": true, + "https": true, + "ssl": true, + "tcp": true, + } + + if _, ok := validProtocols[s]; !ok { + return false + } + + return true +} diff --git a/builtin/providers/aws/resource_aws_elb_test.go b/builtin/providers/aws/resource_aws_elb_test.go index c47cf48c7..6a676fd4d 100644 --- a/builtin/providers/aws/resource_aws_elb_test.go +++ b/builtin/providers/aws/resource_aws_elb_test.go @@ -599,6 +599,183 @@ func TestResourceAWSELB_validateElbNameCannotEndWithHyphen(t *testing.T) { } } +func TestResourceAWSELB_validateAccessLogsInterval(t *testing.T) { + type testCases struct { + Value int + ErrCount int + } + + invalidCases := []testCases{ + { + Value: 0, + ErrCount: 1, + }, + { + Value: 10, + ErrCount: 1, + }, + { + Value: -1, + ErrCount: 1, + }, + } + + for _, tc := range invalidCases { + _, errors := validateAccessLogsInterval(tc.Value, "interval") + if len(errors) != tc.ErrCount { + t.Fatalf("Expected %q to trigger a validation error.", tc.Value) + } + } + +} + +func TestResourceAWSELB_validateListenerProtocol(t *testing.T) { + type testCases struct { + Value string + ErrCount int + } + + invalidCases := []testCases{ + { + Value: "", + ErrCount: 1, + }, + { + Value: "incorrect", + ErrCount: 1, + }, + { + Value: "HTTP:", + ErrCount: 1, + }, + } + + for _, tc := range invalidCases { + _, errors := validateListenerProtocol(tc.Value, "protocol") + if len(errors) != tc.ErrCount { + t.Fatalf("Expected %q to trigger a validation error.", tc.Value) + } + } + + validCases := []testCases{ + { + Value: "TCP", + ErrCount: 0, + }, + { + Value: "ssl", + ErrCount: 0, + }, + { + Value: "HTTP", + ErrCount: 0, + }, + } + + for _, tc := range validCases { + _, errors := validateListenerProtocol(tc.Value, "protocol") + if len(errors) != tc.ErrCount { + t.Fatalf("Expected %q not to trigger a validation error.", tc.Value) + } + } +} + +func TestResourceAWSELB_validateHealthCheckTarget(t *testing.T) { + type testCase struct { + Value string + ErrCount int + } + + randomRunes := func(n int) string { + rand.Seed(time.Now().UTC().UnixNano()) + + // A complete set of modern Katakana characters. + runes := []rune("アイウエオ" + + "カキクケコガギグゲゴサシスセソザジズゼゾ" + + "タチツテトダヂヅデドナニヌネノハヒフヘホ" + + "バビブベボパピプペポマミムメモヤユヨラリ" + + "ルレロワヰヱヲン") + + s := make([]rune, n) + for i := range s { + s[i] = runes[rand.Intn(len(runes))] + } + return string(s) + } + + validCases := []testCase{ + { + Value: "TCP:1234", + ErrCount: 0, + }, + { + Value: "http:80/test", + ErrCount: 0, + }, + { + Value: fmt.Sprintf("HTTP:8080/%s", randomRunes(5)), + ErrCount: 0, + }, + { + Value: "SSL:8080", + ErrCount: 0, + }, + } + + for _, tc := range validCases { + _, errors := validateHeathCheckTarget(tc.Value, "target") + if len(errors) != tc.ErrCount { + t.Fatalf("Expected %q not to trigger a validation error.", tc.Value) + } + } + + invalidCases := []testCase{ + { + Value: "", + ErrCount: 1, + }, + { + Value: "TCP:", + ErrCount: 1, + }, + { + Value: "TCP:1234/", + ErrCount: 1, + }, + { + Value: "SSL:8080/", + ErrCount: 1, + }, + { + Value: "HTTP:8080", + ErrCount: 1, + }, + { + Value: "incorrect-value", + ErrCount: 1, + }, + { + Value: "TCP:123456", + ErrCount: 1, + }, + { + Value: "incorrect:80/", + ErrCount: 1, + }, + { + Value: fmt.Sprintf("HTTP:8080/%s%s", randomString(512), randomRunes(512)), + ErrCount: 1, + }, + } + + for _, tc := range invalidCases { + _, errors := validateHeathCheckTarget(tc.Value, "target") + if len(errors) != tc.ErrCount { + t.Fatalf("Expected %q to trigger a validation error.", tc.Value) + } + } +} + func testAccCheckAWSELBDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).elbconn