github.com/lfch/etcd-io/tests/v3@v3.0.0-20221004140520-eac99acd3e9d/framework/e2e/cluster.go (about) 1 // Copyright 2016 The etcd Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package e2e 16 17 import ( 18 "context" 19 "fmt" 20 "net/url" 21 "path" 22 "regexp" 23 "strings" 24 "testing" 25 "time" 26 27 "go.uber.org/zap" 28 "go.uber.org/zap/zaptest" 29 30 "github.com/lfch/etcd-io/server/v3/etcdserver" 31 ) 32 33 const EtcdProcessBasePort = 20000 34 35 type ClientConnType int 36 37 const ( 38 ClientNonTLS ClientConnType = iota 39 ClientTLS 40 ClientTLSAndNonTLS 41 ) 42 43 // allow alphanumerics, underscores and dashes 44 var testNameCleanRegex = regexp.MustCompile(`[^a-zA-Z0-9 \-_]+`) 45 46 func NewConfigNoTLS() *EtcdProcessClusterConfig { 47 return &EtcdProcessClusterConfig{ClusterSize: 3, 48 InitialToken: "new", 49 } 50 } 51 52 func NewConfigAutoTLS() *EtcdProcessClusterConfig { 53 return &EtcdProcessClusterConfig{ 54 ClusterSize: 3, 55 IsPeerTLS: true, 56 IsPeerAutoTLS: true, 57 InitialToken: "new", 58 } 59 } 60 61 func NewConfigTLS() *EtcdProcessClusterConfig { 62 return &EtcdProcessClusterConfig{ 63 ClusterSize: 3, 64 ClientTLS: ClientTLS, 65 IsPeerTLS: true, 66 InitialToken: "new", 67 } 68 } 69 70 func NewConfigClientTLS() *EtcdProcessClusterConfig { 71 return &EtcdProcessClusterConfig{ 72 ClusterSize: 3, 73 ClientTLS: ClientTLS, 74 InitialToken: "new", 75 } 76 } 77 78 func NewConfigClientAutoTLS() *EtcdProcessClusterConfig { 79 return &EtcdProcessClusterConfig{ 80 ClusterSize: 1, 81 IsClientAutoTLS: true, 82 ClientTLS: ClientTLS, 83 InitialToken: "new", 84 } 85 } 86 87 func NewConfigPeerTLS() *EtcdProcessClusterConfig { 88 return &EtcdProcessClusterConfig{ 89 ClusterSize: 3, 90 IsPeerTLS: true, 91 InitialToken: "new", 92 } 93 } 94 95 func NewConfigClientTLSCertAuth() *EtcdProcessClusterConfig { 96 return &EtcdProcessClusterConfig{ 97 ClusterSize: 1, 98 ClientTLS: ClientTLS, 99 InitialToken: "new", 100 ClientCertAuthEnabled: true, 101 } 102 } 103 104 func NewConfigClientTLSCertAuthWithNoCN() *EtcdProcessClusterConfig { 105 return &EtcdProcessClusterConfig{ 106 ClusterSize: 1, 107 ClientTLS: ClientTLS, 108 InitialToken: "new", 109 ClientCertAuthEnabled: true, 110 NoCN: true, 111 } 112 } 113 114 func NewConfigJWT() *EtcdProcessClusterConfig { 115 return &EtcdProcessClusterConfig{ 116 ClusterSize: 1, 117 InitialToken: "new", 118 AuthTokenOpts: "jwt,pub-key=" + path.Join(FixturesDir, "server.crt") + 119 ",priv-key=" + path.Join(FixturesDir, "server.key.insecure") + ",sign-method=RS256,ttl=1s", 120 } 121 } 122 123 func ConfigStandalone(cfg EtcdProcessClusterConfig) *EtcdProcessClusterConfig { 124 ret := cfg 125 ret.ClusterSize = 1 126 return &ret 127 } 128 129 type EtcdProcessCluster struct { 130 lg *zap.Logger 131 Cfg *EtcdProcessClusterConfig 132 Procs []EtcdProcess 133 } 134 135 type EtcdProcessClusterConfig struct { 136 ExecPath string 137 DataDirPath string 138 KeepDataDir bool 139 EnvVars map[string]string 140 141 ClusterSize int 142 143 BaseScheme string 144 BasePort int 145 146 MetricsURLScheme string 147 148 SnapshotCount int // default is 10000 149 150 ClientTLS ClientConnType 151 ClientCertAuthEnabled bool 152 IsPeerTLS bool 153 IsPeerAutoTLS bool 154 IsClientAutoTLS bool 155 IsClientCRL bool 156 NoCN bool 157 158 CipherSuites []string 159 160 ForceNewCluster bool 161 InitialToken string 162 QuotaBackendBytes int64 163 DisableStrictReconfigCheck bool 164 EnableV2 bool 165 InitialCorruptCheck bool 166 AuthTokenOpts string 167 V2deprecation string 168 169 RollingStart bool 170 171 Discovery string // v2 discovery 172 173 DiscoveryEndpoints []string // v3 discovery 174 DiscoveryToken string 175 LogLevel string 176 177 MaxConcurrentStreams uint32 // default is math.MaxUint32 178 CorruptCheckTime time.Duration 179 CompactHashCheckEnabled bool 180 CompactHashCheckTime time.Duration 181 } 182 183 // NewEtcdProcessCluster launches a new cluster from etcd processes, returning 184 // a new EtcdProcessCluster once all nodes are ready to accept client requests. 185 func NewEtcdProcessCluster(ctx context.Context, t testing.TB, cfg *EtcdProcessClusterConfig) (*EtcdProcessCluster, error) { 186 epc, err := InitEtcdProcessCluster(t, cfg) 187 if err != nil { 188 return nil, err 189 } 190 191 return StartEtcdProcessCluster(ctx, epc, cfg) 192 } 193 194 // InitEtcdProcessCluster initializes a new cluster based on the given config. 195 // It doesn't start the cluster. 196 func InitEtcdProcessCluster(t testing.TB, cfg *EtcdProcessClusterConfig) (*EtcdProcessCluster, error) { 197 SkipInShortMode(t) 198 199 etcdCfgs := cfg.EtcdServerProcessConfigs(t) 200 epc := &EtcdProcessCluster{ 201 Cfg: cfg, 202 lg: zaptest.NewLogger(t), 203 Procs: make([]EtcdProcess, cfg.ClusterSize), 204 } 205 206 // launch etcd processes 207 for i := range etcdCfgs { 208 proc, err := NewEtcdProcess(etcdCfgs[i]) 209 if err != nil { 210 epc.Close() 211 return nil, fmt.Errorf("cannot configure: %v", err) 212 } 213 epc.Procs[i] = proc 214 } 215 216 return epc, nil 217 } 218 219 // StartEtcdProcessCluster launches a new cluster from etcd processes. 220 func StartEtcdProcessCluster(ctx context.Context, epc *EtcdProcessCluster, cfg *EtcdProcessClusterConfig) (*EtcdProcessCluster, error) { 221 if cfg.RollingStart { 222 if err := epc.RollingStart(ctx); err != nil { 223 return nil, fmt.Errorf("cannot rolling-start: %v", err) 224 } 225 } else { 226 if err := epc.Start(ctx); err != nil { 227 return nil, fmt.Errorf("cannot start: %v", err) 228 } 229 } 230 231 return epc, nil 232 } 233 234 func (cfg *EtcdProcessClusterConfig) ClientScheme() string { 235 if cfg.ClientTLS == ClientTLS { 236 return "https" 237 } 238 return "http" 239 } 240 241 func (cfg *EtcdProcessClusterConfig) PeerScheme() string { 242 peerScheme := cfg.BaseScheme 243 if peerScheme == "" { 244 peerScheme = "http" 245 } 246 if cfg.IsPeerTLS { 247 peerScheme += "s" 248 } 249 return peerScheme 250 } 251 252 func (cfg *EtcdProcessClusterConfig) EtcdServerProcessConfigs(tb testing.TB) []*EtcdServerProcessConfig { 253 lg := zaptest.NewLogger(tb) 254 255 if cfg.BasePort == 0 { 256 cfg.BasePort = EtcdProcessBasePort 257 } 258 if cfg.ExecPath == "" { 259 cfg.ExecPath = BinPath 260 } 261 if cfg.SnapshotCount == 0 { 262 cfg.SnapshotCount = etcdserver.DefaultSnapshotCount 263 } 264 265 etcdCfgs := make([]*EtcdServerProcessConfig, cfg.ClusterSize) 266 initialCluster := make([]string, cfg.ClusterSize) 267 for i := 0; i < cfg.ClusterSize; i++ { 268 var curls []string 269 var curl, curltls string 270 port := cfg.BasePort + 5*i 271 curlHost := fmt.Sprintf("localhost:%d", port) 272 273 switch cfg.ClientTLS { 274 case ClientNonTLS, ClientTLS: 275 curl = (&url.URL{Scheme: cfg.ClientScheme(), Host: curlHost}).String() 276 curls = []string{curl} 277 case ClientTLSAndNonTLS: 278 curl = (&url.URL{Scheme: "http", Host: curlHost}).String() 279 curltls = (&url.URL{Scheme: "https", Host: curlHost}).String() 280 curls = []string{curl, curltls} 281 } 282 283 purl := url.URL{Scheme: cfg.PeerScheme(), Host: fmt.Sprintf("localhost:%d", port+1)} 284 285 name := fmt.Sprintf("%s-test-%d", testNameCleanRegex.ReplaceAllString(tb.Name(), ""), i) 286 dataDirPath := cfg.DataDirPath 287 if cfg.DataDirPath == "" { 288 dataDirPath = tb.TempDir() 289 } 290 initialCluster[i] = fmt.Sprintf("%s=%s", name, purl.String()) 291 292 args := []string{ 293 "--name", name, 294 "--listen-client-urls", strings.Join(curls, ","), 295 "--advertise-client-urls", strings.Join(curls, ","), 296 "--listen-peer-urls", purl.String(), 297 "--initial-advertise-peer-urls", purl.String(), 298 "--initial-cluster-token", cfg.InitialToken, 299 "--data-dir", dataDirPath, 300 "--snapshot-count", fmt.Sprintf("%d", cfg.SnapshotCount), 301 } 302 303 if cfg.ForceNewCluster { 304 args = append(args, "--force-new-cluster") 305 } 306 if cfg.QuotaBackendBytes > 0 { 307 args = append(args, 308 "--quota-backend-bytes", fmt.Sprintf("%d", cfg.QuotaBackendBytes), 309 ) 310 } 311 if cfg.DisableStrictReconfigCheck { 312 args = append(args, "--strict-reconfig-check=false") 313 } 314 if cfg.EnableV2 { 315 args = append(args, "--enable-v2") 316 } 317 if cfg.InitialCorruptCheck { 318 args = append(args, "--experimental-initial-corrupt-check") 319 } 320 var murl string 321 if cfg.MetricsURLScheme != "" { 322 murl = (&url.URL{ 323 Scheme: cfg.MetricsURLScheme, 324 Host: fmt.Sprintf("localhost:%d", port+2), 325 }).String() 326 args = append(args, "--listen-metrics-urls", murl) 327 } 328 329 args = append(args, cfg.TlsArgs()...) 330 331 if cfg.AuthTokenOpts != "" { 332 args = append(args, "--auth-token", cfg.AuthTokenOpts) 333 } 334 335 if cfg.V2deprecation != "" { 336 args = append(args, "--v2-deprecation", cfg.V2deprecation) 337 } 338 339 if cfg.Discovery != "" { 340 args = append(args, "--discovery", cfg.Discovery) 341 } 342 343 if cfg.LogLevel != "" { 344 args = append(args, "--log-level", cfg.LogLevel) 345 } 346 347 if cfg.MaxConcurrentStreams != 0 { 348 args = append(args, "--max-concurrent-streams", fmt.Sprintf("%d", cfg.MaxConcurrentStreams)) 349 } 350 351 if cfg.CorruptCheckTime != 0 { 352 args = append(args, "--experimental-corrupt-check-time", fmt.Sprintf("%s", cfg.CorruptCheckTime)) 353 } 354 if cfg.CompactHashCheckEnabled { 355 args = append(args, "--experimental-compact-hash-check-enabled") 356 } 357 if cfg.CompactHashCheckTime != 0 { 358 args = append(args, "--experimental-compact-hash-check-time", cfg.CompactHashCheckTime.String()) 359 } 360 361 etcdCfgs[i] = &EtcdServerProcessConfig{ 362 lg: lg, 363 ExecPath: cfg.ExecPath, 364 Args: args, 365 EnvVars: cfg.EnvVars, 366 TlsArgs: cfg.TlsArgs(), 367 DataDirPath: dataDirPath, 368 KeepDataDir: cfg.KeepDataDir, 369 Name: name, 370 Purl: purl, 371 Acurl: curl, 372 Murl: murl, 373 InitialToken: cfg.InitialToken, 374 } 375 } 376 377 if cfg.Discovery == "" && len(cfg.DiscoveryEndpoints) == 0 { 378 for i := range etcdCfgs { 379 initialClusterArgs := []string{"--initial-cluster", strings.Join(initialCluster, ",")} 380 etcdCfgs[i].InitialCluster = strings.Join(initialCluster, ",") 381 etcdCfgs[i].Args = append(etcdCfgs[i].Args, initialClusterArgs...) 382 } 383 } 384 385 if len(cfg.DiscoveryEndpoints) > 0 { 386 for i := range etcdCfgs { 387 etcdCfgs[i].Args = append(etcdCfgs[i].Args, fmt.Sprintf("--discovery-token=%s", cfg.DiscoveryToken)) 388 etcdCfgs[i].Args = append(etcdCfgs[i].Args, fmt.Sprintf("--discovery-endpoints=%s", strings.Join(cfg.DiscoveryEndpoints, ","))) 389 } 390 } 391 392 return etcdCfgs 393 } 394 395 func (cfg *EtcdProcessClusterConfig) TlsArgs() (args []string) { 396 if cfg.ClientTLS != ClientNonTLS { 397 if cfg.IsClientAutoTLS { 398 args = append(args, "--auto-tls") 399 } else { 400 tlsClientArgs := []string{ 401 "--cert-file", CertPath, 402 "--key-file", PrivateKeyPath, 403 "--trusted-ca-file", CaPath, 404 } 405 args = append(args, tlsClientArgs...) 406 407 if cfg.ClientCertAuthEnabled { 408 args = append(args, "--client-cert-auth") 409 } 410 } 411 } 412 413 if cfg.IsPeerTLS { 414 if cfg.IsPeerAutoTLS { 415 args = append(args, "--peer-auto-tls") 416 } else { 417 tlsPeerArgs := []string{ 418 "--peer-cert-file", CertPath, 419 "--peer-key-file", PrivateKeyPath, 420 "--peer-trusted-ca-file", CaPath, 421 } 422 args = append(args, tlsPeerArgs...) 423 } 424 } 425 426 if cfg.IsClientCRL { 427 args = append(args, "--client-crl-file", CrlPath, "--client-cert-auth") 428 } 429 430 if len(cfg.CipherSuites) > 0 { 431 args = append(args, "--cipher-suites", strings.Join(cfg.CipherSuites, ",")) 432 } 433 434 return args 435 } 436 437 func (epc *EtcdProcessCluster) EndpointsV2() []string { 438 return epc.Endpoints(func(ep EtcdProcess) []string { return ep.EndpointsV2() }) 439 } 440 441 func (epc *EtcdProcessCluster) EndpointsV3() []string { 442 return epc.Endpoints(func(ep EtcdProcess) []string { return ep.EndpointsV3() }) 443 } 444 445 func (epc *EtcdProcessCluster) Endpoints(f func(ep EtcdProcess) []string) (ret []string) { 446 for _, p := range epc.Procs { 447 ret = append(ret, f(p)...) 448 } 449 return ret 450 } 451 452 func (epc *EtcdProcessCluster) Start(ctx context.Context) error { 453 return epc.start(func(ep EtcdProcess) error { return ep.Start(ctx) }) 454 } 455 456 func (epc *EtcdProcessCluster) RollingStart(ctx context.Context) error { 457 return epc.rollingStart(func(ep EtcdProcess) error { return ep.Start(ctx) }) 458 } 459 460 func (epc *EtcdProcessCluster) Restart(ctx context.Context) error { 461 return epc.start(func(ep EtcdProcess) error { return ep.Restart(ctx) }) 462 } 463 464 func (epc *EtcdProcessCluster) start(f func(ep EtcdProcess) error) error { 465 readyC := make(chan error, len(epc.Procs)) 466 for i := range epc.Procs { 467 go func(n int) { readyC <- f(epc.Procs[n]) }(i) 468 } 469 for range epc.Procs { 470 if err := <-readyC; err != nil { 471 epc.Close() 472 return err 473 } 474 } 475 return nil 476 } 477 478 func (epc *EtcdProcessCluster) rollingStart(f func(ep EtcdProcess) error) error { 479 readyC := make(chan error, len(epc.Procs)) 480 for i := range epc.Procs { 481 go func(n int) { readyC <- f(epc.Procs[n]) }(i) 482 // make sure the servers do not start at the same time 483 time.Sleep(time.Second) 484 } 485 for range epc.Procs { 486 if err := <-readyC; err != nil { 487 epc.Close() 488 return err 489 } 490 } 491 return nil 492 } 493 494 func (epc *EtcdProcessCluster) Stop() (err error) { 495 for _, p := range epc.Procs { 496 if p == nil { 497 continue 498 } 499 if curErr := p.Stop(); curErr != nil { 500 if err != nil { 501 err = fmt.Errorf("%v; %v", err, curErr) 502 } else { 503 err = curErr 504 } 505 } 506 } 507 return err 508 } 509 510 func (epc *EtcdProcessCluster) Close() error { 511 epc.lg.Info("closing test cluster...") 512 err := epc.Stop() 513 for _, p := range epc.Procs { 514 // p is nil when NewEtcdProcess fails in the middle 515 // Close still gets called to clean up test data 516 if p == nil { 517 continue 518 } 519 if cerr := p.Close(); cerr != nil { 520 err = cerr 521 } 522 } 523 epc.lg.Info("closed test cluster.") 524 return err 525 }