github.com/lfch/etcd-io/tests/v3@v3.0.0-20221004140520-eac99acd3e9d/e2e/ctl_v3_test.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  	"os"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/stretchr/testify/assert"
    26  	"github.com/lfch/etcd-io/api/v3/version"
    27  	"github.com/lfch/etcd-io/client/pkg/v3/fileutil"
    28  	"github.com/lfch/etcd-io/client/pkg/v3/testutil"
    29  	"github.com/lfch/etcd-io/pkg/v3/flags"
    30  	"github.com/lfch/etcd-io/tests/v3/framework/e2e"
    31  )
    32  
    33  func TestCtlV3Version(t *testing.T) { testCtl(t, versionTest) }
    34  
    35  func TestClusterVersion(t *testing.T) {
    36  	e2e.BeforeTest(t)
    37  
    38  	tests := []struct {
    39  		name         string
    40  		rollingStart bool
    41  	}{
    42  		{
    43  			name:         "When start servers at the same time",
    44  			rollingStart: false,
    45  		},
    46  		{
    47  			name:         "When start servers one by one",
    48  			rollingStart: true,
    49  		},
    50  	}
    51  
    52  	for _, tt := range tests {
    53  		t.Run(tt.name, func(t *testing.T) {
    54  			binary := e2e.BinDir + "/etcd"
    55  			if !fileutil.Exist(binary) {
    56  				t.Skipf("%q does not exist", binary)
    57  			}
    58  			e2e.BeforeTest(t)
    59  			cfg := e2e.NewConfigNoTLS()
    60  			cfg.ExecPath = binary
    61  			cfg.SnapshotCount = 3
    62  			cfg.BaseScheme = "unix" // to avoid port conflict
    63  			cfg.RollingStart = tt.rollingStart
    64  
    65  			epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t, cfg)
    66  			if err != nil {
    67  				t.Fatalf("could not start etcd process cluster (%v)", err)
    68  			}
    69  			defer func() {
    70  				if errC := epc.Close(); errC != nil {
    71  					t.Fatalf("error closing etcd processes (%v)", errC)
    72  				}
    73  			}()
    74  
    75  			ctx := ctlCtx{
    76  				t:   t,
    77  				cfg: *cfg,
    78  				epc: epc,
    79  			}
    80  			cv := version.Cluster(version.Version)
    81  			clusterVersionTest(ctx, `"etcdcluster":"`+cv)
    82  		})
    83  	}
    84  }
    85  
    86  func versionTest(cx ctlCtx) {
    87  	if err := ctlV3Version(cx); err != nil {
    88  		cx.t.Fatalf("versionTest ctlV3Version error (%v)", err)
    89  	}
    90  }
    91  
    92  func clusterVersionTest(cx ctlCtx, expected string) {
    93  	var err error
    94  	for i := 0; i < 35; i++ {
    95  		if err = e2e.CURLGet(cx.epc, e2e.CURLReq{Endpoint: "/version", Expected: expected}); err != nil {
    96  			cx.t.Logf("#%d: v3 is not ready yet (%v)", i, err)
    97  			time.Sleep(200 * time.Millisecond)
    98  			continue
    99  		}
   100  		break
   101  	}
   102  	if err != nil {
   103  		cx.t.Fatalf("failed cluster version test expected %v got (%v)", expected, err)
   104  	}
   105  }
   106  
   107  func ctlV3Version(cx ctlCtx) error {
   108  	cmdArgs := append(cx.PrefixArgs(), "version")
   109  	return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, version.Version)
   110  }
   111  
   112  // TestCtlV3DialWithHTTPScheme ensures that client handles Endpoints with HTTPS scheme.
   113  func TestCtlV3DialWithHTTPScheme(t *testing.T) {
   114  	testCtl(t, dialWithSchemeTest, withCfg(*e2e.NewConfigClientTLS()))
   115  }
   116  
   117  func dialWithSchemeTest(cx ctlCtx) {
   118  	cmdArgs := append(cx.prefixArgs(cx.epc.EndpointsV3()), "put", "foo", "bar")
   119  	if err := e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, "OK"); err != nil {
   120  		cx.t.Fatal(err)
   121  	}
   122  }
   123  
   124  type ctlCtx struct {
   125  	t         *testing.T
   126  	apiPrefix string
   127  	cfg       e2e.EtcdProcessClusterConfig
   128  
   129  	corruptFunc                func(string) error
   130  	disableStrictReconfigCheck bool
   131  
   132  	epc *e2e.EtcdProcessCluster
   133  
   134  	envMap map[string]string
   135  
   136  	dialTimeout time.Duration
   137  	testTimeout time.Duration
   138  
   139  	quorum      bool // if true, set up 3-node cluster and linearizable read
   140  	interactive bool
   141  
   142  	user string
   143  	pass string
   144  
   145  	initialCorruptCheck bool
   146  
   147  	// dir that was used during the test
   148  	dataDir string
   149  }
   150  
   151  type ctlOption func(*ctlCtx)
   152  
   153  func (cx *ctlCtx) applyOpts(opts []ctlOption) {
   154  	for _, opt := range opts {
   155  		opt(cx)
   156  	}
   157  
   158  	cx.initialCorruptCheck = true
   159  }
   160  
   161  func withCfg(cfg e2e.EtcdProcessClusterConfig) ctlOption {
   162  	return func(cx *ctlCtx) { cx.cfg = cfg }
   163  }
   164  
   165  func withDialTimeout(timeout time.Duration) ctlOption {
   166  	return func(cx *ctlCtx) { cx.dialTimeout = timeout }
   167  }
   168  
   169  func withTestTimeout(timeout time.Duration) ctlOption {
   170  	return func(cx *ctlCtx) { cx.testTimeout = timeout }
   171  }
   172  
   173  func withQuorum() ctlOption {
   174  	return func(cx *ctlCtx) { cx.quorum = true }
   175  }
   176  
   177  func withInteractive() ctlOption {
   178  	return func(cx *ctlCtx) { cx.interactive = true }
   179  }
   180  
   181  func withInitialCorruptCheck() ctlOption {
   182  	return func(cx *ctlCtx) { cx.initialCorruptCheck = true }
   183  }
   184  
   185  func withCorruptFunc(f func(string) error) ctlOption {
   186  	return func(cx *ctlCtx) { cx.corruptFunc = f }
   187  }
   188  
   189  func withDisableStrictReconfig() ctlOption {
   190  	return func(cx *ctlCtx) { cx.disableStrictReconfigCheck = true }
   191  }
   192  
   193  func withApiPrefix(p string) ctlOption {
   194  	return func(cx *ctlCtx) { cx.apiPrefix = p }
   195  }
   196  
   197  func withFlagByEnv() ctlOption {
   198  	return func(cx *ctlCtx) { cx.envMap = make(map[string]string) }
   199  }
   200  
   201  // This function must be called after the `withCfg`, otherwise its value
   202  // may be overwritten by `withCfg`.
   203  func withMaxConcurrentStreams(streams uint32) ctlOption {
   204  	return func(cx *ctlCtx) {
   205  		cx.cfg.MaxConcurrentStreams = streams
   206  	}
   207  }
   208  
   209  func testCtl(t *testing.T, testFunc func(ctlCtx), opts ...ctlOption) {
   210  	testCtlWithOffline(t, testFunc, nil, opts...)
   211  }
   212  
   213  func getDefaultCtlCtx(t *testing.T) ctlCtx {
   214  	return ctlCtx{
   215  		t:           t,
   216  		cfg:         *e2e.NewConfigAutoTLS(),
   217  		dialTimeout: 7 * time.Second,
   218  	}
   219  }
   220  
   221  func testCtlWithOffline(t *testing.T, testFunc func(ctlCtx), testOfflineFunc func(ctlCtx), opts ...ctlOption) {
   222  	e2e.BeforeTest(t)
   223  
   224  	ret := getDefaultCtlCtx(t)
   225  	ret.applyOpts(opts)
   226  
   227  	if !ret.quorum {
   228  		ret.cfg = *e2e.ConfigStandalone(ret.cfg)
   229  	}
   230  	ret.cfg.DisableStrictReconfigCheck = ret.disableStrictReconfigCheck
   231  	if ret.initialCorruptCheck {
   232  		ret.cfg.InitialCorruptCheck = ret.initialCorruptCheck
   233  	}
   234  	if testOfflineFunc != nil {
   235  		ret.cfg.KeepDataDir = true
   236  	}
   237  
   238  	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t, &ret.cfg)
   239  	if err != nil {
   240  		t.Fatalf("could not start etcd process cluster (%v)", err)
   241  	}
   242  	ret.epc = epc
   243  	ret.dataDir = epc.Procs[0].Config().DataDirPath
   244  
   245  	runCtlTest(t, testFunc, testOfflineFunc, ret)
   246  }
   247  
   248  func runCtlTest(t *testing.T, testFunc func(ctlCtx), testOfflineFunc func(ctlCtx), cx ctlCtx) {
   249  	defer func() {
   250  		if cx.envMap != nil {
   251  			for k := range cx.envMap {
   252  				os.Unsetenv(k)
   253  			}
   254  			cx.envMap = make(map[string]string)
   255  		}
   256  		if cx.epc != nil {
   257  			if errC := cx.epc.Close(); errC != nil {
   258  				t.Fatalf("error closing etcd processes (%v)", errC)
   259  			}
   260  		}
   261  	}()
   262  
   263  	donec := make(chan struct{})
   264  	go func() {
   265  		defer close(donec)
   266  		testFunc(cx)
   267  		t.Log("---testFunc logic DONE")
   268  	}()
   269  
   270  	timeout := cx.getTestTimeout()
   271  
   272  	select {
   273  	case <-time.After(timeout):
   274  		testutil.FatalStack(t, fmt.Sprintf("test timed out after %v", timeout))
   275  	case <-donec:
   276  	}
   277  
   278  	t.Log("closing test cluster...")
   279  	assert.NoError(t, cx.epc.Close())
   280  	cx.epc = nil
   281  	t.Log("closed test cluster...")
   282  
   283  	if testOfflineFunc != nil {
   284  		testOfflineFunc(cx)
   285  	}
   286  }
   287  
   288  func (cx *ctlCtx) getTestTimeout() time.Duration {
   289  	timeout := cx.testTimeout
   290  	if timeout == 0 {
   291  		timeout = 2*cx.dialTimeout + time.Second
   292  		if cx.dialTimeout == 0 {
   293  			timeout = 30 * time.Second
   294  		}
   295  	}
   296  	return timeout
   297  }
   298  
   299  func (cx *ctlCtx) prefixArgs(eps []string) []string {
   300  	fmap := make(map[string]string)
   301  	fmap["endpoints"] = strings.Join(eps, ",")
   302  	fmap["dial-timeout"] = cx.dialTimeout.String()
   303  	if cx.epc.Cfg.ClientTLS == e2e.ClientTLS {
   304  		if cx.epc.Cfg.IsClientAutoTLS {
   305  			fmap["insecure-transport"] = "false"
   306  			fmap["insecure-skip-tls-verify"] = "true"
   307  		} else if cx.epc.Cfg.IsClientCRL {
   308  			fmap["cacert"] = e2e.CaPath
   309  			fmap["cert"] = e2e.RevokedCertPath
   310  			fmap["key"] = e2e.RevokedPrivateKeyPath
   311  		} else {
   312  			fmap["cacert"] = e2e.CaPath
   313  			fmap["cert"] = e2e.CertPath
   314  			fmap["key"] = e2e.PrivateKeyPath
   315  		}
   316  	}
   317  	if cx.user != "" {
   318  		fmap["user"] = cx.user + ":" + cx.pass
   319  	}
   320  
   321  	useEnv := cx.envMap != nil
   322  
   323  	cmdArgs := []string{e2e.CtlBinPath}
   324  	for k, v := range fmap {
   325  		if useEnv {
   326  			ek := flags.FlagToEnv("ETCDCTL", k)
   327  			cx.envMap[ek] = v
   328  		} else {
   329  			cmdArgs = append(cmdArgs, fmt.Sprintf("--%s=%s", k, v))
   330  		}
   331  	}
   332  	return cmdArgs
   333  }
   334  
   335  // PrefixArgs prefixes etcdctl command.
   336  // Make sure to unset environment variables after tests.
   337  func (cx *ctlCtx) PrefixArgs() []string {
   338  	return cx.prefixArgs(cx.epc.EndpointsV3())
   339  }
   340  
   341  // PrefixArgsUtl returns prefix of the command that is etcdutl
   342  // Please not thet 'utl' compatible commands does not consume --endpoints flag.
   343  func (cx *ctlCtx) PrefixArgsUtl() []string {
   344  	return []string{e2e.UtlBinPath}
   345  }
   346  
   347  func isGRPCTimedout(err error) bool {
   348  	return strings.Contains(err.Error(), "grpc: timed out trying to connect")
   349  }
   350  
   351  func (cx *ctlCtx) memberToRemove() (ep string, memberID string, clusterID string) {
   352  	n1 := cx.cfg.ClusterSize
   353  	if n1 < 2 {
   354  		cx.t.Fatalf("%d-node is too small to test 'member remove'", n1)
   355  	}
   356  
   357  	resp, err := getMemberList(*cx)
   358  	if err != nil {
   359  		cx.t.Fatal(err)
   360  	}
   361  	if n1 != len(resp.Members) {
   362  		cx.t.Fatalf("expected %d, got %d", n1, len(resp.Members))
   363  	}
   364  
   365  	ep = resp.Members[0].ClientURLs[0]
   366  	clusterID = fmt.Sprintf("%x", resp.Header.ClusterId)
   367  	memberID = fmt.Sprintf("%x", resp.Members[1].ID)
   368  
   369  	return ep, memberID, clusterID
   370  }