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  }