diff --git a/staging/BUILD b/staging/BUILD index f86f9b10acb..b997ee21102 100644 --- a/staging/BUILD +++ b/staging/BUILD @@ -117,6 +117,7 @@ filegroup( "//staging/src/k8s.io/apiserver/pkg/authentication/request/websocket:all-srcs", "//staging/src/k8s.io/apiserver/pkg/authentication/request/x509:all-srcs", "//staging/src/k8s.io/apiserver/pkg/authentication/serviceaccount:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/authentication/token/cache:all-srcs", "//staging/src/k8s.io/apiserver/pkg/authentication/token/tokenfile:all-srcs", "//staging/src/k8s.io/apiserver/pkg/authentication/user:all-srcs", "//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:all-srcs", diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/BUILD b/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/BUILD new file mode 100644 index 00000000000..3384859f723 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/BUILD @@ -0,0 +1,54 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_test( + name = "go_default_test", + srcs = [ + "cache_test.go", + "cached_token_authenticator_test.go", + ], + library = ":go_default_library", + tags = ["automanaged"], + deps = [ + "//vendor/github.com/pborman/uuid:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/clock:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", + ], +) + +go_library( + name = "go_default_library", + srcs = [ + "cache_simple.go", + "cache_striped.go", + "cached_token_authenticator.go", + ], + tags = ["automanaged"], + deps = [ + "//vendor/k8s.io/apimachinery/pkg/util/cache:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/clock:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cache_simple.go b/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cache_simple.go new file mode 100644 index 00000000000..18d5692d7a7 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cache_simple.go @@ -0,0 +1,49 @@ +/* +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 cache + +import ( + "time" + + lrucache "k8s.io/apimachinery/pkg/util/cache" + "k8s.io/apimachinery/pkg/util/clock" +) + +type simpleCache struct { + lru *lrucache.LRUExpireCache +} + +func newSimpleCache(size int, clock clock.Clock) cache { + return &simpleCache{lru: lrucache.NewLRUExpireCacheWithClock(size, clock)} +} + +func (c *simpleCache) get(key string) (*cacheRecord, bool) { + record, ok := c.lru.Get(key) + if !ok { + return nil, false + } + value, ok := record.(*cacheRecord) + return value, ok +} + +func (c *simpleCache) set(key string, value *cacheRecord, ttl time.Duration) { + c.lru.Add(key, value, ttl) +} + +func (c *simpleCache) remove(key string) { + c.lru.Remove(key) +} diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cache_striped.go b/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cache_striped.go new file mode 100644 index 00000000000..b791260fc24 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cache_striped.go @@ -0,0 +1,60 @@ +/* +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 cache + +import ( + "hash/fnv" + "time" +) + +// split cache lookups across N striped caches +type stripedCache struct { + stripeCount uint32 + keyFunc func(string) uint32 + caches []cache +} + +type keyFunc func(string) uint32 +type newCacheFunc func() cache + +func newStripedCache(stripeCount int, keyFunc keyFunc, newCacheFunc newCacheFunc) cache { + caches := []cache{} + for i := 0; i < stripeCount; i++ { + caches = append(caches, newCacheFunc()) + } + return &stripedCache{ + stripeCount: uint32(stripeCount), + keyFunc: keyFunc, + caches: caches, + } +} + +func (c *stripedCache) get(key string) (*cacheRecord, bool) { + return c.caches[c.keyFunc(key)%c.stripeCount].get(key) +} +func (c *stripedCache) set(key string, value *cacheRecord, ttl time.Duration) { + c.caches[c.keyFunc(key)%c.stripeCount].set(key, value, ttl) +} +func (c *stripedCache) remove(key string) { + c.caches[c.keyFunc(key)%c.stripeCount].remove(key) +} + +func fnvKeyFunc(key string) uint32 { + f := fnv.New32() + f.Write([]byte(key)) + return f.Sum32() +} diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cache_test.go b/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cache_test.go new file mode 100644 index 00000000000..d4e9adff7a7 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cache_test.go @@ -0,0 +1,94 @@ +/* +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 cache + +import ( + "math/rand" + "testing" + "time" + + "k8s.io/apimachinery/pkg/util/clock" + "k8s.io/apiserver/pkg/authentication/user" + + "github.com/pborman/uuid" +) + +func TestSimpleCache(t *testing.T) { + testCache(newSimpleCache(4096, clock.RealClock{}), t) +} + +func BenchmarkSimpleCache(b *testing.B) { + benchmarkCache(newSimpleCache(4096, clock.RealClock{}), b) +} + +func TestStripedCache(t *testing.T) { + testCache(newStripedCache(32, fnvKeyFunc, func() cache { return newSimpleCache(128, clock.RealClock{}) }), t) +} + +func BenchmarkStripedCache(b *testing.B) { + benchmarkCache(newStripedCache(32, fnvKeyFunc, func() cache { return newSimpleCache(128, clock.RealClock{}) }), b) +} + +func benchmarkCache(cache cache, b *testing.B) { + keys := []string{} + for i := 0; i < b.N; i++ { + key := uuid.NewRandom().String() + keys = append(keys, key) + } + + b.ResetTimer() + + b.SetParallelism(500) + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + key := keys[rand.Intn(b.N)] + _, ok := cache.get(key) + if ok { + cache.remove(key) + } else { + cache.set(key, &cacheRecord{}, time.Second) + } + } + }) +} + +func testCache(cache cache, t *testing.T) { + if result, ok := cache.get("foo"); ok || result != nil { + t.Errorf("Expected null, false, got %#v, %v", result, ok) + } + + record1 := &cacheRecord{user: &user.DefaultInfo{Name: "bob"}} + record2 := &cacheRecord{user: &user.DefaultInfo{Name: "alice"}} + + // when empty, record is stored + cache.set("foo", record1, time.Hour) + if result, ok := cache.get("foo"); !ok || result != record1 { + t.Errorf("Expected %#v, true, got %#v, %v", record1, ok) + } + + // newer record overrides + cache.set("foo", record2, time.Hour) + if result, ok := cache.get("foo"); !ok || result != record2 { + t.Errorf("Expected %#v, true, got %#v, %v", record2, ok) + } + + // removing the current value removes + cache.remove("foo") + if result, ok := cache.get("foo"); ok || result != nil { + t.Errorf("Expected null, false, got %#v, %v", result, ok) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cached_token_authenticator.go b/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cached_token_authenticator.go new file mode 100644 index 00000000000..d2fd28d2346 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cached_token_authenticator.go @@ -0,0 +1,82 @@ +/* +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 cache + +import ( + "time" + + utilclock "k8s.io/apimachinery/pkg/util/clock" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" +) + +// cacheRecord holds the three return values of the authenticator.Token AuthenticateToken method +type cacheRecord struct { + user user.Info + ok bool + err error +} + +type cachedTokenAuthenticator struct { + authenticator authenticator.Token + + successTTL time.Duration + failureTTL time.Duration + + cache cache +} + +type cache interface { + // given a key, return the record, and whether or not it existed + get(key string) (value *cacheRecord, exists bool) + // caches the record for the key + set(key string, value *cacheRecord, ttl time.Duration) + // removes the record for the key + remove(key string) +} + +// New returns a token authenticator that caches the results of the specified authenticator. A ttl of 0 bypasses the cache. +func New(authenticator authenticator.Token, successTTL, failureTTL time.Duration) authenticator.Token { + return newWithClock(authenticator, successTTL, failureTTL, utilclock.RealClock{}) +} + +func newWithClock(authenticator authenticator.Token, successTTL, failureTTL time.Duration, clock utilclock.Clock) authenticator.Token { + return &cachedTokenAuthenticator{ + authenticator: authenticator, + successTTL: successTTL, + failureTTL: failureTTL, + cache: newStripedCache(32, fnvKeyFunc, func() cache { return newSimpleCache(128, clock) }), + } +} + +// AuthenticateToken implements authenticator.Token +func (a *cachedTokenAuthenticator) AuthenticateToken(token string) (user.Info, bool, error) { + if record, ok := a.cache.get(token); ok { + return record.user, record.ok, record.err + } + + user, ok, err := a.authenticator.AuthenticateToken(token) + + switch { + case ok && a.successTTL > 0: + a.cache.set(token, &cacheRecord{user: user, ok: ok, err: err}, a.successTTL) + case !ok && a.failureTTL > 0: + a.cache.set(token, &cacheRecord{user: user, ok: ok, err: err}, a.failureTTL) + } + + return user, ok, err +} diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cached_token_authenticator_test.go b/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cached_token_authenticator_test.go new file mode 100644 index 00000000000..200d1147841 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cached_token_authenticator_test.go @@ -0,0 +1,105 @@ +/* +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 cache + +import ( + "reflect" + "testing" + "time" + + utilclock "k8s.io/apimachinery/pkg/util/clock" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" +) + +func TestCachedTokenAuthenticator(t *testing.T) { + var ( + calledWithToken []string + + resultUsers map[string]user.Info + resultOk bool + resultErr error + ) + fakeAuth := authenticator.TokenFunc(func(token string) (user.Info, bool, error) { + calledWithToken = append(calledWithToken, token) + return resultUsers[token], resultOk, resultErr + }) + fakeClock := utilclock.NewFakeClock(time.Now()) + + a := newWithClock(fakeAuth, time.Minute, 0, fakeClock) + + calledWithToken, resultUsers, resultOk, resultErr = []string{}, nil, false, nil + a.AuthenticateToken("bad1") + a.AuthenticateToken("bad2") + a.AuthenticateToken("bad3") + a.AuthenticateToken("bad1") + a.AuthenticateToken("bad2") + a.AuthenticateToken("bad3") + if !reflect.DeepEqual(calledWithToken, []string{"bad1", "bad2", "bad3", "bad1", "bad2", "bad3"}) { + t.Errorf("Expected failing calls to bypass cache, got %v", calledWithToken) + } + + // reset calls, make the backend return success for three user tokens + calledWithToken = []string{} + resultUsers, resultOk, resultErr = map[string]user.Info{}, true, nil + resultUsers["usertoken1"] = &user.DefaultInfo{Name: "user1"} + resultUsers["usertoken2"] = &user.DefaultInfo{Name: "user2"} + resultUsers["usertoken3"] = &user.DefaultInfo{Name: "user3"} + + // populate cache + if user, ok, err := a.AuthenticateToken("usertoken1"); err != nil || !ok || user.GetName() != "user1" { + t.Errorf("Expected user1") + } + if user, ok, err := a.AuthenticateToken("usertoken2"); err != nil || !ok || user.GetName() != "user2" { + t.Errorf("Expected user2") + } + if user, ok, err := a.AuthenticateToken("usertoken3"); err != nil || !ok || user.GetName() != "user3" { + t.Errorf("Expected user3") + } + if !reflect.DeepEqual(calledWithToken, []string{"usertoken1", "usertoken2", "usertoken3"}) { + t.Errorf("Expected token calls, got %v", calledWithToken) + } + + // reset calls, make the backend return failures + calledWithToken = []string{} + resultUsers, resultOk, resultErr = nil, false, nil + + // authenticate calls still succeed and backend is not hit + if user, ok, err := a.AuthenticateToken("usertoken1"); err != nil || !ok || user.GetName() != "user1" { + t.Errorf("Expected user1") + } + if user, ok, err := a.AuthenticateToken("usertoken2"); err != nil || !ok || user.GetName() != "user2" { + t.Errorf("Expected user2") + } + if user, ok, err := a.AuthenticateToken("usertoken3"); err != nil || !ok || user.GetName() != "user3" { + t.Errorf("Expected user3") + } + if !reflect.DeepEqual(calledWithToken, []string{}) { + t.Errorf("Expected no token calls, got %v", calledWithToken) + } + + // skip forward in time + fakeClock.Step(2 * time.Minute) + + // backend is consulted again and fails + a.AuthenticateToken("usertoken1") + a.AuthenticateToken("usertoken2") + a.AuthenticateToken("usertoken3") + if !reflect.DeepEqual(calledWithToken, []string{"usertoken1", "usertoken2", "usertoken3"}) { + t.Errorf("Expected token calls, got %v", calledWithToken) + } +}