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 }