mirror of https://github.com/k3s-io/k3s
503 lines
15 KiB
Go
503 lines
15 KiB
Go
/*
|
|
Copyright 2017 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 eventratelimit
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/clock"
|
|
"k8s.io/apiserver/pkg/admission"
|
|
"k8s.io/apiserver/pkg/authentication/user"
|
|
api "k8s.io/kubernetes/pkg/apis/core"
|
|
eventratelimitapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
|
|
)
|
|
|
|
const (
|
|
qps = 1
|
|
eventKind = "Event"
|
|
nonEventKind = "NonEvent"
|
|
)
|
|
|
|
// attributesForRequest generates the admission.Attributes that for the specified request
|
|
func attributesForRequest(rq request) admission.Attributes {
|
|
return admission.NewAttributesRecord(
|
|
rq.event,
|
|
nil,
|
|
api.Kind(rq.kind).WithVersion("version"),
|
|
rq.namespace,
|
|
"name",
|
|
api.Resource("resource").WithVersion("version"),
|
|
"",
|
|
admission.Create,
|
|
&user.DefaultInfo{Name: rq.username})
|
|
}
|
|
|
|
type request struct {
|
|
kind string
|
|
namespace string
|
|
username string
|
|
event *api.Event
|
|
delay time.Duration
|
|
accepted bool
|
|
}
|
|
|
|
func newRequest(kind string) request {
|
|
return request{
|
|
kind: kind,
|
|
accepted: true,
|
|
}
|
|
}
|
|
|
|
func newEventRequest() request {
|
|
return newRequest(eventKind)
|
|
}
|
|
|
|
func newNonEventRequest() request {
|
|
return newRequest(nonEventKind)
|
|
}
|
|
|
|
func (r request) withNamespace(namespace string) request {
|
|
r.namespace = namespace
|
|
return r
|
|
}
|
|
|
|
func (r request) withEvent(event *api.Event) request {
|
|
r.event = event
|
|
return r
|
|
}
|
|
|
|
func (r request) withEventComponent(component string) request {
|
|
return r.withEvent(&api.Event{
|
|
Source: api.EventSource{
|
|
Component: component,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (r request) withUser(name string) request {
|
|
r.username = name
|
|
return r
|
|
}
|
|
|
|
func (r request) blocked() request {
|
|
r.accepted = false
|
|
return r
|
|
}
|
|
|
|
// withDelay will adjust the clock to simulate the specified delay, in seconds
|
|
func (r request) withDelay(delayInSeconds int) request {
|
|
r.delay = time.Duration(delayInSeconds) * time.Second
|
|
return r
|
|
}
|
|
|
|
// createSourceAndObjectKeyInclusionRequests creates a series of requests that can be used
|
|
// to test that a particular part of the event is included in the source+object key
|
|
func createSourceAndObjectKeyInclusionRequests(eventFactory func(label string) *api.Event) []request {
|
|
return []request{
|
|
newEventRequest().withEvent(eventFactory("A")),
|
|
newEventRequest().withEvent(eventFactory("A")).blocked(),
|
|
newEventRequest().withEvent(eventFactory("B")),
|
|
}
|
|
}
|
|
|
|
func TestEventRateLimiting(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
serverBurst int32
|
|
namespaceBurst int32
|
|
namespaceCacheSize int32
|
|
sourceAndObjectBurst int32
|
|
sourceAndObjectCacheSize int32
|
|
userBurst int32
|
|
userCacheSize int32
|
|
requests []request
|
|
}{
|
|
{
|
|
name: "event not blocked when tokens available",
|
|
serverBurst: 3,
|
|
requests: []request{
|
|
newEventRequest(),
|
|
},
|
|
},
|
|
{
|
|
name: "non-event not blocked",
|
|
serverBurst: 3,
|
|
requests: []request{
|
|
newNonEventRequest(),
|
|
},
|
|
},
|
|
{
|
|
name: "event blocked after tokens exhausted",
|
|
serverBurst: 3,
|
|
requests: []request{
|
|
newEventRequest(),
|
|
newEventRequest(),
|
|
newEventRequest(),
|
|
newEventRequest().blocked(),
|
|
},
|
|
},
|
|
{
|
|
name: "non-event not blocked after tokens exhausted",
|
|
serverBurst: 3,
|
|
requests: []request{
|
|
newEventRequest(),
|
|
newEventRequest(),
|
|
newEventRequest(),
|
|
newNonEventRequest(),
|
|
},
|
|
},
|
|
{
|
|
name: "non-events should not count against limit",
|
|
serverBurst: 3,
|
|
requests: []request{
|
|
newEventRequest(),
|
|
newEventRequest(),
|
|
newNonEventRequest(),
|
|
newEventRequest(),
|
|
},
|
|
},
|
|
{
|
|
name: "event accepted after token refill",
|
|
serverBurst: 3,
|
|
requests: []request{
|
|
newEventRequest(),
|
|
newEventRequest(),
|
|
newEventRequest(),
|
|
newEventRequest().blocked(),
|
|
newEventRequest().withDelay(1),
|
|
},
|
|
},
|
|
{
|
|
name: "event blocked by namespace limits",
|
|
serverBurst: 100,
|
|
namespaceBurst: 3,
|
|
namespaceCacheSize: 10,
|
|
requests: []request{
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("A").blocked(),
|
|
},
|
|
},
|
|
{
|
|
name: "event from other namespace not blocked",
|
|
serverBurst: 100,
|
|
namespaceBurst: 3,
|
|
namespaceCacheSize: 10,
|
|
requests: []request{
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("B"),
|
|
},
|
|
},
|
|
{
|
|
name: "events from other namespaces should not count against limit",
|
|
serverBurst: 100,
|
|
namespaceBurst: 3,
|
|
namespaceCacheSize: 10,
|
|
requests: []request{
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("B"),
|
|
newEventRequest().withNamespace("A"),
|
|
},
|
|
},
|
|
{
|
|
name: "event accepted after namespace token refill",
|
|
serverBurst: 100,
|
|
namespaceBurst: 3,
|
|
namespaceCacheSize: 10,
|
|
requests: []request{
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("A").blocked(),
|
|
newEventRequest().withNamespace("A").withDelay(1),
|
|
},
|
|
},
|
|
{
|
|
name: "event from other namespaces should not clear namespace limits",
|
|
serverBurst: 100,
|
|
namespaceBurst: 3,
|
|
namespaceCacheSize: 10,
|
|
requests: []request{
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("B"),
|
|
newEventRequest().withNamespace("A").blocked(),
|
|
},
|
|
},
|
|
{
|
|
name: "namespace limits from lru namespace should clear when cache size exceeded",
|
|
serverBurst: 100,
|
|
namespaceBurst: 3,
|
|
namespaceCacheSize: 2,
|
|
requests: []request{
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("B"),
|
|
newEventRequest().withNamespace("B"),
|
|
newEventRequest().withNamespace("B"),
|
|
newEventRequest().withNamespace("A"),
|
|
newEventRequest().withNamespace("B").blocked(),
|
|
newEventRequest().withNamespace("A").blocked(),
|
|
// This should clear out namespace B from the lru cache
|
|
newEventRequest().withNamespace("C"),
|
|
newEventRequest().withNamespace("A").blocked(),
|
|
newEventRequest().withNamespace("B"),
|
|
},
|
|
},
|
|
{
|
|
name: "event blocked by source+object limits",
|
|
serverBurst: 100,
|
|
sourceAndObjectBurst: 3,
|
|
sourceAndObjectCacheSize: 10,
|
|
requests: []request{
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("A").blocked(),
|
|
},
|
|
},
|
|
{
|
|
name: "event from other source+object not blocked",
|
|
serverBurst: 100,
|
|
sourceAndObjectBurst: 3,
|
|
sourceAndObjectCacheSize: 10,
|
|
requests: []request{
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("B"),
|
|
},
|
|
},
|
|
{
|
|
name: "events from other source+object should not count against limit",
|
|
serverBurst: 100,
|
|
sourceAndObjectBurst: 3,
|
|
sourceAndObjectCacheSize: 10,
|
|
requests: []request{
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("B"),
|
|
newEventRequest().withEventComponent("A"),
|
|
},
|
|
},
|
|
{
|
|
name: "event accepted after source+object token refill",
|
|
serverBurst: 100,
|
|
sourceAndObjectBurst: 3,
|
|
sourceAndObjectCacheSize: 10,
|
|
requests: []request{
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("A").blocked(),
|
|
newEventRequest().withEventComponent("A").withDelay(1),
|
|
},
|
|
},
|
|
{
|
|
name: "event from other source+object should not clear source+object limits",
|
|
serverBurst: 100,
|
|
sourceAndObjectBurst: 3,
|
|
sourceAndObjectCacheSize: 10,
|
|
requests: []request{
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("B"),
|
|
newEventRequest().withEventComponent("A").blocked(),
|
|
},
|
|
},
|
|
{
|
|
name: "source+object limits from lru source+object should clear when cache size exceeded",
|
|
serverBurst: 100,
|
|
sourceAndObjectBurst: 3,
|
|
sourceAndObjectCacheSize: 2,
|
|
requests: []request{
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("B"),
|
|
newEventRequest().withEventComponent("B"),
|
|
newEventRequest().withEventComponent("B"),
|
|
newEventRequest().withEventComponent("A"),
|
|
newEventRequest().withEventComponent("B").blocked(),
|
|
newEventRequest().withEventComponent("A").blocked(),
|
|
// This should clear out component B from the lru cache
|
|
newEventRequest().withEventComponent("C"),
|
|
newEventRequest().withEventComponent("A").blocked(),
|
|
newEventRequest().withEventComponent("B"),
|
|
},
|
|
},
|
|
{
|
|
name: "source host should be included in source+object key",
|
|
serverBurst: 100,
|
|
sourceAndObjectBurst: 1,
|
|
sourceAndObjectCacheSize: 10,
|
|
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
|
|
return &api.Event{Source: api.EventSource{Host: label}}
|
|
}),
|
|
},
|
|
{
|
|
name: "involved object kind should be included in source+object key",
|
|
serverBurst: 100,
|
|
sourceAndObjectBurst: 1,
|
|
sourceAndObjectCacheSize: 10,
|
|
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
|
|
return &api.Event{InvolvedObject: api.ObjectReference{Kind: label}}
|
|
}),
|
|
},
|
|
{
|
|
name: "involved object namespace should be included in source+object key",
|
|
serverBurst: 100,
|
|
sourceAndObjectBurst: 1,
|
|
sourceAndObjectCacheSize: 10,
|
|
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
|
|
return &api.Event{InvolvedObject: api.ObjectReference{Namespace: label}}
|
|
}),
|
|
},
|
|
{
|
|
name: "involved object name should be included in source+object key",
|
|
serverBurst: 100,
|
|
sourceAndObjectBurst: 1,
|
|
sourceAndObjectCacheSize: 10,
|
|
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
|
|
return &api.Event{InvolvedObject: api.ObjectReference{Name: label}}
|
|
}),
|
|
},
|
|
{
|
|
name: "involved object UID should be included in source+object key",
|
|
serverBurst: 100,
|
|
sourceAndObjectBurst: 1,
|
|
sourceAndObjectCacheSize: 10,
|
|
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
|
|
return &api.Event{InvolvedObject: api.ObjectReference{UID: types.UID(label)}}
|
|
}),
|
|
},
|
|
{
|
|
name: "involved object APIVersion should be included in source+object key",
|
|
serverBurst: 100,
|
|
sourceAndObjectBurst: 1,
|
|
sourceAndObjectCacheSize: 10,
|
|
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
|
|
return &api.Event{InvolvedObject: api.ObjectReference{APIVersion: label}}
|
|
}),
|
|
},
|
|
{
|
|
name: "event blocked by user limits",
|
|
userBurst: 3,
|
|
userCacheSize: 10,
|
|
requests: []request{
|
|
newEventRequest().withUser("A"),
|
|
newEventRequest().withUser("A"),
|
|
newEventRequest().withUser("A"),
|
|
newEventRequest().withUser("A").blocked(),
|
|
},
|
|
},
|
|
{
|
|
name: "event from other user not blocked",
|
|
requests: []request{
|
|
newEventRequest().withUser("A"),
|
|
newEventRequest().withUser("A"),
|
|
newEventRequest().withUser("A"),
|
|
newEventRequest().withUser("B"),
|
|
},
|
|
},
|
|
{
|
|
name: "events from other user should not count against limit",
|
|
requests: []request{
|
|
newEventRequest().withUser("A"),
|
|
newEventRequest().withUser("A"),
|
|
newEventRequest().withUser("B"),
|
|
newEventRequest().withUser("A"),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
clock := clock.NewFakeClock(time.Now())
|
|
config := &eventratelimitapi.Configuration{}
|
|
if tc.serverBurst > 0 {
|
|
serverLimit := eventratelimitapi.Limit{
|
|
Type: eventratelimitapi.ServerLimitType,
|
|
QPS: qps,
|
|
Burst: tc.serverBurst,
|
|
}
|
|
config.Limits = append(config.Limits, serverLimit)
|
|
}
|
|
if tc.namespaceBurst > 0 {
|
|
namespaceLimit := eventratelimitapi.Limit{
|
|
Type: eventratelimitapi.NamespaceLimitType,
|
|
Burst: tc.namespaceBurst,
|
|
QPS: qps,
|
|
CacheSize: tc.namespaceCacheSize,
|
|
}
|
|
config.Limits = append(config.Limits, namespaceLimit)
|
|
}
|
|
if tc.userBurst > 0 {
|
|
userLimit := eventratelimitapi.Limit{
|
|
Type: eventratelimitapi.UserLimitType,
|
|
Burst: tc.userBurst,
|
|
QPS: qps,
|
|
CacheSize: tc.userCacheSize,
|
|
}
|
|
config.Limits = append(config.Limits, userLimit)
|
|
}
|
|
if tc.sourceAndObjectBurst > 0 {
|
|
sourceAndObjectLimit := eventratelimitapi.Limit{
|
|
Type: eventratelimitapi.SourceAndObjectLimitType,
|
|
Burst: tc.sourceAndObjectBurst,
|
|
QPS: qps,
|
|
CacheSize: tc.sourceAndObjectCacheSize,
|
|
}
|
|
config.Limits = append(config.Limits, sourceAndObjectLimit)
|
|
}
|
|
eventratelimit, err := newEventRateLimit(config, clock)
|
|
if err != nil {
|
|
t.Fatalf("%v: Could not create EventRateLimit: %v", tc.name, err)
|
|
}
|
|
|
|
for rqIndex, rq := range tc.requests {
|
|
if rq.delay > 0 {
|
|
clock.Step(rq.delay)
|
|
}
|
|
attributes := attributesForRequest(rq)
|
|
err = eventratelimit.Validate(attributes)
|
|
if rq.accepted != (err == nil) {
|
|
expectedAction := "admitted"
|
|
if !rq.accepted {
|
|
expectedAction = "blocked"
|
|
}
|
|
t.Fatalf("%v: Request %v should have been %v: %v", tc.name, rqIndex, expectedAction, err)
|
|
}
|
|
if err != nil {
|
|
statusErr, ok := err.(*errors.StatusError)
|
|
if ok && statusErr.ErrStatus.Code != errors.StatusTooManyRequests {
|
|
t.Fatalf("%v: Request %v should yield a 429 response: %v", tc.name, rqIndex, err)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|