diff --git a/controllers/nginx/controller.go b/controllers/nginx/controller.go index 84653bab9..3f8776d7d 100644 --- a/controllers/nginx/controller.go +++ b/controllers/nginx/controller.go @@ -42,6 +42,7 @@ import ( "k8s.io/contrib/ingress/controllers/nginx/healthcheck" "k8s.io/contrib/ingress/controllers/nginx/nginx" + "k8s.io/contrib/ingress/controllers/nginx/nginx/ratelimit" "k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite" ) @@ -583,6 +584,12 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg nginx.NginxConfigur continue } + rl, err := ratelimit.ParseAnnotations(ing) + glog.V(3).Infof("nginx rate limit %v", rl) + if err != nil { + glog.V(3).Infof("error reading rate limit annotation in Ingress %v/%v: %v", ing.GetNamespace(), ing.GetName(), err) + } + host := rule.Host if host == "" { host = defServerName @@ -615,6 +622,7 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg nginx.NginxConfigur glog.V(3).Infof("error parsing rewrite annotations for Ingress rule %v/%v: %v", ing.GetNamespace(), ing.GetName(), err) } loc.Redirect = *locRew + loc.RateLimit = *rl addLoc = false continue @@ -635,9 +643,10 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg nginx.NginxConfigur } server.Locations = append(server.Locations, &nginx.Location{ - Path: nginxPath, - Upstream: *ups, - Redirect: *locRew, + Path: nginxPath, + Upstream: *ups, + Redirect: *locRew, + RateLimit: *rl, }) } } diff --git a/controllers/nginx/nginx.tmpl b/controllers/nginx/nginx.tmpl index fe2bbb3e0..b12b0c996 100644 --- a/controllers/nginx/nginx.tmpl +++ b/controllers/nginx/nginx.tmpl @@ -186,7 +186,10 @@ http { {{- range $location := $server.Locations }} {{- $path := buildLocation $location }} location {{ $path }} { - location {{ $path }} { + {{/* if the location contains a rate limit annotation, create one */}} + {{ $limits := buildRateLimit $location }} + {{- range $limit := $limits }} + {{ $limit }}{{ end }} proxy_set_header Host $host; # Pass Real IP diff --git a/controllers/nginx/nginx/nginx.go b/controllers/nginx/nginx/nginx.go index 7b2b58488..3b1f26dd0 100644 --- a/controllers/nginx/nginx/nginx.go +++ b/controllers/nginx/nginx/nginx.go @@ -17,6 +17,7 @@ limitations under the License. package nginx import ( + "k8s.io/contrib/ingress/controllers/nginx/nginx/ratelimit" "k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite" ) @@ -93,6 +94,7 @@ type Location struct { IsDefBackend bool Upstream Upstream Redirect rewrite.Redirect + RateLimit ratelimit.RateLimit } // LocationByPath sorts location by path diff --git a/controllers/nginx/nginx/ratelimit/main.go b/controllers/nginx/nginx/ratelimit/main.go index 1d30feedc..097d3b10f 100644 --- a/controllers/nginx/nginx/ratelimit/main.go +++ b/controllers/nginx/nginx/ratelimit/main.go @@ -25,8 +25,8 @@ import ( ) const ( - limitIp = "ingress-nginx.kubernetes.io/limit-connections" - limitRps = "ingress-nginx.kubernetes.io/limit-rps" + limitIp = "ingress.kubernetes.io/limit-connections" + limitRps = "ingress.kubernetes.io/limit-rps" // allow 5 times the specified limit as burst defBurst = 5 @@ -39,18 +39,12 @@ const ( 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 + ErrMissingAnnotations = errors.New("no annotations present") ) -// 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. @@ -62,7 +56,8 @@ type RateLimit struct { RPS Zone } -// Zone returns information about the rate limit +// Zone returns information about the NGINX rate limit (limit_req_zone) +// http://nginx.org/en/docs/http/ngx_http_limit_req_module.html#limit_req_zone type Zone struct { Name string Limit int @@ -81,7 +76,7 @@ func (a ingAnnotations) limitIp() int { } } - return -1 + return 0 } func (a ingAnnotations) limitRps() int { @@ -92,14 +87,14 @@ func (a ingAnnotations) limitRps() int { } } - return -1 + return 0 } // 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"} + return &RateLimit{}, ErrMissingAnnotations } rps := ingAnnotations(ing.GetAnnotations()).limitRps() @@ -107,8 +102,8 @@ func ParseAnnotations(ing *extensions.Ingress) (*RateLimit, error) { if rps == 0 && conn == 0 { return &RateLimit{ - Connections: Zone{"", -1, -1, 1}, - RPS: Zone{"", -1, -1, 1}, + Connections: Zone{}, + RPS: Zone{}, }, ErrInvalidRateLimit } diff --git a/controllers/nginx/nginx/ratelimit/main_test.go b/controllers/nginx/nginx/ratelimit/main_test.go index 7a7f88f33..5242f57e9 100644 --- a/controllers/nginx/nginx/ratelimit/main_test.go +++ b/controllers/nginx/nginx/ratelimit/main_test.go @@ -63,13 +63,13 @@ 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) + if lip != 0 { + t.Error("Expected 0 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) + if lrps != 0 { + t.Error("Expected 0 in limit by rps but %v was returend", lrps) } data := map[string]string{} diff --git a/controllers/nginx/nginx/template.go b/controllers/nginx/nginx/template.go index a844cb944..32e3a3350 100644 --- a/controllers/nginx/nginx/template.go +++ b/controllers/nginx/nginx/template.go @@ -45,8 +45,10 @@ var ( return true }, - "buildLocation": buildLocation, - "buildProxyPass": buildProxyPass, + "buildLocation": buildLocation, + "buildProxyPass": buildProxyPass, + "buildRateLimitZones": buildRateLimitZones, + "buildRateLimit": buildRateLimit, } ) @@ -180,3 +182,59 @@ func buildProxyPass(input interface{}) string { // default proxy_pass return defProxyPass } + +// buildRateLimitZones produces an array of limit_conn_zone in order to allow +// rate limiting of request. Each Ingress rule could have up to two zones, one +// for connection limit by IP address and other for limiting request per second +func buildRateLimitZones(input interface{}) []string { + zones := []string{} + + servers, ok := input.([]*Server) + if !ok { + return zones + } + + for _, server := range servers { + for _, loc := range server.Locations { + + if loc.RateLimit.Connections.Limit != -1 { + zone := fmt.Sprintf("limit_conn_zone $binary_remote_addr zone=%v:%v;", + loc.RateLimit.Connections.Name, loc.RateLimit.Connections.SharedSize) + zones = append(zones, zone) + } + + if loc.RateLimit.RPS.Limit != -1 { + zone := fmt.Sprintf("limit_conn_zone $binary_remote_addr zone=%v:%v rate=%vr/s;", + loc.RateLimit.Connections.Name, loc.RateLimit.Connections.SharedSize, loc.RateLimit.Connections.Limit) + zones = append(zones, zone) + } + } + } + + return zones +} + +// buildRateLimit produces an array of limit_req to be used inside the Path of +// Ingress rules. The order: connections by IP first and RPS next. +func buildRateLimit(input interface{}) []string { + limits := []string{} + + loc, ok := input.(*Location) + if !ok { + return limits + } + + if loc.RateLimit.Connections.Limit != -1 { + limit := fmt.Sprintf("limit_conn %v %v;", + loc.RateLimit.Connections.Name, loc.RateLimit.Connections.Limit) + limits = append(limits, limit) + } + + if loc.RateLimit.RPS.Limit != -1 { + limit := fmt.Sprintf("limit_req zone=%v burst=%v nodelay;", + loc.RateLimit.Connections.Name, loc.RateLimit.Connections.Burst) + limits = append(limits, limit) + } + + return limits +}