770 lines
23 KiB
Go
770 lines
23 KiB
Go
/*
|
|
Copyright 2016 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 etcd3
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
clientv3 "go.etcd.io/etcd/client/v3"
|
|
"go.etcd.io/etcd/server/v3/embed"
|
|
"google.golang.org/grpc/grpclog"
|
|
|
|
"k8s.io/apimachinery/pkg/api/apitesting"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/fields"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
"k8s.io/apiserver/pkg/apis/example"
|
|
examplev1 "k8s.io/apiserver/pkg/apis/example/v1"
|
|
"k8s.io/apiserver/pkg/storage"
|
|
"k8s.io/apiserver/pkg/storage/etcd3/testserver"
|
|
storagetesting "k8s.io/apiserver/pkg/storage/testing"
|
|
"k8s.io/apiserver/pkg/storage/value"
|
|
)
|
|
|
|
var scheme = runtime.NewScheme()
|
|
var codecs = serializer.NewCodecFactory(scheme)
|
|
|
|
const defaultTestPrefix = "test!"
|
|
|
|
func init() {
|
|
metav1.AddToGroupVersion(scheme, metav1.SchemeGroupVersion)
|
|
utilruntime.Must(example.AddToScheme(scheme))
|
|
utilruntime.Must(examplev1.AddToScheme(scheme))
|
|
|
|
grpclog.SetLoggerV2(grpclog.NewLoggerV2(ioutil.Discard, ioutil.Discard, os.Stderr))
|
|
}
|
|
|
|
func newPod() runtime.Object {
|
|
return &example.Pod{}
|
|
}
|
|
|
|
func newPodList() runtime.Object {
|
|
return &example.PodList{}
|
|
}
|
|
|
|
func checkStorageInvariants(etcdClient *clientv3.Client, codec runtime.Codec) storagetesting.KeyValidation {
|
|
return func(ctx context.Context, t *testing.T, key string) {
|
|
getResp, err := etcdClient.KV.Get(ctx, key)
|
|
if err != nil {
|
|
t.Fatalf("etcdClient.KV.Get failed: %v", err)
|
|
}
|
|
if len(getResp.Kvs) == 0 {
|
|
t.Fatalf("expecting non empty result on key: %s", key)
|
|
}
|
|
decoded, err := runtime.Decode(codec, getResp.Kvs[0].Value[len(defaultTestPrefix):])
|
|
if err != nil {
|
|
t.Fatalf("expecting successful decode of object from %v\n%v", err, string(getResp.Kvs[0].Value))
|
|
}
|
|
obj := decoded.(*example.Pod)
|
|
if obj.ResourceVersion != "" {
|
|
t.Errorf("stored object should have empty resource version")
|
|
}
|
|
if obj.SelfLink != "" {
|
|
t.Errorf("stored output should have empty selfLink")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreate(t *testing.T) {
|
|
ctx, store, etcdClient := testSetup(t)
|
|
storagetesting.RunTestCreate(ctx, t, store, checkStorageInvariants(etcdClient, store.codec))
|
|
}
|
|
|
|
func TestCreateWithTTL(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestCreateWithTTL(ctx, t, store)
|
|
}
|
|
|
|
func TestCreateWithKeyExist(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestCreateWithKeyExist(ctx, t, store)
|
|
}
|
|
|
|
func TestGet(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestGet(ctx, t, store)
|
|
}
|
|
|
|
func TestUnconditionalDelete(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestUnconditionalDelete(ctx, t, store)
|
|
}
|
|
|
|
func TestConditionalDelete(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestConditionalDelete(ctx, t, store)
|
|
}
|
|
|
|
func TestDeleteWithSuggestion(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestDeleteWithSuggestion(ctx, t, store)
|
|
}
|
|
|
|
func TestDeleteWithSuggestionAndConflict(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestDeleteWithSuggestionAndConflict(ctx, t, store)
|
|
}
|
|
|
|
func TestDeleteWithSuggestionOfDeletedObject(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestDeleteWithSuggestionOfDeletedObject(ctx, t, store)
|
|
}
|
|
|
|
func TestValidateDeletionWithSuggestion(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestValidateDeletionWithSuggestion(ctx, t, store)
|
|
}
|
|
|
|
func TestValidateDeletionWithOnlySuggestionValid(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestValidateDeletionWithOnlySuggestionValid(ctx, t, store)
|
|
}
|
|
|
|
func TestDeleteWithConflict(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestDeleteWithConflict(ctx, t, store)
|
|
}
|
|
|
|
func TestPreconditionalDeleteWithSuggestion(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestPreconditionalDeleteWithSuggestion(ctx, t, store)
|
|
}
|
|
|
|
func TestPreconditionalDeleteWithSuggestionPass(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestPreconditionalDeleteWithOnlySuggestionPass(ctx, t, store)
|
|
}
|
|
|
|
func TestGetListNonRecursive(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestGetListNonRecursive(ctx, t, store)
|
|
}
|
|
|
|
type storeWithPrefixTransformer struct {
|
|
*store
|
|
}
|
|
|
|
func (s *storeWithPrefixTransformer) UpdatePrefixTransformer(modifier storagetesting.PrefixTransformerModifier) func() {
|
|
originalTransformer := s.transformer.(*storagetesting.PrefixTransformer)
|
|
transformer := *originalTransformer
|
|
s.transformer = modifier(&transformer)
|
|
s.watcher.transformer = modifier(&transformer)
|
|
return func() {
|
|
s.transformer = originalTransformer
|
|
s.watcher.transformer = originalTransformer
|
|
}
|
|
}
|
|
|
|
func TestGuaranteedUpdate(t *testing.T) {
|
|
ctx, store, etcdClient := testSetup(t)
|
|
storagetesting.RunTestGuaranteedUpdate(ctx, t, &storeWithPrefixTransformer{store}, checkStorageInvariants(etcdClient, store.codec))
|
|
}
|
|
|
|
func TestGuaranteedUpdateWithTTL(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestGuaranteedUpdateWithTTL(ctx, t, store)
|
|
}
|
|
|
|
func TestGuaranteedUpdateChecksStoredData(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestGuaranteedUpdateChecksStoredData(ctx, t, &storeWithPrefixTransformer{store})
|
|
}
|
|
|
|
func TestGuaranteedUpdateWithConflict(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestGuaranteedUpdateWithConflict(ctx, t, store)
|
|
}
|
|
|
|
func TestGuaranteedUpdateWithSuggestionAndConflict(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestGuaranteedUpdateWithSuggestionAndConflict(ctx, t, store)
|
|
}
|
|
|
|
func TestTransformationFailure(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestTransformationFailure(ctx, t, &storeWithPrefixTransformer{store})
|
|
}
|
|
|
|
func TestList(t *testing.T) {
|
|
ctx, store, client := testSetup(t)
|
|
storagetesting.RunTestList(ctx, t, store, compactStorage(client), false)
|
|
}
|
|
|
|
func checkStorageCallsInvariants(transformer *storagetesting.PrefixTransformer, recorder *clientRecorder) storagetesting.CallsValidation {
|
|
return func(t *testing.T, pageSize, estimatedProcessedObjects uint64) {
|
|
if reads := transformer.GetReadsAndReset(); reads != estimatedProcessedObjects {
|
|
t.Errorf("unexpected reads: %d, expected: %d", reads, estimatedProcessedObjects)
|
|
}
|
|
estimatedGetCalls := uint64(1)
|
|
if pageSize != 0 {
|
|
// We expect that kube-apiserver will be increasing page sizes
|
|
// if not full pages are received, so we should see significantly less
|
|
// than 1000 pages (which would be result of talking to etcd with page size
|
|
// copied from pred.Limit).
|
|
// The expected number of calls is n+1 where n is the smallest n so that:
|
|
// pageSize + pageSize * 2 + pageSize * 4 + ... + pageSize * 2^n >= podCount.
|
|
// For pageSize = 1, podCount = 1000, we get n+1 = 10, 2 ^ 10 = 1024.
|
|
currLimit := pageSize
|
|
for sum := uint64(1); sum < estimatedProcessedObjects; {
|
|
currLimit *= 2
|
|
if currLimit > maxLimit {
|
|
currLimit = maxLimit
|
|
}
|
|
sum += currLimit
|
|
estimatedGetCalls++
|
|
}
|
|
}
|
|
if reads := recorder.GetReadsAndReset(); reads != estimatedGetCalls {
|
|
t.Errorf("unexpected reads: %d", reads)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListContinuation(t *testing.T) {
|
|
ctx, store, etcdClient := testSetup(t, withRecorder())
|
|
validation := checkStorageCallsInvariants(
|
|
store.transformer.(*storagetesting.PrefixTransformer), etcdClient.KV.(*clientRecorder))
|
|
storagetesting.RunTestListContinuation(ctx, t, store, validation)
|
|
}
|
|
|
|
func TestListPaginationRareObject(t *testing.T) {
|
|
ctx, store, etcdClient := testSetup(t, withRecorder())
|
|
validation := checkStorageCallsInvariants(
|
|
store.transformer.(*storagetesting.PrefixTransformer), etcdClient.KV.(*clientRecorder))
|
|
storagetesting.RunTestListPaginationRareObject(ctx, t, store, validation)
|
|
}
|
|
|
|
func TestListContinuationWithFilter(t *testing.T) {
|
|
ctx, store, etcdClient := testSetup(t, withRecorder())
|
|
validation := checkStorageCallsInvariants(
|
|
store.transformer.(*storagetesting.PrefixTransformer), etcdClient.KV.(*clientRecorder))
|
|
storagetesting.RunTestListContinuationWithFilter(ctx, t, store, validation)
|
|
}
|
|
|
|
func compactStorage(etcdClient *clientv3.Client) storagetesting.Compaction {
|
|
return func(ctx context.Context, t *testing.T, resourceVersion string) {
|
|
versioner := storage.APIObjectVersioner{}
|
|
rv, err := versioner.ParseResourceVersion(resourceVersion)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err = compact(ctx, etcdClient, 0, int64(rv)); err != nil {
|
|
t.Fatalf("Unable to compact, %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListInconsistentContinuation(t *testing.T) {
|
|
ctx, store, client := testSetup(t)
|
|
storagetesting.RunTestListInconsistentContinuation(ctx, t, store, compactStorage(client))
|
|
}
|
|
|
|
func TestConsistentList(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestConsistentList(ctx, t, &storeWithPrefixTransformer{store})
|
|
}
|
|
|
|
func TestCount(t *testing.T) {
|
|
ctx, store, _ := testSetup(t)
|
|
storagetesting.RunTestCount(ctx, t, store)
|
|
}
|
|
|
|
// =======================================================================
|
|
// Implementation-specific tests are following.
|
|
// The following tests are exercising the details of the implementation
|
|
// not the actual user-facing contract of storage interface.
|
|
// As such, they may focus e.g. on non-functional aspects like performance
|
|
// impact.
|
|
// =======================================================================
|
|
|
|
func TestPrefix(t *testing.T) {
|
|
testcases := map[string]string{
|
|
"custom/prefix": "/custom/prefix/",
|
|
"/custom//prefix//": "/custom/prefix/",
|
|
"/registry": "/registry/",
|
|
}
|
|
for configuredPrefix, effectivePrefix := range testcases {
|
|
_, store, _ := testSetup(t, withPrefix(configuredPrefix))
|
|
if store.pathPrefix != effectivePrefix {
|
|
t.Errorf("configured prefix of %s, expected effective prefix of %s, got %s", configuredPrefix, effectivePrefix, store.pathPrefix)
|
|
}
|
|
}
|
|
}
|
|
|
|
func Test_growSlice(t *testing.T) {
|
|
type args struct {
|
|
initialCapacity int
|
|
initialLen int
|
|
v reflect.Value
|
|
maxCapacity int
|
|
sizes []int
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
cap int
|
|
len int
|
|
}{
|
|
{
|
|
name: "empty",
|
|
args: args{v: reflect.ValueOf([]example.Pod{})},
|
|
cap: 0,
|
|
},
|
|
{
|
|
name: "no sizes",
|
|
args: args{v: reflect.ValueOf([]example.Pod{}), maxCapacity: 10},
|
|
cap: 10,
|
|
},
|
|
{
|
|
name: "above maxCapacity",
|
|
args: args{v: reflect.ValueOf([]example.Pod{}), maxCapacity: 10, sizes: []int{1, 12}},
|
|
cap: 10,
|
|
},
|
|
{
|
|
name: "takes max",
|
|
args: args{v: reflect.ValueOf([]example.Pod{}), maxCapacity: 10, sizes: []int{8, 4}},
|
|
cap: 8,
|
|
},
|
|
{
|
|
name: "with existing capacity above max",
|
|
args: args{initialCapacity: 12, maxCapacity: 10, sizes: []int{8, 4}},
|
|
cap: 12,
|
|
},
|
|
{
|
|
name: "with existing capacity below max",
|
|
args: args{initialCapacity: 5, maxCapacity: 10, sizes: []int{8, 4}},
|
|
cap: 8,
|
|
},
|
|
{
|
|
name: "with existing capacity and length above max",
|
|
args: args{initialCapacity: 12, initialLen: 5, maxCapacity: 10, sizes: []int{8, 4}},
|
|
cap: 12,
|
|
len: 5,
|
|
},
|
|
{
|
|
name: "with existing capacity and length below max",
|
|
args: args{initialCapacity: 5, initialLen: 3, maxCapacity: 10, sizes: []int{8, 4}},
|
|
cap: 8,
|
|
len: 3,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.args.initialCapacity > 0 {
|
|
val := make([]example.Pod, tt.args.initialLen, tt.args.initialCapacity)
|
|
for i := 0; i < tt.args.initialLen; i++ {
|
|
val[i].Name = fmt.Sprintf("test-%d", i)
|
|
}
|
|
tt.args.v = reflect.ValueOf(val)
|
|
}
|
|
// reflection requires that the value be addressable in order to call set,
|
|
// so we must ensure the value we created is available on the heap (not a problem
|
|
// for normal usage)
|
|
if !tt.args.v.CanAddr() {
|
|
x := reflect.New(tt.args.v.Type())
|
|
x.Elem().Set(tt.args.v)
|
|
tt.args.v = x.Elem()
|
|
}
|
|
growSlice(tt.args.v, tt.args.maxCapacity, tt.args.sizes...)
|
|
if tt.cap != tt.args.v.Cap() {
|
|
t.Errorf("Unexpected capacity: got=%d want=%d", tt.args.v.Cap(), tt.cap)
|
|
}
|
|
if tt.len != tt.args.v.Len() {
|
|
t.Errorf("Unexpected length: got=%d want=%d", tt.args.v.Len(), tt.len)
|
|
}
|
|
for i := 0; i < tt.args.v.Len(); i++ {
|
|
nameWanted := fmt.Sprintf("test-%d", i)
|
|
val := tt.args.v.Index(i).Interface()
|
|
pod, ok := val.(example.Pod)
|
|
if !ok || pod.Name != nameWanted {
|
|
t.Errorf("Unexpected element value: got=%s, want=%s", pod.Name, nameWanted)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeaseMaxObjectCount(t *testing.T) {
|
|
ctx, store, _ := testSetup(t, withLeaseConfig(LeaseManagerConfig{
|
|
ReuseDurationSeconds: defaultLeaseReuseDurationSeconds,
|
|
MaxObjectCount: 2,
|
|
}))
|
|
|
|
obj := &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}
|
|
out := &example.Pod{}
|
|
|
|
testCases := []struct {
|
|
key string
|
|
expectAttachedCount int64
|
|
}{
|
|
{
|
|
key: "testkey1",
|
|
expectAttachedCount: 1,
|
|
},
|
|
{
|
|
key: "testkey2",
|
|
expectAttachedCount: 2,
|
|
},
|
|
{
|
|
key: "testkey3",
|
|
// We assume each time has 1 object attached to the lease
|
|
// so after granting a new lease, the recorded count is set to 1
|
|
expectAttachedCount: 1,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
err := store.Create(ctx, tc.key, obj, out, 120)
|
|
if err != nil {
|
|
t.Fatalf("Set failed: %v", err)
|
|
}
|
|
if store.leaseManager.leaseAttachedObjectCount != tc.expectAttachedCount {
|
|
t.Errorf("Lease manager recorded count %v should be %v", store.leaseManager.leaseAttachedObjectCount, tc.expectAttachedCount)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===================================================
|
|
// Test-setup related function are following.
|
|
// ===================================================
|
|
|
|
func newTestLeaseManagerConfig() LeaseManagerConfig {
|
|
cfg := NewDefaultLeaseManagerConfig()
|
|
// As 30s is the default timeout for testing in global configuration,
|
|
// we cannot wait longer than that in a single time: change it to 1s
|
|
// for testing purposes. See wait.ForeverTestTimeout
|
|
cfg.ReuseDurationSeconds = 1
|
|
return cfg
|
|
}
|
|
|
|
func newTestTransformer() value.Transformer {
|
|
return storagetesting.NewPrefixTransformer([]byte(defaultTestPrefix), false)
|
|
}
|
|
|
|
type clientRecorder struct {
|
|
reads uint64
|
|
clientv3.KV
|
|
}
|
|
|
|
func (r *clientRecorder) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) {
|
|
atomic.AddUint64(&r.reads, 1)
|
|
return r.KV.Get(ctx, key, opts...)
|
|
}
|
|
|
|
func (r *clientRecorder) GetReadsAndReset() uint64 {
|
|
return atomic.SwapUint64(&r.reads, 0)
|
|
}
|
|
|
|
type setupOptions struct {
|
|
client func(testing.TB) *clientv3.Client
|
|
codec runtime.Codec
|
|
newFunc func() runtime.Object
|
|
newListFunc func() runtime.Object
|
|
prefix string
|
|
resourcePrefix string
|
|
groupResource schema.GroupResource
|
|
transformer value.Transformer
|
|
leaseConfig LeaseManagerConfig
|
|
|
|
recorderEnabled bool
|
|
}
|
|
|
|
type setupOption func(*setupOptions)
|
|
|
|
func withClientConfig(config *embed.Config) setupOption {
|
|
return func(options *setupOptions) {
|
|
options.client = func(t testing.TB) *clientv3.Client {
|
|
return testserver.RunEtcd(t, config)
|
|
}
|
|
}
|
|
}
|
|
|
|
func withPrefix(prefix string) setupOption {
|
|
return func(options *setupOptions) {
|
|
options.prefix = prefix
|
|
}
|
|
}
|
|
|
|
func withLeaseConfig(leaseConfig LeaseManagerConfig) setupOption {
|
|
return func(options *setupOptions) {
|
|
options.leaseConfig = leaseConfig
|
|
}
|
|
}
|
|
|
|
func withRecorder() setupOption {
|
|
return func(options *setupOptions) {
|
|
options.recorderEnabled = true
|
|
}
|
|
}
|
|
|
|
func withDefaults(options *setupOptions) {
|
|
options.client = func(t testing.TB) *clientv3.Client {
|
|
return testserver.RunEtcd(t, nil)
|
|
}
|
|
options.codec = apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion)
|
|
options.newFunc = newPod
|
|
options.newListFunc = newPodList
|
|
options.prefix = ""
|
|
options.resourcePrefix = "/pods"
|
|
options.groupResource = schema.GroupResource{Resource: "pods"}
|
|
options.transformer = newTestTransformer()
|
|
options.leaseConfig = newTestLeaseManagerConfig()
|
|
}
|
|
|
|
var _ setupOption = withDefaults
|
|
|
|
func testSetup(t testing.TB, opts ...setupOption) (context.Context, *store, *clientv3.Client) {
|
|
setupOpts := setupOptions{}
|
|
opts = append([]setupOption{withDefaults}, opts...)
|
|
for _, opt := range opts {
|
|
opt(&setupOpts)
|
|
}
|
|
client := setupOpts.client(t)
|
|
if setupOpts.recorderEnabled {
|
|
client.KV = &clientRecorder{KV: client.KV}
|
|
}
|
|
store := newStore(
|
|
client,
|
|
setupOpts.codec,
|
|
setupOpts.newFunc,
|
|
setupOpts.newListFunc,
|
|
setupOpts.prefix,
|
|
setupOpts.resourcePrefix,
|
|
setupOpts.groupResource,
|
|
setupOpts.transformer,
|
|
setupOpts.leaseConfig,
|
|
)
|
|
ctx := context.Background()
|
|
return ctx, store, client
|
|
}
|
|
|
|
func TestValidateKey(t *testing.T) {
|
|
validKeys := []string{
|
|
"/foo/bar/baz/a.b.c/",
|
|
"/foo",
|
|
"foo/bar/baz",
|
|
"/foo/bar..baz/",
|
|
"/foo/bar..",
|
|
"foo",
|
|
"foo/bar",
|
|
"/foo/bar/",
|
|
}
|
|
invalidKeys := []string{
|
|
"/foo/bar/../a.b.c/",
|
|
"..",
|
|
"/..",
|
|
"../",
|
|
"/foo/bar/..",
|
|
"../foo/bar",
|
|
"/../foo",
|
|
"/foo/bar/../",
|
|
".",
|
|
"/.",
|
|
"./",
|
|
"/./",
|
|
"/foo/.",
|
|
"./bar",
|
|
"/foo/./bar/",
|
|
}
|
|
const (
|
|
pathPrefix = "/first/second"
|
|
expectPrefix = pathPrefix + "/"
|
|
)
|
|
_, store, _ := testSetup(t, withPrefix(pathPrefix))
|
|
|
|
for _, key := range validKeys {
|
|
k, err := store.prepareKey(key)
|
|
if err != nil {
|
|
t.Errorf("key %q should be valid; unexpected error: %v", key, err)
|
|
} else if !strings.HasPrefix(k, expectPrefix) {
|
|
t.Errorf("key %q should have prefix %q", k, expectPrefix)
|
|
}
|
|
}
|
|
|
|
for _, key := range invalidKeys {
|
|
_, err := store.prepareKey(key)
|
|
if err == nil {
|
|
t.Errorf("key %q should be invalid", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInvalidKeys(t *testing.T) {
|
|
const invalidKey = "/foo/bar/../baz"
|
|
expectedError := fmt.Sprintf("invalid key: %q", invalidKey)
|
|
|
|
expectInvalidKey := func(methodName string, err error) {
|
|
if err == nil {
|
|
t.Errorf("[%s] expected invalid key error; got nil", methodName)
|
|
} else if err.Error() != expectedError {
|
|
t.Errorf("[%s] expected invalid key error; got %v", methodName, err)
|
|
}
|
|
}
|
|
|
|
ctx, store, _ := testSetup(t)
|
|
expectInvalidKey("Create", store.Create(ctx, invalidKey, nil, nil, 0))
|
|
expectInvalidKey("Delete", store.Delete(ctx, invalidKey, nil, nil, nil, nil))
|
|
_, watchErr := store.Watch(ctx, invalidKey, storage.ListOptions{})
|
|
expectInvalidKey("Watch", watchErr)
|
|
expectInvalidKey("Get", store.Get(ctx, invalidKey, storage.GetOptions{}, nil))
|
|
expectInvalidKey("GetList", store.GetList(ctx, invalidKey, storage.ListOptions{}, nil))
|
|
expectInvalidKey("GuaranteedUpdate", store.GuaranteedUpdate(ctx, invalidKey, nil, true, nil, nil, nil))
|
|
_, countErr := store.Count(invalidKey)
|
|
expectInvalidKey("Count", countErr)
|
|
}
|
|
|
|
func BenchmarkStore_GetList(b *testing.B) {
|
|
generateBigPod := func(index int, total int, expect int) runtime.Object {
|
|
l := map[string]string{}
|
|
if index%(total/expect) == 0 {
|
|
l["foo"] = "bar"
|
|
}
|
|
terminationGracePeriodSeconds := int64(42)
|
|
activeDeadlineSeconds := int64(42)
|
|
pod := &examplev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Labels: l,
|
|
},
|
|
Spec: examplev1.PodSpec{
|
|
RestartPolicy: examplev1.RestartPolicy("Always"),
|
|
TerminationGracePeriodSeconds: &terminationGracePeriodSeconds,
|
|
ActiveDeadlineSeconds: &activeDeadlineSeconds,
|
|
NodeSelector: map[string]string{},
|
|
ServiceAccountName: "demo-sa",
|
|
},
|
|
}
|
|
pod.Name = fmt.Sprintf("object-%d", index)
|
|
data := make([]byte, 1024*2, 1024*2) // 2k labels
|
|
rand.Read(data)
|
|
pod.Spec.NodeSelector["key"] = string(data)
|
|
return pod
|
|
}
|
|
testCases := []struct {
|
|
name string
|
|
objectNum int
|
|
expectNum int
|
|
selector labels.Selector
|
|
newObjectFunc func(index int, total int, expect int) runtime.Object
|
|
newListObjectFunc func() runtime.Object
|
|
}{
|
|
{
|
|
name: "pick 50 pods out of 5000 pod",
|
|
objectNum: 5000,
|
|
expectNum: 50,
|
|
selector: labels.SelectorFromSet(map[string]string{"foo": "bar"}),
|
|
newObjectFunc: generateBigPod,
|
|
newListObjectFunc: func() runtime.Object { return &examplev1.PodList{} },
|
|
},
|
|
{
|
|
name: "pick 500 pods out of 5000 pod",
|
|
objectNum: 5000,
|
|
expectNum: 500,
|
|
selector: labels.SelectorFromSet(map[string]string{"foo": "bar"}),
|
|
newObjectFunc: generateBigPod,
|
|
newListObjectFunc: func() runtime.Object { return &examplev1.PodList{} },
|
|
},
|
|
{
|
|
name: "pick 1000 pods out of 5000 pod",
|
|
objectNum: 5000,
|
|
expectNum: 1000,
|
|
selector: labels.SelectorFromSet(map[string]string{"foo": "bar"}),
|
|
newObjectFunc: generateBigPod,
|
|
newListObjectFunc: func() runtime.Object { return &examplev1.PodList{} },
|
|
},
|
|
{
|
|
name: "pick 2500 pods out of 5000 pod",
|
|
objectNum: 5000,
|
|
expectNum: 2500,
|
|
selector: labels.SelectorFromSet(map[string]string{"foo": "bar"}),
|
|
newObjectFunc: generateBigPod,
|
|
newListObjectFunc: func() runtime.Object { return &examplev1.PodList{} },
|
|
},
|
|
{
|
|
name: "pick 5000 pods out of 5000 pod",
|
|
objectNum: 5000,
|
|
expectNum: 5000,
|
|
selector: labels.SelectorFromSet(map[string]string{"foo": "bar"}),
|
|
newObjectFunc: generateBigPod,
|
|
newListObjectFunc: func() runtime.Object { return &examplev1.PodList{} },
|
|
},
|
|
}
|
|
for _, tc := range testCases {
|
|
b.Run(tc.name, func(b *testing.B) {
|
|
// booting etcd instance
|
|
ctx, store, etcdClient := testSetup(b)
|
|
defer etcdClient.Close()
|
|
|
|
// make fake objects..
|
|
dir := "/testing"
|
|
originalRevision := ""
|
|
for i := 0; i < tc.objectNum; i++ {
|
|
obj := tc.newObjectFunc(i, tc.objectNum, tc.expectNum)
|
|
o := obj.(metav1.Object)
|
|
key := fmt.Sprintf("/testing/testkey/%s", o.GetName())
|
|
out := tc.newObjectFunc(i, tc.objectNum, tc.expectNum)
|
|
if err := store.Create(ctx, key, obj, out, 0); err != nil {
|
|
b.Fatalf("Set failed: %v", err)
|
|
}
|
|
originalRevision = out.(metav1.Object).GetResourceVersion()
|
|
}
|
|
|
|
// prepare result and pred
|
|
pred := storage.SelectionPredicate{
|
|
Label: tc.selector,
|
|
Field: fields.Everything(),
|
|
GetAttrs: func(obj runtime.Object) (labels.Set, fields.Set, error) {
|
|
pod, ok := obj.(*examplev1.Pod)
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("not a pod")
|
|
}
|
|
return pod.ObjectMeta.Labels, fields.Set{
|
|
"metadata.name": pod.Name,
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
// now we start benchmarking
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
list := tc.newListObjectFunc()
|
|
if err := store.GetList(ctx, dir, storage.ListOptions{Predicate: pred, Recursive: true}, list); err != nil {
|
|
b.Errorf("Unexpected List error: %v", err)
|
|
}
|
|
listObject := list.(*examplev1.PodList)
|
|
if originalRevision != listObject.GetResourceVersion() {
|
|
b.Fatalf("original revision (%s) did not match final revision after linearized reads (%s)", originalRevision, listObject.GetResourceVersion())
|
|
}
|
|
if len(listObject.Items) != tc.expectNum {
|
|
b.Fatalf("expect (%d) items but got (%d)", tc.expectNum, len(listObject.Items))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|