Merge pull request #8578 from kwilczynski/feature/health-check-target-validation-aws_elb
provider/aws: Add validation of Health Check target to aws_elb.
This commit is contained in:
commit
9ad4e8453b
|
@ -4,6 +4,8 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -95,6 +97,7 @@ func resourceAwsElb() *schema.Resource {
|
||||||
Type: schema.TypeInt,
|
Type: schema.TypeInt,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Default: 60,
|
Default: 60,
|
||||||
|
ValidateFunc: validateIntegerInRange(1, 3600),
|
||||||
},
|
},
|
||||||
|
|
||||||
"connection_draining": &schema.Schema{
|
"connection_draining": &schema.Schema{
|
||||||
|
@ -118,6 +121,7 @@ func resourceAwsElb() *schema.Resource {
|
||||||
Type: schema.TypeInt,
|
Type: schema.TypeInt,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Default: 60,
|
Default: 60,
|
||||||
|
ValidateFunc: validateAccessLogsInterval,
|
||||||
},
|
},
|
||||||
"bucket": &schema.Schema{
|
"bucket": &schema.Schema{
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
|
@ -144,21 +148,25 @@ func resourceAwsElb() *schema.Resource {
|
||||||
"instance_port": &schema.Schema{
|
"instance_port": &schema.Schema{
|
||||||
Type: schema.TypeInt,
|
Type: schema.TypeInt,
|
||||||
Required: true,
|
Required: true,
|
||||||
|
ValidateFunc: validateIntegerInRange(1, 65535),
|
||||||
},
|
},
|
||||||
|
|
||||||
"instance_protocol": &schema.Schema{
|
"instance_protocol": &schema.Schema{
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Required: true,
|
Required: true,
|
||||||
|
ValidateFunc: validateListenerProtocol,
|
||||||
},
|
},
|
||||||
|
|
||||||
"lb_port": &schema.Schema{
|
"lb_port": &schema.Schema{
|
||||||
Type: schema.TypeInt,
|
Type: schema.TypeInt,
|
||||||
Required: true,
|
Required: true,
|
||||||
|
ValidateFunc: validateIntegerInRange(1, 65535),
|
||||||
},
|
},
|
||||||
|
|
||||||
"lb_protocol": &schema.Schema{
|
"lb_protocol": &schema.Schema{
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Required: true,
|
Required: true,
|
||||||
|
ValidateFunc: validateListenerProtocol,
|
||||||
},
|
},
|
||||||
|
|
||||||
"ssl_certificate_id": &schema.Schema{
|
"ssl_certificate_id": &schema.Schema{
|
||||||
|
@ -180,26 +188,31 @@ func resourceAwsElb() *schema.Resource {
|
||||||
"healthy_threshold": &schema.Schema{
|
"healthy_threshold": &schema.Schema{
|
||||||
Type: schema.TypeInt,
|
Type: schema.TypeInt,
|
||||||
Required: true,
|
Required: true,
|
||||||
|
ValidateFunc: validateIntegerInRange(2, 10),
|
||||||
},
|
},
|
||||||
|
|
||||||
"unhealthy_threshold": &schema.Schema{
|
"unhealthy_threshold": &schema.Schema{
|
||||||
Type: schema.TypeInt,
|
Type: schema.TypeInt,
|
||||||
Required: true,
|
Required: true,
|
||||||
|
ValidateFunc: validateIntegerInRange(2, 10),
|
||||||
},
|
},
|
||||||
|
|
||||||
"target": &schema.Schema{
|
"target": &schema.Schema{
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Required: true,
|
Required: true,
|
||||||
|
ValidateFunc: validateHeathCheckTarget,
|
||||||
},
|
},
|
||||||
|
|
||||||
"interval": &schema.Schema{
|
"interval": &schema.Schema{
|
||||||
Type: schema.TypeInt,
|
Type: schema.TypeInt,
|
||||||
Required: true,
|
Required: true,
|
||||||
|
ValidateFunc: validateIntegerInRange(5, 300),
|
||||||
},
|
},
|
||||||
|
|
||||||
"timeout": &schema.Schema{
|
"timeout": &schema.Schema{
|
||||||
Type: schema.TypeInt,
|
Type: schema.TypeInt,
|
||||||
Required: true,
|
Required: true,
|
||||||
|
ValidateFunc: validateIntegerInRange(2, 60),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -807,3 +820,112 @@ func sourceSGIdByName(meta interface{}, sg, vpcId string) (string, error) {
|
||||||
group := resp.SecurityGroups[0]
|
group := resp.SecurityGroups[0]
|
||||||
return *group.GroupId, nil
|
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 <PROTOCOL>:<PORT> 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 <PROTOCOL>:<PORT>/<PATH> 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
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
func testAccCheckAWSELBDestroy(s *terraform.State) error {
|
||||||
conn := testAccProvider.Meta().(*AWSClient).elbconn
|
conn := testAccProvider.Meta().(*AWSClient).elbconn
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue