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  }