k8s.io/apiserver@v0.29.3/pkg/storage/storagebackend/factory/factory_test.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package factory 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "strings" 24 "sync" 25 "sync/atomic" 26 "testing" 27 "time" 28 29 clientv3 "go.etcd.io/etcd/client/v3" 30 "k8s.io/apiserver/pkg/storage/etcd3/testserver" 31 "k8s.io/apiserver/pkg/storage/storagebackend" 32 ) 33 34 type mockKV struct { 35 get func(ctx context.Context) (*clientv3.GetResponse, error) 36 } 37 38 func (mkv mockKV) Put(ctx context.Context, key, val string, opts ...clientv3.OpOption) (*clientv3.PutResponse, error) { 39 return nil, nil 40 } 41 func (mkv mockKV) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) { 42 return mkv.get(ctx) 43 } 44 func (mockKV) Delete(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.DeleteResponse, error) { 45 return nil, nil 46 } 47 func (mockKV) Compact(ctx context.Context, rev int64, opts ...clientv3.CompactOption) (*clientv3.CompactResponse, error) { 48 return nil, nil 49 } 50 func (mockKV) Do(ctx context.Context, op clientv3.Op) (clientv3.OpResponse, error) { 51 return clientv3.OpResponse{}, nil 52 } 53 func (mockKV) Txn(ctx context.Context) clientv3.Txn { 54 return nil 55 } 56 57 func TestCreateHealthcheck(t *testing.T) { 58 etcdConfig := testserver.NewTestConfig(t) 59 client := testserver.RunEtcd(t, etcdConfig) 60 newETCD3ClientFn := newETCD3Client 61 defer func() { 62 newETCD3Client = newETCD3ClientFn 63 }() 64 tests := []struct { 65 name string 66 cfg storagebackend.Config 67 want error 68 responseTime time.Duration 69 }{ 70 { 71 name: "ok if response time lower than default timeout", 72 cfg: storagebackend.Config{ 73 Type: storagebackend.StorageTypeETCD3, 74 Transport: storagebackend.TransportConfig{}, 75 }, 76 responseTime: 1 * time.Second, 77 want: nil, 78 }, 79 { 80 name: "ok if response time lower than custom timeout", 81 cfg: storagebackend.Config{ 82 Type: storagebackend.StorageTypeETCD3, 83 Transport: storagebackend.TransportConfig{}, 84 HealthcheckTimeout: 5 * time.Second, 85 }, 86 responseTime: 3 * time.Second, 87 want: nil, 88 }, 89 { 90 name: "timeouts if response time higher than default timeout", 91 cfg: storagebackend.Config{ 92 Type: storagebackend.StorageTypeETCD3, 93 Transport: storagebackend.TransportConfig{}, 94 }, 95 responseTime: 3 * time.Second, 96 want: context.DeadlineExceeded, 97 }, 98 { 99 name: "timeouts if response time higher than custom timeout", 100 cfg: storagebackend.Config{ 101 Type: storagebackend.StorageTypeETCD3, 102 Transport: storagebackend.TransportConfig{}, 103 HealthcheckTimeout: 3 * time.Second, 104 }, 105 responseTime: 5 * time.Second, 106 want: context.DeadlineExceeded, 107 }, 108 } 109 110 for _, tc := range tests { 111 t.Run(tc.name, func(t *testing.T) { 112 ready := make(chan struct{}) 113 tc.cfg.Transport.ServerList = client.Endpoints() 114 newETCD3Client = func(c storagebackend.TransportConfig) (*clientv3.Client, error) { 115 defer close(ready) 116 dummyKV := mockKV{ 117 get: func(ctx context.Context) (*clientv3.GetResponse, error) { 118 select { 119 case <-ctx.Done(): 120 return nil, ctx.Err() 121 case <-time.After(tc.responseTime): 122 return nil, nil 123 } 124 }, 125 } 126 client.KV = dummyKV 127 return client, nil 128 } 129 stop := make(chan struct{}) 130 defer close(stop) 131 132 healthcheck, err := CreateHealthCheck(tc.cfg, stop) 133 if err != nil { 134 t.Fatal(err) 135 } 136 // Wait for healthcheck to establish connection 137 <-ready 138 got := healthcheck() 139 140 if !errors.Is(got, tc.want) { 141 t.Errorf("healthcheck() missmatch want %v got %v", tc.want, got) 142 } 143 }) 144 } 145 } 146 147 func TestCreateReadycheck(t *testing.T) { 148 etcdConfig := testserver.NewTestConfig(t) 149 client := testserver.RunEtcd(t, etcdConfig) 150 newETCD3ClientFn := newETCD3Client 151 defer func() { 152 newETCD3Client = newETCD3ClientFn 153 }() 154 tests := []struct { 155 name string 156 cfg storagebackend.Config 157 want error 158 responseTime time.Duration 159 }{ 160 { 161 name: "ok if response time lower than default timeout", 162 cfg: storagebackend.Config{ 163 Type: storagebackend.StorageTypeETCD3, 164 Transport: storagebackend.TransportConfig{}, 165 }, 166 responseTime: 1 * time.Second, 167 want: nil, 168 }, 169 { 170 name: "ok if response time lower than custom timeout", 171 cfg: storagebackend.Config{ 172 Type: storagebackend.StorageTypeETCD3, 173 Transport: storagebackend.TransportConfig{}, 174 ReadycheckTimeout: 5 * time.Second, 175 }, 176 responseTime: 3 * time.Second, 177 want: nil, 178 }, 179 { 180 name: "timeouts if response time higher than default timeout", 181 cfg: storagebackend.Config{ 182 Type: storagebackend.StorageTypeETCD3, 183 Transport: storagebackend.TransportConfig{}, 184 }, 185 responseTime: 3 * time.Second, 186 want: context.DeadlineExceeded, 187 }, 188 { 189 name: "timeouts if response time higher than custom timeout", 190 cfg: storagebackend.Config{ 191 Type: storagebackend.StorageTypeETCD3, 192 Transport: storagebackend.TransportConfig{}, 193 ReadycheckTimeout: 3 * time.Second, 194 }, 195 responseTime: 5 * time.Second, 196 want: context.DeadlineExceeded, 197 }, 198 { 199 name: "timeouts if response time higher than default timeout with custom healthcheck timeout", 200 cfg: storagebackend.Config{ 201 Type: storagebackend.StorageTypeETCD3, 202 Transport: storagebackend.TransportConfig{}, 203 HealthcheckTimeout: 10 * time.Second, 204 }, 205 responseTime: 3 * time.Second, 206 want: context.DeadlineExceeded, 207 }, 208 } 209 210 for _, tc := range tests { 211 t.Run(tc.name, func(t *testing.T) { 212 ready := make(chan struct{}) 213 tc.cfg.Transport.ServerList = client.Endpoints() 214 newETCD3Client = func(c storagebackend.TransportConfig) (*clientv3.Client, error) { 215 defer close(ready) 216 dummyKV := mockKV{ 217 get: func(ctx context.Context) (*clientv3.GetResponse, error) { 218 select { 219 case <-ctx.Done(): 220 return nil, ctx.Err() 221 case <-time.After(tc.responseTime): 222 return nil, nil 223 } 224 }, 225 } 226 client.KV = dummyKV 227 return client, nil 228 } 229 stop := make(chan struct{}) 230 defer close(stop) 231 232 healthcheck, err := CreateReadyCheck(tc.cfg, stop) 233 if err != nil { 234 t.Fatal(err) 235 } 236 // Wait for healthcheck to establish connection 237 <-ready 238 239 got := healthcheck() 240 241 if !errors.Is(got, tc.want) { 242 t.Errorf("healthcheck() missmatch want %v got %v", tc.want, got) 243 } 244 }) 245 } 246 } 247 248 func TestRateLimitHealthcheck(t *testing.T) { 249 etcdConfig := testserver.NewTestConfig(t) 250 client := testserver.RunEtcd(t, etcdConfig) 251 newETCD3ClientFn := newETCD3Client 252 defer func() { 253 newETCD3Client = newETCD3ClientFn 254 }() 255 256 cfg := storagebackend.Config{ 257 Type: storagebackend.StorageTypeETCD3, 258 Transport: storagebackend.TransportConfig{}, 259 HealthcheckTimeout: 5 * time.Second, 260 } 261 cfg.Transport.ServerList = client.Endpoints() 262 tests := []struct { 263 name string 264 want error 265 }{ 266 { 267 name: "etcd ok", 268 }, 269 { 270 name: "etcd down", 271 want: errors.New("etcd down"), 272 }, 273 } 274 for _, tc := range tests { 275 t.Run(tc.name, func(t *testing.T) { 276 277 ready := make(chan struct{}) 278 279 var counter uint64 280 newETCD3Client = func(c storagebackend.TransportConfig) (*clientv3.Client, error) { 281 defer close(ready) 282 dummyKV := mockKV{ 283 get: func(ctx context.Context) (*clientv3.GetResponse, error) { 284 atomic.AddUint64(&counter, 1) 285 select { 286 case <-ctx.Done(): 287 return nil, ctx.Err() 288 default: 289 return nil, tc.want 290 } 291 }, 292 } 293 client.KV = dummyKV 294 return client, nil 295 } 296 297 stop := make(chan struct{}) 298 defer close(stop) 299 healthcheck, err := CreateHealthCheck(cfg, stop) 300 if err != nil { 301 t.Fatal(err) 302 } 303 // Wait for healthcheck to establish connection 304 <-ready 305 // run a first request to obtain the state 306 err = healthcheck() 307 if !errors.Is(err, tc.want) { 308 t.Errorf("healthcheck() mismatch want %v got %v", tc.want, err) 309 } 310 311 // run multiple request in parallel, they should have the same state that the first one 312 var wg sync.WaitGroup 313 for i := 0; i < 100; i++ { 314 wg.Add(1) 315 go func() { 316 defer wg.Done() 317 err := healthcheck() 318 if !errors.Is(err, tc.want) { 319 t.Errorf("healthcheck() mismatch want %v got %v", tc.want, err) 320 } 321 322 }() 323 } 324 325 // check the counter once the requests have finished 326 wg.Wait() 327 if counter != 1 { 328 t.Errorf("healthcheck() called etcd %d times, expected only one call", counter) 329 } 330 331 // wait until the rate limit allows new connections 332 time.Sleep(cfg.HealthcheckTimeout / 2) 333 334 // a new run on request should increment the counter only once 335 // run multiple request in parallel, they should have the same state that the first one 336 for i := 0; i < 100; i++ { 337 wg.Add(1) 338 go func() { 339 defer wg.Done() 340 err := healthcheck() 341 if !errors.Is(err, tc.want) { 342 t.Errorf("healthcheck() mismatch want %v got %v", tc.want, err) 343 } 344 345 }() 346 } 347 wg.Wait() 348 349 if counter != 2 { 350 t.Errorf("healthcheck() called etcd %d times, expected only two calls", counter) 351 } 352 }) 353 } 354 355 } 356 357 func TestTimeTravelHealthcheck(t *testing.T) { 358 etcdConfig := testserver.NewTestConfig(t) 359 client := testserver.RunEtcd(t, etcdConfig) 360 newETCD3ClientFn := newETCD3Client 361 defer func() { 362 newETCD3Client = newETCD3ClientFn 363 }() 364 365 cfg := storagebackend.Config{ 366 Type: storagebackend.StorageTypeETCD3, 367 Transport: storagebackend.TransportConfig{}, 368 HealthcheckTimeout: 5 * time.Second, 369 } 370 cfg.Transport.ServerList = client.Endpoints() 371 372 ready := make(chan struct{}) 373 signal := make(chan struct{}) 374 375 var counter uint64 376 newETCD3Client = func(c storagebackend.TransportConfig) (*clientv3.Client, error) { 377 defer close(ready) 378 dummyKV := mockKV{ 379 get: func(ctx context.Context) (*clientv3.GetResponse, error) { 380 atomic.AddUint64(&counter, 1) 381 val := atomic.LoadUint64(&counter) 382 // the first request wait for a custom timeout to trigger an error. 383 // We don't use the context timeout because we want to check that 384 // the cached answer is not overridden, and since the rate limit is 385 // based on cfg.HealthcheckTimeout / 2, the timeout will race with 386 // the race limiter to server the new request from the cache or allow 387 // it to go through 388 if val == 1 { 389 select { 390 case <-ctx.Done(): 391 return nil, ctx.Err() 392 case <-time.After((2 * cfg.HealthcheckTimeout) / 3): 393 return nil, fmt.Errorf("etcd down") 394 } 395 } 396 // subsequent requests will always work 397 return nil, nil 398 }, 399 } 400 client.KV = dummyKV 401 return client, nil 402 } 403 404 stop := make(chan struct{}) 405 defer close(stop) 406 healthcheck, err := CreateHealthCheck(cfg, stop) 407 if err != nil { 408 t.Fatal(err) 409 } 410 // Wait for healthcheck to establish connection 411 <-ready 412 // run a first request that fails after 2 seconds 413 go func() { 414 err := healthcheck() 415 if !strings.Contains(err.Error(), "etcd down") { 416 t.Errorf("healthcheck() mismatch want %v got %v", fmt.Errorf("etcd down"), err) 417 } 418 close(signal) 419 }() 420 421 // wait until the rate limit allows new connections 422 time.Sleep(cfg.HealthcheckTimeout / 2) 423 424 select { 425 case <-signal: 426 t.Errorf("first request should not return yet") 427 default: 428 } 429 430 // a new run on request should succeed and increment the counter 431 err = healthcheck() 432 if err != nil { 433 t.Errorf("unexpected error: %v", err) 434 } 435 c := atomic.LoadUint64(&counter) 436 if c != 2 { 437 t.Errorf("healthcheck() called etcd %d times, expected only two calls", c) 438 } 439 440 // cached request should be success and not be overridden by the late error 441 <-signal 442 err = healthcheck() 443 if err != nil { 444 t.Errorf("unexpected error: %v", err) 445 } 446 c = atomic.LoadUint64(&counter) 447 if c != 2 { 448 t.Errorf("healthcheck() called etcd %d times, expected only two calls", c) 449 } 450 451 }