github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/cluster/client/etcd/client_test.go (about)

     1  // Copyright (c) 2016 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package etcd
    22  
    23  import (
    24  	"os"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/m3db/m3/src/cluster/kv"
    29  	"github.com/m3db/m3/src/cluster/services"
    30  	integration "github.com/m3db/m3/src/integration/resources/docker/dockerexternal/etcdintegration"
    31  	"github.com/m3db/m3/src/x/retry"
    32  
    33  	"github.com/stretchr/testify/assert"
    34  	"github.com/stretchr/testify/require"
    35  	clientv3 "go.etcd.io/etcd/client/v3"
    36  	"google.golang.org/grpc"
    37  )
    38  
    39  func TestETCDClientGen(t *testing.T) {
    40  	cs, err := NewConfigServiceClient(
    41  		testOptions().
    42  			// These are error cases; don't retry for no reason.
    43  			SetRetryOptions(retry.NewOptions().SetMaxRetries(0)),
    44  	)
    45  	require.NoError(t, err)
    46  
    47  	c := cs.(*csclient)
    48  	// a zone that does not exist
    49  	_, err = c.etcdClientGen("not_exist")
    50  	require.Error(t, err)
    51  	require.Equal(t, 0, len(c.clis))
    52  
    53  	c1, err := c.etcdClientGen("zone1")
    54  	require.NoError(t, err)
    55  	require.Equal(t, 1, len(c.clis))
    56  
    57  	c2, err := c.etcdClientGen("zone2")
    58  	require.NoError(t, err)
    59  	require.Equal(t, 2, len(c.clis))
    60  	require.False(t, c1 == c2)
    61  
    62  	_, err = c.etcdClientGen("zone3")
    63  	require.Error(t, err)
    64  	require.Equal(t, 2, len(c.clis))
    65  
    66  	// TODO(pwoodman): bit of a cop-out- this'll error no matter what as it's looking for
    67  	// a file that won't be in the test environment. So, expect error.
    68  	_, err = c.etcdClientGen("zone4")
    69  	require.Error(t, err)
    70  
    71  	_, err = c.etcdClientGen("zone5")
    72  	require.Error(t, err)
    73  
    74  	c1Again, err := c.etcdClientGen("zone1")
    75  	require.NoError(t, err)
    76  	require.Equal(t, 2, len(c.clis))
    77  	require.True(t, c1 == c1Again)
    78  
    79  	t.Run("TestNewDirectoryMode", func(t *testing.T) {
    80  		require.Equal(t, defaultDirectoryMode, c.opts.NewDirectoryMode())
    81  
    82  		expect := os.FileMode(0744)
    83  		opts := testOptions().SetNewDirectoryMode(expect)
    84  		require.Equal(t, expect, opts.NewDirectoryMode())
    85  		cs, err := NewConfigServiceClient(opts)
    86  		require.NoError(t, err)
    87  		require.Equal(t, expect, cs.(*csclient).opts.NewDirectoryMode())
    88  	})
    89  }
    90  
    91  func TestKVAndHeartbeatServiceSharingETCDClient(t *testing.T) {
    92  	sid := services.NewServiceID().SetName("s1")
    93  
    94  	cs, err := NewConfigServiceClient(testOptions().SetZone("zone1").SetEnv("env"))
    95  	require.NoError(t, err)
    96  
    97  	c := cs.(*csclient)
    98  
    99  	_, err = c.KV()
   100  	require.NoError(t, err)
   101  	require.Equal(t, 1, len(c.clis))
   102  
   103  	_, err = c.heartbeatGen()(sid.SetZone("zone1"))
   104  	require.NoError(t, err)
   105  	require.Equal(t, 1, len(c.clis))
   106  
   107  	_, err = c.heartbeatGen()(sid.SetZone("zone2"))
   108  	require.NoError(t, err)
   109  	require.Equal(t, 2, len(c.clis))
   110  
   111  	_, err = c.heartbeatGen()(sid.SetZone("not_exist"))
   112  	require.Error(t, err)
   113  	require.Equal(t, 2, len(c.clis))
   114  }
   115  
   116  func TestClient(t *testing.T) {
   117  	_, err := NewConfigServiceClient(NewOptions())
   118  	require.Error(t, err)
   119  
   120  	cs, err := NewConfigServiceClient(testOptions())
   121  	require.NoError(t, err)
   122  	_, err = cs.KV()
   123  	require.NoError(t, err)
   124  
   125  	cs, err = NewConfigServiceClient(testOptions())
   126  	require.NoError(t, err)
   127  	c := cs.(*csclient)
   128  
   129  	fn, closer := testNewETCDFn(t)
   130  	defer closer()
   131  	c.newFn = fn
   132  
   133  	txn, err := c.Txn()
   134  	require.NoError(t, err)
   135  
   136  	kv1, err := c.KV()
   137  	require.NoError(t, err)
   138  	require.Equal(t, kv1, txn)
   139  
   140  	kv2, err := c.KV()
   141  	require.NoError(t, err)
   142  	require.Equal(t, kv1, kv2)
   143  
   144  	kv3, err := c.Store(kv.NewOverrideOptions().SetNamespace("ns").SetEnvironment("test_env1"))
   145  	require.NoError(t, err)
   146  	require.NotEqual(t, kv1, kv3)
   147  
   148  	kv4, err := c.Store(kv.NewOverrideOptions().SetNamespace("ns"))
   149  	require.NoError(t, err)
   150  	require.NotEqual(t, kv3, kv4)
   151  
   152  	// KV store will create an etcd cli for local zone only
   153  	require.Equal(t, 1, len(c.clis))
   154  	_, ok := c.clis["zone1"]
   155  	require.True(t, ok)
   156  
   157  	kv5, err := c.Store(kv.NewOverrideOptions().SetZone("zone2").SetNamespace("ns"))
   158  	require.NoError(t, err)
   159  	require.NotEqual(t, kv4, kv5)
   160  
   161  	require.Equal(t, 2, len(c.clis))
   162  	_, ok = c.clis["zone2"]
   163  	require.True(t, ok)
   164  
   165  	sd1, err := c.Services(nil)
   166  	require.NoError(t, err)
   167  
   168  	err = sd1.SetMetadata(
   169  		services.NewServiceID().SetName("service").SetZone("zone2"),
   170  		services.NewMetadata(),
   171  	)
   172  	require.NoError(t, err)
   173  	// etcd cli for zone1 will be reused
   174  	require.Equal(t, 2, len(c.clis))
   175  	_, ok = c.clis["zone2"]
   176  	require.True(t, ok)
   177  
   178  	err = sd1.SetMetadata(
   179  		services.NewServiceID().SetName("service").SetZone("zone3"),
   180  		services.NewMetadata(),
   181  	)
   182  	require.NoError(t, err)
   183  	// etcd cli for zone2 will be created since the request is going to zone2
   184  	require.Equal(t, 3, len(c.clis))
   185  	_, ok = c.clis["zone3"]
   186  	require.True(t, ok)
   187  }
   188  
   189  func TestServicesWithNamespace(t *testing.T) {
   190  	cs, err := NewConfigServiceClient(testOptions())
   191  	require.NoError(t, err)
   192  	c := cs.(*csclient)
   193  
   194  	fn, closer := testNewETCDFn(t)
   195  	defer closer()
   196  	c.newFn = fn
   197  
   198  	sd1, err := c.Services(services.NewOverrideOptions())
   199  	require.NoError(t, err)
   200  
   201  	nOpts := services.NewNamespaceOptions().SetPlacementNamespace("p").SetMetadataNamespace("m")
   202  	sd2, err := c.Services(services.NewOverrideOptions().SetNamespaceOptions(nOpts))
   203  	require.NoError(t, err)
   204  
   205  	require.NotEqual(t, sd1, sd2)
   206  
   207  	sid := services.NewServiceID().SetName("service").SetZone("zone2")
   208  	err = sd1.SetMetadata(sid, services.NewMetadata())
   209  	require.NoError(t, err)
   210  
   211  	_, err = sd1.Metadata(sid)
   212  	require.NoError(t, err)
   213  
   214  	_, err = sd2.Metadata(sid)
   215  	require.Error(t, err)
   216  
   217  	sid2 := services.NewServiceID().SetName("service").SetZone("zone2").SetEnvironment("test")
   218  	err = sd2.SetMetadata(sid2, services.NewMetadata())
   219  	require.NoError(t, err)
   220  
   221  	_, err = sd1.Metadata(sid2)
   222  	require.Error(t, err)
   223  }
   224  
   225  func newOverrideOpts(zone, namespace, environment string) kv.OverrideOptions {
   226  	return kv.NewOverrideOptions().
   227  		SetZone(zone).
   228  		SetNamespace(namespace).
   229  		SetEnvironment(environment)
   230  }
   231  
   232  func TestCacheFileForZone(t *testing.T) {
   233  	c, err := NewConfigServiceClient(testOptions())
   234  	require.NoError(t, err)
   235  	cs := c.(*csclient)
   236  
   237  	kvOpts := cs.newkvOptions(newOverrideOpts("z1", "namespace", ""), cs.cacheFileFn())
   238  	require.Equal(t, "", kvOpts.CacheFileFn()(kvOpts.Prefix()))
   239  
   240  	cs.opts = cs.opts.SetCacheDir("/cacheDir")
   241  	kvOpts = cs.newkvOptions(newOverrideOpts("z1", "", ""), cs.cacheFileFn())
   242  	require.Equal(t, "/cacheDir/test_app_z1.json", kvOpts.CacheFileFn()(kvOpts.Prefix()))
   243  
   244  	kvOpts = cs.newkvOptions(newOverrideOpts("z1", "namespace", ""), cs.cacheFileFn())
   245  	require.Equal(t, "/cacheDir/namespace_test_app_z1.json", kvOpts.CacheFileFn()(kvOpts.Prefix()))
   246  
   247  	kvOpts = cs.newkvOptions(newOverrideOpts("z1", "namespace", ""), cs.cacheFileFn())
   248  	require.Equal(t, "/cacheDir/namespace_test_app_z1.json", kvOpts.CacheFileFn()(kvOpts.Prefix()))
   249  
   250  	kvOpts = cs.newkvOptions(newOverrideOpts("z1", "namespace", "env"), cs.cacheFileFn())
   251  	require.Equal(t, "/cacheDir/namespace_env_test_app_z1.json", kvOpts.CacheFileFn()(kvOpts.Prefix()))
   252  
   253  	kvOpts = cs.newkvOptions(newOverrideOpts("z1", "namespace", ""), cs.cacheFileFn("f1", "", "f2"))
   254  	require.Equal(t, "/cacheDir/namespace_test_app_z1_f1_f2.json", kvOpts.CacheFileFn()(kvOpts.Prefix()))
   255  
   256  	kvOpts = cs.newkvOptions(newOverrideOpts("z2", "", ""), cs.cacheFileFn("/r2/m3agg"))
   257  	require.Equal(t, "/cacheDir/test_app_z2__r2_m3agg.json", kvOpts.CacheFileFn()(kvOpts.Prefix()))
   258  }
   259  
   260  func TestSanitizeKVOverrideOptions(t *testing.T) {
   261  	opts := testOptions()
   262  	cs, err := NewConfigServiceClient(opts)
   263  	require.NoError(t, err)
   264  
   265  	client := cs.(*csclient)
   266  	opts1, err := client.sanitizeOptions(kv.NewOverrideOptions())
   267  	require.NoError(t, err)
   268  	require.Equal(t, opts.Env(), opts1.Environment())
   269  	require.Equal(t, opts.Zone(), opts1.Zone())
   270  	require.Equal(t, kvPrefix, opts1.Namespace())
   271  }
   272  
   273  func TestReuseKVStore(t *testing.T) {
   274  	opts := testOptions()
   275  	cs, err := NewConfigServiceClient(opts)
   276  	require.NoError(t, err)
   277  
   278  	store1, err := cs.Txn()
   279  	require.NoError(t, err)
   280  
   281  	store2, err := cs.KV()
   282  	require.NoError(t, err)
   283  	require.Equal(t, store1, store2)
   284  
   285  	store3, err := cs.Store(kv.NewOverrideOptions())
   286  	require.NoError(t, err)
   287  	require.Equal(t, store1, store3)
   288  
   289  	store4, err := cs.TxnStore(kv.NewOverrideOptions())
   290  	require.NoError(t, err)
   291  	require.Equal(t, store1, store4)
   292  
   293  	store5, err := cs.Store(kv.NewOverrideOptions().SetNamespace("foo"))
   294  	require.NoError(t, err)
   295  	require.NotEqual(t, store1, store5)
   296  
   297  	store6, err := cs.TxnStore(kv.NewOverrideOptions().SetNamespace("foo"))
   298  	require.NoError(t, err)
   299  	require.Equal(t, store5, store6)
   300  
   301  	client := cs.(*csclient)
   302  
   303  	client.storeLock.Lock()
   304  	require.Equal(t, 2, len(client.stores))
   305  	client.storeLock.Unlock()
   306  }
   307  
   308  func TestGetEtcdClients(t *testing.T) {
   309  	opts := testOptions()
   310  	c, err := NewEtcdConfigServiceClient(opts)
   311  	require.NoError(t, err)
   312  
   313  	c1, err := c.etcdClientGen("zone2")
   314  	require.NoError(t, err)
   315  	require.Equal(t, 1, len(c.clis))
   316  
   317  	c2, err := c.etcdClientGen("zone1")
   318  	require.NoError(t, err)
   319  	require.Equal(t, 2, len(c.clis))
   320  	require.False(t, c1 == c2)
   321  
   322  	clients := c.Clients()
   323  	require.Len(t, clients, 2)
   324  
   325  	assert.Equal(t, clients[0].Zone, "zone1")
   326  	assert.Equal(t, clients[0].Client, c2)
   327  	assert.Equal(t, clients[1].Zone, "zone2")
   328  	assert.Equal(t, clients[1].Client, c1)
   329  }
   330  
   331  func TestValidateNamespace(t *testing.T) {
   332  	inputs := []struct {
   333  		ns        string
   334  		expectErr bool
   335  	}{
   336  		{
   337  			ns:        "ns",
   338  			expectErr: false,
   339  		},
   340  		{
   341  			ns:        "/ns",
   342  			expectErr: false,
   343  		},
   344  		{
   345  			ns:        "/ns/ab",
   346  			expectErr: false,
   347  		},
   348  		{
   349  			ns:        "ns/ab",
   350  			expectErr: false,
   351  		},
   352  		{
   353  			ns:        "_ns",
   354  			expectErr: true,
   355  		},
   356  		{
   357  			ns:        "/_ns",
   358  			expectErr: true,
   359  		},
   360  		{
   361  			ns:        "",
   362  			expectErr: true,
   363  		},
   364  		{
   365  			ns:        "/",
   366  			expectErr: true,
   367  		},
   368  	}
   369  
   370  	for _, input := range inputs {
   371  		err := validateTopLevelNamespace(input.ns)
   372  		if input.expectErr {
   373  			require.Error(t, err)
   374  		}
   375  	}
   376  }
   377  
   378  func Test_newConfigFromCluster(t *testing.T) {
   379  	testRnd := func(n int64) (int64, error) {
   380  		return 10, nil
   381  	}
   382  
   383  	newFullConfig := func() ClusterConfig {
   384  		// Go all the way from config; might as well.
   385  		return ClusterConfig{
   386  			Zone:      "foo",
   387  			Endpoints: []string{"i1"},
   388  			KeepAlive: &KeepAliveConfig{
   389  				Enabled: true,
   390  				Period:  5 * time.Second,
   391  				Jitter:  6 * time.Second,
   392  				Timeout: 7 * time.Second,
   393  			},
   394  			TLS:              nil, // TODO: TLS config gets read eagerly here; test it separately.
   395  			AutoSyncInterval: 21 * time.Second,
   396  			DialTimeout:      42 * time.Second,
   397  		}
   398  	}
   399  
   400  	t.Run("translates config options", func(t *testing.T) {
   401  		cfg, err := newConfigFromCluster(testRnd, newFullConfig().NewCluster())
   402  		require.NoError(t, err)
   403  
   404  		assert.Equal(t,
   405  			clientv3.Config{
   406  				Endpoints:            []string{"i1"},
   407  				AutoSyncInterval:     21 * time.Second,
   408  				DialTimeout:          42 * time.Second,
   409  				DialKeepAliveTime:    5*time.Second + 10, // generated using fake rnd above
   410  				DialKeepAliveTimeout: 7 * time.Second,
   411  				MaxCallSendMsgSize:   33554432,
   412  				MaxCallRecvMsgSize:   33554432,
   413  				RejectOldCluster:     false,
   414  				DialOptions:          []grpc.DialOption(nil),
   415  
   416  				PermitWithoutStream: true,
   417  			},
   418  			cfg,
   419  		)
   420  	})
   421  
   422  	t.Run("negative autosync on M3 disables autosync for etcd", func(t *testing.T) {
   423  		inputCfg := newFullConfig()
   424  		inputCfg.AutoSyncInterval = -1
   425  		etcdCfg, err := newConfigFromCluster(testRnd, inputCfg.NewCluster())
   426  		require.NoError(t, err)
   427  
   428  		assert.Equal(t, time.Duration(0), etcdCfg.AutoSyncInterval)
   429  	})
   430  
   431  	// Separate test just because the assert.Equal won't work for functions.
   432  	t.Run("passes through dial options", func(t *testing.T) {
   433  		clusterCfg := newFullConfig()
   434  		clusterCfg.DialOptions = []grpc.DialOption{grpc.WithNoProxy()}
   435  		etcdCfg, err := newConfigFromCluster(testRnd, clusterCfg.NewCluster())
   436  		require.NoError(t, err)
   437  
   438  		assert.Len(t, etcdCfg.DialOptions, 1)
   439  	})
   440  }
   441  
   442  func Test_cryptoRandInt63n(t *testing.T) {
   443  	r, err := cryptoRandInt63n(185)
   444  	require.NoError(t, err)
   445  	// Real dumb sanity check. Doesn't flake on -test.count=10000, so probably ok.
   446  	assert.True(t, r >= 0 && r < 185)
   447  }
   448  
   449  func testOptions() Options {
   450  	clusters := []Cluster{
   451  		NewCluster().SetZone("zone1").SetEndpoints([]string{"i1"}),
   452  		NewCluster().SetZone("zone2").SetEndpoints([]string{"i2"}),
   453  		NewCluster().SetZone("zone3").SetEndpoints([]string{"i3"}).
   454  			SetTLSOptions(NewTLSOptions().SetCrtPath("foo.crt.pem")),
   455  		NewCluster().SetZone("zone4").SetEndpoints([]string{"i4"}).
   456  			SetTLSOptions(NewTLSOptions().SetCrtPath("foo.crt.pem").SetKeyPath("foo.key.pem")),
   457  		NewCluster().SetZone("zone5").SetEndpoints([]string{"i5"}).
   458  			SetTLSOptions(NewTLSOptions().SetCrtPath("foo.crt.pem").SetKeyPath("foo.key.pem").SetCACrtPath("foo_ca.pem")),
   459  	}
   460  	return NewOptions().
   461  		SetClusters(clusters).
   462  		SetService("test_app").
   463  		SetZone("zone1").
   464  		SetEnv("env")
   465  }
   466  
   467  func testNewETCDFn(t *testing.T) (newClientFn, func()) {
   468  	integration.BeforeTestExternal(t)
   469  	ecluster := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
   470  	ec := ecluster.RandClient()
   471  
   472  	newFn := func(Cluster) (*clientv3.Client, error) {
   473  		return ec, nil
   474  	}
   475  
   476  	closer := func() {
   477  		ecluster.Terminate(t)
   478  	}
   479  
   480  	return newFn, closer
   481  }