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