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
15package e2e
16
17import (
18	"fmt"
19	"os"
20	"strings"
21	"testing"
22	"time"
23
24	"github.com/coreos/etcd/pkg/flags"
25	"github.com/coreos/etcd/pkg/testutil"
26	"github.com/coreos/etcd/version"
27)
28
29func TestCtlV3Version(t *testing.T) { testCtl(t, versionTest) }
30
31func versionTest(cx ctlCtx) {
32	if err := ctlV3Version(cx); err != nil {
33		cx.t.Fatalf("versionTest ctlV3Version error (%v)", err)
34	}
35}
36
37func ctlV3Version(cx ctlCtx) error {
38	cmdArgs := append(cx.PrefixArgs(), "version")
39	return spawnWithExpect(cmdArgs, version.Version)
40}
41
42// TestCtlV3DialWithHTTPScheme ensures that client handles endpoints with HTTPS scheme.
43func TestCtlV3DialWithHTTPScheme(t *testing.T) {
44	testCtl(t, dialWithSchemeTest, withCfg(configClientTLS))
45}
46
47func dialWithSchemeTest(cx ctlCtx) {
48	cmdArgs := append(cx.prefixArgs(cx.epc.endpoints()), "put", "foo", "bar")
49	if err := spawnWithExpect(cmdArgs, "OK"); err != nil {
50		cx.t.Fatal(err)
51	}
52}
53
54type ctlCtx struct {
55	t                 *testing.T
56	cfg               etcdProcessClusterConfig
57	quotaBackendBytes int64
58	noStrictReconfig  bool
59
60	epc *etcdProcessCluster
61
62	envMap map[string]struct{}
63
64	dialTimeout time.Duration
65
66	quorum      bool // if true, set up 3-node cluster and linearizable read
67	interactive bool
68
69	user string
70	pass string
71
72	// for compaction
73	compactPhysical bool
74}
75
76type ctlOption func(*ctlCtx)
77
78func (cx *ctlCtx) applyOpts(opts []ctlOption) {
79	for _, opt := range opts {
80		opt(cx)
81	}
82}
83
84func withCfg(cfg etcdProcessClusterConfig) ctlOption {
85	return func(cx *ctlCtx) { cx.cfg = cfg }
86}
87
88func withDialTimeout(timeout time.Duration) ctlOption {
89	return func(cx *ctlCtx) { cx.dialTimeout = timeout }
90}
91
92func withQuorum() ctlOption {
93	return func(cx *ctlCtx) { cx.quorum = true }
94}
95
96func withInteractive() ctlOption {
97	return func(cx *ctlCtx) { cx.interactive = true }
98}
99
100func withQuota(b int64) ctlOption {
101	return func(cx *ctlCtx) { cx.quotaBackendBytes = b }
102}
103
104func withCompactPhysical() ctlOption {
105	return func(cx *ctlCtx) { cx.compactPhysical = true }
106}
107
108func withNoStrictReconfig() ctlOption {
109	return func(cx *ctlCtx) { cx.noStrictReconfig = true }
110}
111
112func withFlagByEnv() ctlOption {
113	return func(cx *ctlCtx) { cx.envMap = make(map[string]struct{}) }
114}
115
116func testCtl(t *testing.T, testFunc func(ctlCtx), opts ...ctlOption) {
117	defer testutil.AfterTest(t)
118
119	ret := ctlCtx{
120		t:           t,
121		cfg:         configAutoTLS,
122		dialTimeout: 7 * time.Second,
123	}
124	ret.applyOpts(opts)
125
126	os.Setenv("ETCDCTL_API", "3")
127	mustEtcdctl(t)
128	if !ret.quorum {
129		ret.cfg = *configStandalone(ret.cfg)
130	}
131	if ret.quotaBackendBytes > 0 {
132		ret.cfg.quotaBackendBytes = ret.quotaBackendBytes
133	}
134	ret.cfg.noStrictReconfig = ret.noStrictReconfig
135
136	epc, err := newEtcdProcessCluster(&ret.cfg)
137	if err != nil {
138		t.Fatalf("could not start etcd process cluster (%v)", err)
139	}
140	ret.epc = epc
141
142	defer func() {
143		os.Unsetenv("ETCDCTL_API")
144		if ret.envMap != nil {
145			for k := range ret.envMap {
146				os.Unsetenv(k)
147			}
148		}
149		if errC := ret.epc.Close(); errC != nil {
150			t.Fatalf("error closing etcd processes (%v)", errC)
151		}
152	}()
153
154	donec := make(chan struct{})
155	go func() {
156		defer close(donec)
157		testFunc(ret)
158	}()
159
160	timeout := 2*ret.dialTimeout + time.Second
161	if ret.dialTimeout == 0 {
162		timeout = 30 * time.Second
163	}
164	select {
165	case <-time.After(timeout):
166		testutil.FatalStack(t, fmt.Sprintf("test timed out after %v", timeout))
167	case <-donec:
168	}
169}
170
171func (cx *ctlCtx) prefixArgs(eps []string) []string {
172	if len(cx.epc.proxies()) > 0 { // TODO: add proxy check as in v2
173		panic("v3 proxy not implemented")
174	}
175
176	fmap := make(map[string]string)
177	fmap["endpoints"] = strings.Join(eps, ",")
178	fmap["dial-timeout"] = cx.dialTimeout.String()
179	if cx.epc.cfg.clientTLS == clientTLS {
180		if cx.epc.cfg.isClientAutoTLS {
181			fmap["insecure-transport"] = "false"
182			fmap["insecure-skip-tls-verify"] = "true"
183		} else {
184			fmap["cacert"] = caPath
185			fmap["cert"] = certPath
186			fmap["key"] = privateKeyPath
187		}
188	}
189	if cx.user != "" {
190		fmap["user"] = cx.user + ":" + cx.pass
191	}
192
193	useEnv := cx.envMap != nil
194
195	cmdArgs := []string{ctlBinPath}
196	for k, v := range fmap {
197		if useEnv {
198			ek := flags.FlagToEnv("ETCDCTL", k)
199			os.Setenv(ek, v)
200			cx.envMap[ek] = struct{}{}
201		} else {
202			cmdArgs = append(cmdArgs, fmt.Sprintf("--%s=%s", k, v))
203		}
204	}
205	return cmdArgs
206}
207
208// PrefixArgs prefixes etcdctl command.
209// Make sure to unset environment variables after tests.
210func (cx *ctlCtx) PrefixArgs() []string {
211	return cx.prefixArgs(cx.epc.grpcEndpoints())
212}
213
214func isGRPCTimedout(err error) bool {
215	return strings.Contains(err.Error(), "grpc: timed out trying to connect")
216}
217
218func (cx *ctlCtx) memberToRemove() (ep string, memberID string, clusterID string) {
219	n1 := cx.cfg.clusterSize
220	if n1 < 2 {
221		cx.t.Fatalf("%d-node is too small to test 'member remove'", n1)
222	}
223
224	resp, err := getMemberList(*cx)
225	if err != nil {
226		cx.t.Fatal(err)
227	}
228	if n1 != len(resp.Members) {
229		cx.t.Fatalf("expected %d, got %d", n1, len(resp.Members))
230	}
231
232	ep = resp.Members[0].ClientURLs[0]
233	clusterID = fmt.Sprintf("%x", resp.Header.ClusterId)
234	memberID = fmt.Sprintf("%x", resp.Members[1].ID)
235
236	return ep, memberID, clusterID
237}
238