diff --git a/cmd/sync/aws.go b/cmd/sync/aws.go index 3221bb734..e3829f24f 100644 --- a/cmd/sync/aws.go +++ b/cmd/sync/aws.go @@ -10,7 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2/ec2iface" - yaml "gopkg.in/yaml.v2" + "gopkg.in/yaml.v2" ) // AWSClient allows you to get the list of IP addresses of instanes of an Auto Scaling group. It implements the CloudProvider interface @@ -65,8 +65,12 @@ func (client *AWSClient) GetUpstreams() []Upstream { u := Upstream{ Name: awsU.Name, Port: awsU.Port, - Kind: awsU.Kind, ScalingGroup: awsU.AutoscalingGroup, + Kind: awsU.Kind, + MaxConns: awsU.MaxConns, + MaxFails: &awsU.MaxFails, + FailTimeout: awsU.FailTimeout, + SlowStart: awsU.SlowStart, } upstreams = append(upstreams, u) } @@ -171,6 +175,10 @@ type awsUpstream struct { AutoscalingGroup string `yaml:"autoscaling_group"` Port int Kind string + MaxConns int `yaml:"max_conns"` + MaxFails int `yaml:"max_fails"` + FailTimeout string `yaml:"fail_timeout"` + SlowStart string `yaml:"slow_start"` } func validateAWSConfig(cfg *awsConfig) error { @@ -195,6 +203,19 @@ func validateAWSConfig(cfg *awsConfig) error { if ups.Kind == "" || !(ups.Kind == "http" || ups.Kind == "stream") { return fmt.Errorf(upstreamKindErrorMsgFormat, ups.Name) } + if ups.MaxConns < 0 { + return fmt.Errorf(upstreamMaxConnsErrorMsg, ups.MaxConns) + } + if ups.MaxFails < 0 { + return fmt.Errorf(upstreamMaxFailsErrorMsg, ups.MaxFails) + } + if err := validateTime(ups.FailTimeout); err != nil { + return fmt.Errorf(upstreamFailTimeoutErrorMsg, ups.FailTimeout, err) + } + if err := validateTime(ups.SlowStart); err != nil { + return fmt.Errorf(upstreamSlowStartErrorMsg, ups.SlowStart, err) + } + } return nil diff --git a/cmd/sync/aws_test.go b/cmd/sync/aws_test.go index 55c893cc4..e0fb75205 100644 --- a/cmd/sync/aws_test.go +++ b/cmd/sync/aws_test.go @@ -14,6 +14,10 @@ func getValidAWSConfig() *awsConfig { AutoscalingGroup: "backend-group", Port: 80, Kind: "http", + MaxConns: 1000, + MaxFails: 10, + FailTimeout: "5s", + SlowStart: "30s", }, } cfg := awsConfig{ @@ -51,6 +55,22 @@ func getInvalidAWSConfigInput() []*testInputAWS { invalidUpstreamKindCfg.Upstreams[0].Kind = "" input = append(input, &testInputAWS{invalidUpstreamKindCfg, "invalid kind of the upstream"}) + invalidUpstreamMaxConnsCfg := getValidAWSConfig() + invalidUpstreamMaxConnsCfg.Upstreams[0].MaxConns = -10 + input = append(input, &testInputAWS{invalidUpstreamMaxConnsCfg, "invalid max_conns of the upstream"}) + + invalidUpstreamMaxFailsCfg := getValidAWSConfig() + invalidUpstreamMaxFailsCfg.Upstreams[0].MaxFails = -10 + input = append(input, &testInputAWS{invalidUpstreamMaxFailsCfg, "invalid max_fails of the upstream"}) + + invalidUpstreamFailTimeoutCfg := getValidAWSConfig() + invalidUpstreamFailTimeoutCfg.Upstreams[0].FailTimeout = "-10s" + input = append(input, &testInputAWS{invalidUpstreamFailTimeoutCfg, "invalid fail_timeout of the upstream"}) + + invalidUpstreamSlowStartCfg := getValidAWSConfig() + invalidUpstreamSlowStartCfg.Upstreams[0].SlowStart = "-10s" + input = append(input, &testInputAWS{invalidUpstreamSlowStartCfg, "invalid slow_start of the upstream"}) + return input } diff --git a/cmd/sync/azure.go b/cmd/sync/azure.go index 15eed078d..a14e4d2b7 100644 --- a/cmd/sync/azure.go +++ b/cmd/sync/azure.go @@ -7,7 +7,7 @@ import ( "github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute" "github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network" "github.com/Azure/go-autorest/autorest/azure/auth" - yaml "gopkg.in/yaml.v2" + "gopkg.in/yaml.v2" ) // AzureClient allows you to get the list of IP addresses of VirtualMachines of a VirtualMachine Scale Set. It implements the CloudProvider interface @@ -134,8 +134,12 @@ func (client *AzureClient) GetUpstreams() []Upstream { u := Upstream{ Name: azureU.Name, Port: azureU.Port, - Kind: azureU.Kind, ScalingGroup: azureU.VMScaleSet, + Kind: azureU.Kind, + MaxConns: azureU.MaxConns, + MaxFails: &azureU.MaxFails, + FailTimeout: azureU.FailTimeout, + SlowStart: azureU.SlowStart, } upstreams = append(upstreams, u) } @@ -149,10 +153,14 @@ type azureConfig struct { } type azureUpstream struct { - Name string - VMScaleSet string `yaml:"virtual_machine_scale_set"` - Port int - Kind string + Name string + VMScaleSet string `yaml:"virtual_machine_scale_set"` + Port int + Kind string + MaxConns int `yaml:"max_conns"` + MaxFails int `yaml:"max_fails"` + FailTimeout string `yaml:"fail_timeout"` + SlowStart string `yaml:"slow_start"` } func validateAzureConfig(cfg *azureConfig) error { @@ -181,6 +189,18 @@ func validateAzureConfig(cfg *azureConfig) error { if ups.Kind == "" || !(ups.Kind == "http" || ups.Kind == "stream") { return fmt.Errorf(upstreamKindErrorMsgFormat, ups.Name) } + if ups.MaxConns < 0 { + return fmt.Errorf(upstreamMaxConnsErrorMsg, ups.MaxConns) + } + if ups.MaxFails < 0 { + return fmt.Errorf(upstreamMaxFailsErrorMsg, ups.MaxFails) + } + if err := validateTime(ups.FailTimeout); err != nil { + return fmt.Errorf(upstreamFailTimeoutErrorMsg, ups.FailTimeout, err) + } + if err := validateTime(ups.SlowStart); err != nil { + return fmt.Errorf(upstreamSlowStartErrorMsg, ups.SlowStart, err) + } } return nil } diff --git a/cmd/sync/azure_test.go b/cmd/sync/azure_test.go index 397129cfa..d4b5bdb42 100644 --- a/cmd/sync/azure_test.go +++ b/cmd/sync/azure_test.go @@ -14,10 +14,14 @@ type testInputAzure struct { func getValidAzureConfig() *azureConfig { upstreams := []azureUpstream{ { - Name: "backend1", - VMScaleSet: "backend-group", - Port: 80, - Kind: "http", + Name: "backend1", + VMScaleSet: "backend-group", + Port: 80, + Kind: "http", + MaxConns: 1000, + MaxFails: 10, + FailTimeout: "5s", + SlowStart: "30s", }, } cfg := azureConfig{ @@ -60,6 +64,22 @@ func getInvalidAzureConfigInput() []*testInputAzure { invalidUpstreamKindCfg.Upstreams[0].Kind = "" input = append(input, &testInputAzure{invalidUpstreamKindCfg, "invalid kind of the upstream"}) + invalidUpstreamMaxConnsCfg := getValidAzureConfig() + invalidUpstreamMaxConnsCfg.Upstreams[0].MaxConns = -10 + input = append(input, &testInputAzure{invalidUpstreamMaxConnsCfg, "invalid max_conns of the upstream"}) + + invalidUpstreamMaxFailsCfg := getValidAzureConfig() + invalidUpstreamMaxFailsCfg.Upstreams[0].MaxFails = -10 + input = append(input, &testInputAzure{invalidUpstreamMaxFailsCfg, "invalid max_fails of the upstream"}) + + invalidUpstreamFailTimeoutCfg := getValidAzureConfig() + invalidUpstreamFailTimeoutCfg.Upstreams[0].FailTimeout = "-10s" + input = append(input, &testInputAzure{invalidUpstreamFailTimeoutCfg, "invalid fail_timeout of the upstream"}) + + invalidUpstreamSlowStartCfg := getValidAzureConfig() + invalidUpstreamSlowStartCfg.Upstreams[0].SlowStart = "-10s" + input = append(input, &testInputAzure{invalidUpstreamSlowStartCfg, "invalid slow_start of the upstream"}) + return input } diff --git a/cmd/sync/config.go b/cmd/sync/config.go index a9b4cbb8e..94a5f7e6b 100644 --- a/cmd/sync/config.go +++ b/cmd/sync/config.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - yaml "gopkg.in/yaml.v2" + "gopkg.in/yaml.v2" ) // commonConfig stores the configuration parameters common to all providers @@ -55,4 +55,8 @@ type Upstream struct { Port int ScalingGroup string Kind string + MaxConns int + MaxFails *int + FailTimeout string + SlowStart string } diff --git a/cmd/sync/errormessages.go b/cmd/sync/errormessages.go index d4179efaa..87ebfbdb3 100644 --- a/cmd/sync/errormessages.go +++ b/cmd/sync/errormessages.go @@ -8,3 +8,7 @@ const upstreamNameErrorMsg = "The mandatory field name is either empty or missin const upstreamErrorMsgFormat = "The mandatory field %v is either empty or missing for the upstream %v in the config file" const upstreamPortErrorMsgFormat = "The mandatory field port is either zero or missing for the upstream %v in the config file" const upstreamKindErrorMsgFormat = "The mandatory field kind is either not equal to http or tcp or missing for the upstream %v in the config file" +const upstreamMaxConnsErrorMsg = "The field max_conns has invalid value %v, must be positive or zero in the config file" +const upstreamMaxFailsErrorMsg = "The field max_fails has invalid value %v, must be positive or zero in the config file" +const upstreamFailTimeoutErrorMsg = "The field fail_timeout has invalid value %v and returned errors %v" +const upstreamSlowStartErrorMsg = "The field slow_start has invalid value %v and returned errors %v" diff --git a/cmd/sync/main.go b/cmd/sync/main.go index f1f77902e..f80e0d587 100644 --- a/cmd/sync/main.go +++ b/cmd/sync/main.go @@ -113,8 +113,11 @@ func main() { for _, ip := range ips { backend := fmt.Sprintf("%v:%v", ip, upstream.Port) upsServers = append(upsServers, nginx.UpstreamServer{ - Server: backend, - MaxFails: 1, + Server: backend, + MaxConns: setPositiveInt(upstream.MaxConns, 0), + MaxFails: setPositiveIntOrZeroFromPointer(upstream.MaxFails, 1), + FailTimeout: setTime(upstream.FailTimeout, "10s"), + SlowStart: setTime(upstream.SlowStart, "0s"), }) } @@ -132,8 +135,11 @@ func main() { for _, ip := range ips { backend := fmt.Sprintf("%v:%v", ip, upstream.Port) upsServers = append(upsServers, nginx.StreamUpstreamServer{ - Server: backend, - MaxFails: 1, + Server: backend, + MaxConns: setPositiveInt(upstream.MaxConns, 0), + MaxFails: setPositiveIntOrZeroFromPointer(upstream.MaxFails, 1), + FailTimeout: setTime(upstream.FailTimeout, "10s"), + SlowStart: setTime(upstream.SlowStart, "0s"), }) } @@ -158,3 +164,27 @@ func main() { } } } + +func setPositiveInt(value int, defaultValue int) int { + if value == 0 { + return defaultValue + } + + return value +} + +func setPositiveIntOrZeroFromPointer(value *int, defaultValue int) int { + if value == nil { + return defaultValue + } + + return *value +} + +func setTime(time string, defaultTime string) string { + if time == "" { + return defaultTime + } + + return time +} diff --git a/cmd/sync/main_test.go b/cmd/sync/main_test.go new file mode 100644 index 000000000..0df8fbb4c --- /dev/null +++ b/cmd/sync/main_test.go @@ -0,0 +1,48 @@ +package main + +import "testing" + +func TestSetPositiveInt(t *testing.T) { + defaultValue := 0 + value := setPositiveInt(10, defaultValue) + if value == 0 { + t.Errorf("setPositiveInt() should return value %v but returned invalid value %v", value, defaultValue) + } + + defaultValue = 1 + value = setPositiveInt(0, defaultValue) + if value != 1 { + t.Errorf("setPositiveInt() should return default value %v but returned invalid value %v", defaultValue, value) + } +} + +func TestSetTime(t *testing.T) { + defaultTime := "10s" + time := setTime("20s", defaultTime) + + if time != "20s" { + t.Errorf("setTime() should return time %v but returned invalid time %v", time, defaultTime) + } + + defaultTime = "10s" + time = setTime("", defaultTime) + + if time != "10s" { + t.Errorf("setTime() should return default time %v but returned invalid time %v", defaultTime, time) + } +} + +func TestSetPositiveIntOrZeroFromPointer(t *testing.T) { + defaultValue := 1 + v := 10 + value := setPositiveIntOrZeroFromPointer(&v, defaultValue) + if value == 1 { + t.Errorf("setPositiveIntOrZeroFromPointer() should return value %v but returned invalid value %v", value, defaultValue) + } + + defaultValue = 1 + value = setPositiveIntOrZeroFromPointer(nil, defaultValue) + if value != 1 { + t.Errorf("setPositiveIntOrZeroFromPointer() should return default value %v but returned invalid value %v", defaultValue, value) + } +} diff --git a/cmd/sync/validation.go b/cmd/sync/validation.go new file mode 100644 index 000000000..77044f7f5 --- /dev/null +++ b/cmd/sync/validation.go @@ -0,0 +1,44 @@ +package main + +import ( + "errors" + "regexp" + "strings" +) + +// http://nginx.org/en/docs/syntax.html +var validTimeSuffixes = []string{ + "ms", + "s", + "m", + "h", + "d", + "w", + "M", + "y", +} + +var durationEscaped = strings.Join(validTimeSuffixes, "|") +var validNginxTime = regexp.MustCompile(`^([0-9]+([` + durationEscaped + `]?){0,1} *)+$`) + +func validateTime(time string) error { + if time == "" { + return nil + } + + if _, err := parseTime(time); err != nil { + return err + } + + return nil +} + +// ParseTime ensures that the string value in the annotation is a valid time. +func parseTime(s string) (string, error) { + s = strings.TrimSpace(s) + + if validNginxTime.MatchString(s) { + return s, nil + } + return "", errors.New("Invalid time string") +} diff --git a/cmd/sync/validation_test.go b/cmd/sync/validation_test.go new file mode 100644 index 000000000..1c8860e90 --- /dev/null +++ b/cmd/sync/validation_test.go @@ -0,0 +1,32 @@ +package main + +import "testing" + +func TestValidateTime(t *testing.T) { + time := "10" + err := validateTime(time) + + if err != nil { + t.Errorf("validateTime(%v) returned an error %v for valid input", time, err) + } +} + +func TestParseTime(t *testing.T) { + var testsWithValidInput = []string{"1", "1m10s", "11 11", "5m 30s", "1s", "100m", "5w", "15m", "11M", "3h", "100y", "600"} + var invalidInput = []string{"ss", "rM", "m0m", "s1s", "-5s", "", "1L"} + for _, test := range testsWithValidInput { + result, err := parseTime(test) + if err != nil { + t.Errorf("parseTime(%q) returned an error for valid input: %v", test, err) + } + if test != result { + t.Errorf("parseTime(%q) returned %q expected %q", test, result, test) + } + } + for _, test := range invalidInput { + result, err := parseTime(test) + if err == nil { + t.Errorf("parseTime(%q) didn't return error. Returned: %q", test, result) + } + } +} diff --git a/examples/aws.md b/examples/aws.md index 38b219e2d..7f4510c94 100644 --- a/examples/aws.md +++ b/examples/aws.md @@ -22,10 +22,18 @@ upstreams: autoscaling_group: backend-one-group port: 80 kind: http + max_conns: 1000 + max_fails: 10 + fail_timeout: 5s + slow_start: 30s - name: backend-two autoscaling_group: backend-two-group port: 80 kind: http + max_conns: 1000 + max_fails: 10 + fail_timeout: 5s + slow_start: 30s ``` * The `api_endpoint` key defines the NGINX Plus API endpoint. @@ -37,3 +45,7 @@ upstreams: * `autoscaling_group` – The name of the corresponding Auto Scaling group. Use of wildcards is supported. For example, `backend-*`. * `port` – The port on which our backend applications are exposed. * `kind` – The protocol of the traffic NGINX Plus load balances to the backend application, here `http`. If the application uses TCP/UDP, specify `stream` instead. + * `max_conns` – The maximum number of simultaneous active connections to an upstream server. Default value is 0, meaning there is no limit. + * `max_fails` – The number of unsuccessful attempts to communicate with an upstream server that should happen in the duration set by the `fail-timeout` to consider the server unavailable. Default value is 1. The zero value 0 disables the accounting of attempts. + * `fail_timeout` – The time during which the specified number of unsuccessful attempts to communicate with an upstream server should happen to consider the server unavailable. Default value is 10s. + * `slow_start` – The slow start allows an upstream server to gradually recover its weight from 0 to its nominal value after it has been recovered or became available or when the server becomes available after a period of time it was considered unavailable. By default, the slow start is disabled. diff --git a/examples/azure.md b/examples/azure.md index d18680860..a8e100d91 100644 --- a/examples/azure.md +++ b/examples/azure.md @@ -22,10 +22,18 @@ upstreams: virtual_machine_scale_set: backend-one-group port: 80 kind: http + max_conns: 1000 + max_fails: 10 + fail_timeout: 5s + slow_start: 30s - name: backend-two virtual_machine_scale_set: backend-two-group port: 80 kind: http + max_conns: 1000 + max_fails: 10 + fail_timeout: 5s + slow_start: 30s ``` * The `api_endpoint` key defines the NGINX Plus API endpoint. @@ -37,4 +45,8 @@ upstreams: * `name` – The name we specified for the upstream block in the NGINX Plus configuration. * `virtual_machine_scale_set` – The name of the corresponding Virtual Machine Scale Set. * `port` – The port on which our backend applications are exposed. - * `kind` – The protocol of the traffic NGINX Plus load balances to the backend application, here `http`. If the application uses TCP/UDP, specify `stream` instead. \ No newline at end of file + * `kind` – The protocol of the traffic NGINX Plus load balances to the backend application, here `http`. If the application uses TCP/UDP, specify `stream` instead. + * `max_conns` – The maximum number of simultaneous active connections to an upstream server. Default value is 0, meaning there is no limit. + * `max_fails` – The number of unsuccessful attempts to communicate with an upstream server that should happen in the duration set by the `fail-timeout` to consider the server unavailable. Default value is 1. The zero value 0 disables the accounting of attempts. + * `fail_timeout` – The time during which the specified number of unsuccessful attempts to communicate with an upstream server should happen to consider the server unavailable. Default value is 10s. + * `slow_start` – The slow start allows an upstream server to gradually recover its weight from 0 to its nominal value after it has been recovered or became available or when the server becomes available after a period of time it was considered unavailable. By default, the slow start is disabled.