diff --git a/ingress/controllers/nginx/nginx.tmpl b/ingress/controllers/nginx/nginx.tmpl index 68277d3513..fe2bbb3e07 100644 --- a/ingress/controllers/nginx/nginx.tmpl +++ b/ingress/controllers/nginx/nginx.tmpl @@ -156,6 +156,12 @@ http { } {{ end }} + {{/* build all the required rate limit zones. Each annotation requires a dedicated zone */}} + {{/* 1MB -> 16 thousand 64-byte states or about 8 thousand 128-byte states */}} + {{ $zone := range (buildRateLimitZones .servers) }} + {{ $zone }} + {{ end }} + {{ range $server := .servers }} server { server_name {{ $server.Name }}; diff --git a/ingress/controllers/nginx/nginx/ratelimit/main.go b/ingress/controllers/nginx/nginx/ratelimit/main.go new file mode 100644 index 0000000000..1d30feedc1 --- /dev/null +++ b/ingress/controllers/nginx/nginx/ratelimit/main.go @@ -0,0 +1,131 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ratelimit + +import ( + "errors" + "fmt" + "strconv" + + "k8s.io/kubernetes/pkg/apis/extensions" +) + +const ( + limitIp = "ingress-nginx.kubernetes.io/limit-connections" + limitRps = "ingress-nginx.kubernetes.io/limit-rps" + + // allow 5 times the specified limit as burst + defBurst = 5 + + // 1MB -> 16 thousand 64-byte states or about 8 thousand 128-byte states + // default is 5MB + defSharedSize = 5 +) + +var ( + // ErrInvalidRateLimit is returned when the annotation caontains invalid values + ErrInvalidRateLimit = errors.New("invalid rate limit value. Must be > 0") +) + +// ErrMissingAnnotations is returned when the ingress rule +// does not contains annotations related with rate limit +type ErrMissingAnnotations struct { + msg string +} + +func (e ErrMissingAnnotations) Error() string { + return e.msg +} + +// RateLimit returns rate limit configuration for an Ingress rule +// Is possible to limit the number of connections per IP address or +// connections per second. +// Note: Is possible to specify both limits +type RateLimit struct { + // Connections indicates a limit with the number of connections per IP address + Connections Zone + // RPS indicates a limit with the number of connections per second + RPS Zone +} + +// Zone returns information about the rate limit +type Zone struct { + Name string + Limit int + Burst int + // SharedSize amount of shared memory for the zone + SharedSize int +} + +type ingAnnotations map[string]string + +func (a ingAnnotations) limitIp() int { + val, ok := a[limitIp] + if ok { + if i, err := strconv.Atoi(val); err == nil { + return i + } + } + + return -1 +} + +func (a ingAnnotations) limitRps() int { + val, ok := a[limitRps] + if ok { + if i, err := strconv.Atoi(val); err == nil { + return i + } + } + + return -1 +} + +// ParseAnnotations parses the annotations contained in the ingress +// rule used to rewrite the defined paths +func ParseAnnotations(ing *extensions.Ingress) (*RateLimit, error) { + if ing.GetAnnotations() == nil { + return &RateLimit{}, ErrMissingAnnotations{"no annotations present"} + } + + rps := ingAnnotations(ing.GetAnnotations()).limitRps() + conn := ingAnnotations(ing.GetAnnotations()).limitIp() + + if rps == 0 && conn == 0 { + return &RateLimit{ + Connections: Zone{"", -1, -1, 1}, + RPS: Zone{"", -1, -1, 1}, + }, ErrInvalidRateLimit + } + + zoneName := fmt.Sprintf("%v_%v", ing.GetNamespace(), ing.GetName()) + + return &RateLimit{ + Connections: Zone{ + Name: fmt.Sprintf("%v_conn", zoneName), + Limit: conn, + Burst: conn * defBurst, + SharedSize: defSharedSize, + }, + RPS: Zone{ + Name: fmt.Sprintf("%v_rps", zoneName), + Limit: rps, + Burst: conn * defBurst, + SharedSize: defSharedSize, + }, + }, nil +} diff --git a/ingress/controllers/nginx/nginx/ratelimit/main_test.go b/ingress/controllers/nginx/nginx/ratelimit/main_test.go new file mode 100644 index 0000000000..7a7f88f33b --- /dev/null +++ b/ingress/controllers/nginx/nginx/ratelimit/main_test.go @@ -0,0 +1,129 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ratelimit + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/util/intstr" +) + +func buildIngress() *extensions.Ingress { + defaultBackend := extensions.IngressBackend{ + ServiceName: "default-backend", + ServicePort: intstr.FromInt(80), + } + + return &extensions.Ingress{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: api.NamespaceDefault, + }, + Spec: extensions.IngressSpec{ + Backend: &extensions.IngressBackend{ + ServiceName: "default-backend", + ServicePort: intstr.FromInt(80), + }, + Rules: []extensions.IngressRule{ + { + Host: "foo.bar.com", + IngressRuleValue: extensions.IngressRuleValue{ + HTTP: &extensions.HTTPIngressRuleValue{ + Paths: []extensions.HTTPIngressPath{ + { + Path: "/foo", + Backend: defaultBackend, + }, + }, + }, + }, + }, + }, + }, + } +} + +func TestAnnotations(t *testing.T) { + ing := buildIngress() + + lip := ingAnnotations(ing.GetAnnotations()).limitIp() + if lip != -1 { + t.Error("Expected -1 in limit by ip but %v was returned", lip) + } + + lrps := ingAnnotations(ing.GetAnnotations()).limitRps() + if lrps != -1 { + t.Error("Expected -1 in limit by rps but %v was returend", lrps) + } + + data := map[string]string{} + data[limitIp] = "5" + data[limitRps] = "100" + ing.SetAnnotations(data) + + lip = ingAnnotations(ing.GetAnnotations()).limitIp() + if lip != 5 { + t.Error("Expected %v in limit by ip but %v was returend", lip) + } + + lrps = ingAnnotations(ing.GetAnnotations()).limitRps() + if lrps != 100 { + t.Error("Expected 100 in limit by rps but %v was returend", lrps) + } +} + +func TestWithoutAnnotations(t *testing.T) { + ing := buildIngress() + _, err := ParseAnnotations(ing) + if err == nil { + t.Error("Expected error with ingress without annotations") + } +} + +func TestBadRateLimiting(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + data[limitIp] = "0" + data[limitRps] = "0" + ing.SetAnnotations(data) + + _, err := ParseAnnotations(ing) + if err == nil { + t.Errorf("Expected error with invalid limits (0)") + } + + data = map[string]string{} + data[limitIp] = "5" + data[limitRps] = "100" + ing.SetAnnotations(data) + + rateLimit, err := ParseAnnotations(ing) + if err != nil { + t.Errorf("Uxpected error: %v", err) + } + + if rateLimit.Connections.Limit != 5 { + t.Error("Expected 5 in limit by ip but %v was returend", rateLimit.Connections) + } + + if rateLimit.RPS.Limit != 100 { + t.Error("Expected 100 in limit by rps but %v was returend", rateLimit.RPS) + } +}