Add Global Rate Limiting support
This commit is contained in:
parent
14345ebcfe
commit
e0dece48f7
21 changed files with 1179 additions and 38 deletions
|
|
@ -40,6 +40,7 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/annotations/customhttperrors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/defaultbackend"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/fastcgi"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/globalratelimit"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/http2pushpreload"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/influxdb"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist"
|
||||
|
|
@ -94,6 +95,7 @@ type Ingress struct {
|
|||
Proxy proxy.Config
|
||||
ProxySSL proxyssl.Config
|
||||
RateLimit ratelimit.Config
|
||||
GlobalRateLimit globalratelimit.Config
|
||||
Redirect redirect.Config
|
||||
Rewrite rewrite.Config
|
||||
Satisfy string
|
||||
|
|
@ -142,6 +144,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor {
|
|||
"Proxy": proxy.NewParser(cfg),
|
||||
"ProxySSL": proxyssl.NewParser(cfg),
|
||||
"RateLimit": ratelimit.NewParser(cfg),
|
||||
"GlobalRateLimit": globalratelimit.NewParser(cfg),
|
||||
"Redirect": redirect.NewParser(cfg),
|
||||
"Rewrite": rewrite.NewParser(cfg),
|
||||
"Satisfy": satisfy.NewParser(cfg),
|
||||
|
|
|
|||
111
internal/ingress/annotations/globalratelimit/main.go
Normal file
111
internal/ingress/annotations/globalratelimit/main.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
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 globalratelimit
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
networking "k8s.io/api/networking/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
"k8s.io/ingress-nginx/internal/net"
|
||||
"k8s.io/ingress-nginx/internal/sets"
|
||||
)
|
||||
|
||||
const defaultKey = "$remote_addr"
|
||||
|
||||
// Config encapsulates all global rate limit attributes
|
||||
type Config struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Limit int `json:"limit"`
|
||||
WindowSize int `json:"window-size"`
|
||||
Key string `json:"key"`
|
||||
IgnoredCIDRs []string `json:"ignored-cidrs"`
|
||||
}
|
||||
|
||||
// Equal tests for equality between two Config types
|
||||
func (l *Config) Equal(r *Config) bool {
|
||||
if l.Namespace != r.Namespace {
|
||||
return false
|
||||
}
|
||||
if l.Limit != r.Limit {
|
||||
return false
|
||||
}
|
||||
if l.WindowSize != r.WindowSize {
|
||||
return false
|
||||
}
|
||||
if l.Key != r.Key {
|
||||
return false
|
||||
}
|
||||
if len(l.IgnoredCIDRs) != len(r.IgnoredCIDRs) || !sets.StringElementsMatch(l.IgnoredCIDRs, r.IgnoredCIDRs) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type globalratelimit struct {
|
||||
r resolver.Resolver
|
||||
}
|
||||
|
||||
// NewParser creates a new globalratelimit annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return globalratelimit{r}
|
||||
}
|
||||
|
||||
// Parse extracts globalratelimit annotations from the given ingress
|
||||
// and returns them structured as Config type
|
||||
func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
config := &Config{}
|
||||
|
||||
limit, _ := parser.GetIntAnnotation("global-rate-limit", ing)
|
||||
rawWindowSize, _ := parser.GetStringAnnotation("global-rate-limit-window", ing)
|
||||
|
||||
if limit == 0 || len(rawWindowSize) == 0 {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
windowSize, err := time.ParseDuration(rawWindowSize)
|
||||
if err != nil {
|
||||
return config, ing_errors.LocationDenied{
|
||||
Reason: errors.Wrap(err, "failed to parse 'global-rate-limit-window' value"),
|
||||
}
|
||||
}
|
||||
|
||||
key, _ := parser.GetStringAnnotation("global-rate-limit-key", ing)
|
||||
if len(key) == 0 {
|
||||
key = defaultKey
|
||||
}
|
||||
|
||||
rawIgnoredCIDRs, _ := parser.GetStringAnnotation("global-rate-limit-ignored-cidrs", ing)
|
||||
ignoredCIDRs, err := net.ParseCIDRs(rawIgnoredCIDRs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Namespace = strings.Replace(string(ing.UID), "-", "", -1)
|
||||
config.Limit = limit
|
||||
config.WindowSize = int(windowSize.Seconds())
|
||||
config.Key = key
|
||||
config.IgnoredCIDRs = ignoredCIDRs
|
||||
|
||||
return config, nil
|
||||
}
|
||||
179
internal/ingress/annotations/globalratelimit/main_test.go
Normal file
179
internal/ingress/annotations/globalratelimit/main_test.go
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
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 globalratelimit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
api "k8s.io/api/core/v1"
|
||||
networking "k8s.io/api/networking/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const UID = "31285d47-b150-4dcf-bd6f-12c46d769f6e"
|
||||
const expectedUID = "31285d47b1504dcfbd6f12c46d769f6e"
|
||||
|
||||
func buildIngress() *networking.Ingress {
|
||||
defaultBackend := networking.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
}
|
||||
|
||||
return &networking.Ingress{
|
||||
ObjectMeta: meta_v1.ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: api.NamespaceDefault,
|
||||
UID: UID,
|
||||
},
|
||||
Spec: networking.IngressSpec{
|
||||
Backend: &networking.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Rules: []networking.IngressRule{
|
||||
{
|
||||
Host: "foo.bar.com",
|
||||
IngressRuleValue: networking.IngressRuleValue{
|
||||
HTTP: &networking.HTTPIngressRuleValue{
|
||||
Paths: []networking.HTTPIngressPath{
|
||||
{
|
||||
Path: "/foo",
|
||||
Backend: defaultBackend,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type mockBackend struct {
|
||||
resolver.Mock
|
||||
}
|
||||
|
||||
func TestGlobalRateLimiting(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
annRateLimit := parser.GetAnnotationWithPrefix("global-rate-limit")
|
||||
annRateLimitWindow := parser.GetAnnotationWithPrefix("global-rate-limit-window")
|
||||
annRateLimitKey := parser.GetAnnotationWithPrefix("global-rate-limit-key")
|
||||
annRateLimitIgnoredCIDRs := parser.GetAnnotationWithPrefix("global-rate-limit-ignored-cidrs")
|
||||
|
||||
testCases := []struct {
|
||||
title string
|
||||
annotations map[string]string
|
||||
expectedConfig *Config
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
"no annotation",
|
||||
nil,
|
||||
&Config{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"minimum required annotations",
|
||||
map[string]string{
|
||||
annRateLimit: "100",
|
||||
annRateLimitWindow: "2m",
|
||||
},
|
||||
&Config{
|
||||
Namespace: expectedUID,
|
||||
Limit: 100,
|
||||
WindowSize: 120,
|
||||
Key: "$remote_addr",
|
||||
IgnoredCIDRs: make([]string, 0),
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"global-rate-limit-key annotation",
|
||||
map[string]string{
|
||||
annRateLimit: "100",
|
||||
annRateLimitWindow: "2m",
|
||||
annRateLimitKey: "$http_x_api_user",
|
||||
},
|
||||
&Config{
|
||||
Namespace: expectedUID,
|
||||
Limit: 100,
|
||||
WindowSize: 120,
|
||||
Key: "$http_x_api_user",
|
||||
IgnoredCIDRs: make([]string, 0),
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"global-rate-limit-ignored-cidrs annotation",
|
||||
map[string]string{
|
||||
annRateLimit: "100",
|
||||
annRateLimitWindow: "2m",
|
||||
annRateLimitKey: "$http_x_api_user",
|
||||
annRateLimitIgnoredCIDRs: "127.0.0.1, 200.200.24.0/24",
|
||||
},
|
||||
&Config{
|
||||
Namespace: expectedUID,
|
||||
Limit: 100,
|
||||
WindowSize: 120,
|
||||
Key: "$http_x_api_user",
|
||||
IgnoredCIDRs: []string{"127.0.0.1", "200.200.24.0/24"},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"incorrect duration for window",
|
||||
map[string]string{
|
||||
annRateLimit: "100",
|
||||
annRateLimitWindow: "2mb",
|
||||
annRateLimitKey: "$http_x_api_user",
|
||||
},
|
||||
&Config{},
|
||||
ing_errors.LocationDenied{
|
||||
Reason: errors.Wrap(fmt.Errorf(`time: unknown unit "mb" in duration "2mb"`),
|
||||
"failed to parse 'global-rate-limit-window' value"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
ing.SetAnnotations(testCase.annotations)
|
||||
|
||||
i, actualErr := NewParser(mockBackend{}).Parse(ing)
|
||||
if (testCase.expectedErr == nil || actualErr == nil) && testCase.expectedErr != actualErr {
|
||||
t.Errorf("expected error 'nil' but got '%v'", actualErr)
|
||||
} else if testCase.expectedErr != nil && actualErr != nil &&
|
||||
testCase.expectedErr.Error() != actualErr.Error() {
|
||||
t.Errorf("expected error '%v' but got '%v'", testCase.expectedErr, actualErr)
|
||||
}
|
||||
|
||||
actualConfig := i.(*Config)
|
||||
if !testCase.expectedConfig.Equal(actualConfig) {
|
||||
expectedJSON, _ := json.Marshal(testCase.expectedConfig)
|
||||
actualJSON, _ := json.Marshal(actualConfig)
|
||||
t.Errorf("%v: expected config '%s' but got '%s'", testCase.title, expectedJSON, actualJSON)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue