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 }