mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
tests/e2e: fix
Signed-off-by: Gyuho Lee <leegyuho@amazon.com>
This commit is contained in:
parent
61065db065
commit
be3babffb7
21
tests/e2e/cluster_direct_test.go
Normal file
21
tests/e2e/cluster_direct_test.go
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright 2017 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !cluster_proxy
|
||||
|
||||
package e2e
|
||||
|
||||
func newEtcdProcess(cfg *etcdServerProcessConfig) (etcdProcess, error) {
|
||||
return newEtcdServerProcess(cfg)
|
||||
}
|
402
tests/e2e/cluster_test.go
Normal file
402
tests/e2e/cluster_test.go
Normal file
@ -0,0 +1,402 @@
|
||||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const etcdProcessBasePort = 20000
|
||||
|
||||
type clientConnType int
|
||||
|
||||
const (
|
||||
clientNonTLS clientConnType = iota
|
||||
clientTLS
|
||||
clientTLSAndNonTLS
|
||||
)
|
||||
|
||||
var (
|
||||
configNoTLS = etcdProcessClusterConfig{
|
||||
clusterSize: 3,
|
||||
initialToken: "new",
|
||||
}
|
||||
configAutoTLS = etcdProcessClusterConfig{
|
||||
clusterSize: 3,
|
||||
isPeerTLS: true,
|
||||
isPeerAutoTLS: true,
|
||||
initialToken: "new",
|
||||
}
|
||||
configTLS = etcdProcessClusterConfig{
|
||||
clusterSize: 3,
|
||||
clientTLS: clientTLS,
|
||||
isPeerTLS: true,
|
||||
initialToken: "new",
|
||||
}
|
||||
configClientTLS = etcdProcessClusterConfig{
|
||||
clusterSize: 3,
|
||||
clientTLS: clientTLS,
|
||||
initialToken: "new",
|
||||
}
|
||||
configClientBoth = etcdProcessClusterConfig{
|
||||
clusterSize: 1,
|
||||
clientTLS: clientTLSAndNonTLS,
|
||||
initialToken: "new",
|
||||
}
|
||||
configClientAutoTLS = etcdProcessClusterConfig{
|
||||
clusterSize: 1,
|
||||
isClientAutoTLS: true,
|
||||
clientTLS: clientTLS,
|
||||
initialToken: "new",
|
||||
}
|
||||
configPeerTLS = etcdProcessClusterConfig{
|
||||
clusterSize: 3,
|
||||
isPeerTLS: true,
|
||||
initialToken: "new",
|
||||
}
|
||||
configClientTLSCertAuth = etcdProcessClusterConfig{
|
||||
clusterSize: 1,
|
||||
clientTLS: clientTLS,
|
||||
initialToken: "new",
|
||||
clientCertAuthEnabled: true,
|
||||
}
|
||||
configClientTLSCertAuthWithNoCN = etcdProcessClusterConfig{
|
||||
clusterSize: 1,
|
||||
clientTLS: clientTLS,
|
||||
initialToken: "new",
|
||||
clientCertAuthEnabled: true,
|
||||
noCN: true,
|
||||
}
|
||||
configJWT = etcdProcessClusterConfig{
|
||||
clusterSize: 1,
|
||||
initialToken: "new",
|
||||
authTokenOpts: "jwt,pub-key=../../integration/fixtures/server.crt,priv-key=../../integration/fixtures/server.key.insecure,sign-method=RS256,ttl=1s",
|
||||
}
|
||||
)
|
||||
|
||||
func configStandalone(cfg etcdProcessClusterConfig) *etcdProcessClusterConfig {
|
||||
ret := cfg
|
||||
ret.clusterSize = 1
|
||||
return &ret
|
||||
}
|
||||
|
||||
type etcdProcessCluster struct {
|
||||
cfg *etcdProcessClusterConfig
|
||||
procs []etcdProcess
|
||||
}
|
||||
|
||||
type etcdProcessClusterConfig struct {
|
||||
execPath string
|
||||
dataDirPath string
|
||||
keepDataDir bool
|
||||
|
||||
clusterSize int
|
||||
|
||||
baseScheme string
|
||||
basePort int
|
||||
|
||||
metricsURLScheme string
|
||||
|
||||
snapshotCount int // default is 10000
|
||||
|
||||
clientTLS clientConnType
|
||||
clientCertAuthEnabled bool
|
||||
isPeerTLS bool
|
||||
isPeerAutoTLS bool
|
||||
isClientAutoTLS bool
|
||||
isClientCRL bool
|
||||
noCN bool
|
||||
|
||||
cipherSuites []string
|
||||
|
||||
forceNewCluster bool
|
||||
initialToken string
|
||||
quotaBackendBytes int64
|
||||
noStrictReconfig bool
|
||||
enableV2 bool
|
||||
initialCorruptCheck bool
|
||||
authTokenOpts string
|
||||
}
|
||||
|
||||
// newEtcdProcessCluster launches a new cluster from etcd processes, returning
|
||||
// a new etcdProcessCluster once all nodes are ready to accept client requests.
|
||||
func newEtcdProcessCluster(cfg *etcdProcessClusterConfig) (*etcdProcessCluster, error) {
|
||||
etcdCfgs := cfg.etcdServerProcessConfigs()
|
||||
epc := &etcdProcessCluster{
|
||||
cfg: cfg,
|
||||
procs: make([]etcdProcess, cfg.clusterSize),
|
||||
}
|
||||
|
||||
// launch etcd processes
|
||||
for i := range etcdCfgs {
|
||||
proc, err := newEtcdProcess(etcdCfgs[i])
|
||||
if err != nil {
|
||||
epc.Close()
|
||||
return nil, err
|
||||
}
|
||||
epc.procs[i] = proc
|
||||
}
|
||||
|
||||
if err := epc.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return epc, nil
|
||||
}
|
||||
|
||||
func (cfg *etcdProcessClusterConfig) clientScheme() string {
|
||||
if cfg.clientTLS == clientTLS {
|
||||
return "https"
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (cfg *etcdProcessClusterConfig) peerScheme() string {
|
||||
peerScheme := cfg.baseScheme
|
||||
if peerScheme == "" {
|
||||
peerScheme = "http"
|
||||
}
|
||||
if cfg.isPeerTLS {
|
||||
peerScheme += "s"
|
||||
}
|
||||
return peerScheme
|
||||
}
|
||||
|
||||
func (cfg *etcdProcessClusterConfig) etcdServerProcessConfigs() []*etcdServerProcessConfig {
|
||||
if cfg.basePort == 0 {
|
||||
cfg.basePort = etcdProcessBasePort
|
||||
}
|
||||
if cfg.execPath == "" {
|
||||
cfg.execPath = binPath
|
||||
}
|
||||
if cfg.snapshotCount == 0 {
|
||||
cfg.snapshotCount = 10000
|
||||
}
|
||||
|
||||
etcdCfgs := make([]*etcdServerProcessConfig, cfg.clusterSize)
|
||||
initialCluster := make([]string, cfg.clusterSize)
|
||||
for i := 0; i < cfg.clusterSize; i++ {
|
||||
var curls []string
|
||||
var curl, curltls string
|
||||
port := cfg.basePort + 5*i
|
||||
curlHost := fmt.Sprintf("localhost:%d", port)
|
||||
|
||||
switch cfg.clientTLS {
|
||||
case clientNonTLS, clientTLS:
|
||||
curl = (&url.URL{Scheme: cfg.clientScheme(), Host: curlHost}).String()
|
||||
curls = []string{curl}
|
||||
case clientTLSAndNonTLS:
|
||||
curl = (&url.URL{Scheme: "http", Host: curlHost}).String()
|
||||
curltls = (&url.URL{Scheme: "https", Host: curlHost}).String()
|
||||
curls = []string{curl, curltls}
|
||||
}
|
||||
|
||||
purl := url.URL{Scheme: cfg.peerScheme(), Host: fmt.Sprintf("localhost:%d", port+1)}
|
||||
name := fmt.Sprintf("testname%d", i)
|
||||
dataDirPath := cfg.dataDirPath
|
||||
if cfg.dataDirPath == "" {
|
||||
var derr error
|
||||
dataDirPath, derr = ioutil.TempDir("", name+".etcd")
|
||||
if derr != nil {
|
||||
panic(fmt.Sprintf("could not get tempdir for datadir: %s", derr))
|
||||
}
|
||||
}
|
||||
initialCluster[i] = fmt.Sprintf("%s=%s", name, purl.String())
|
||||
|
||||
args := []string{
|
||||
"--name", name,
|
||||
"--listen-client-urls", strings.Join(curls, ","),
|
||||
"--advertise-client-urls", strings.Join(curls, ","),
|
||||
"--listen-peer-urls", purl.String(),
|
||||
"--initial-advertise-peer-urls", purl.String(),
|
||||
"--initial-cluster-token", cfg.initialToken,
|
||||
"--data-dir", dataDirPath,
|
||||
"--snapshot-count", fmt.Sprintf("%d", cfg.snapshotCount),
|
||||
}
|
||||
args = addV2Args(args)
|
||||
if cfg.forceNewCluster {
|
||||
args = append(args, "--force-new-cluster")
|
||||
}
|
||||
if cfg.quotaBackendBytes > 0 {
|
||||
args = append(args,
|
||||
"--quota-backend-bytes", fmt.Sprintf("%d", cfg.quotaBackendBytes),
|
||||
)
|
||||
}
|
||||
if cfg.noStrictReconfig {
|
||||
args = append(args, "--strict-reconfig-check=false")
|
||||
}
|
||||
if cfg.enableV2 {
|
||||
args = append(args, "--enable-v2")
|
||||
}
|
||||
if cfg.initialCorruptCheck {
|
||||
args = append(args, "--experimental-initial-corrupt-check")
|
||||
}
|
||||
var murl string
|
||||
if cfg.metricsURLScheme != "" {
|
||||
murl = (&url.URL{
|
||||
Scheme: cfg.metricsURLScheme,
|
||||
Host: fmt.Sprintf("localhost:%d", port+2),
|
||||
}).String()
|
||||
args = append(args, "--listen-metrics-urls", murl)
|
||||
}
|
||||
|
||||
args = append(args, cfg.tlsArgs()...)
|
||||
|
||||
if cfg.authTokenOpts != "" {
|
||||
args = append(args, "--auth-token", cfg.authTokenOpts)
|
||||
}
|
||||
|
||||
etcdCfgs[i] = &etcdServerProcessConfig{
|
||||
execPath: cfg.execPath,
|
||||
args: args,
|
||||
tlsArgs: cfg.tlsArgs(),
|
||||
dataDirPath: dataDirPath,
|
||||
keepDataDir: cfg.keepDataDir,
|
||||
name: name,
|
||||
purl: purl,
|
||||
acurl: curl,
|
||||
murl: murl,
|
||||
initialToken: cfg.initialToken,
|
||||
}
|
||||
}
|
||||
|
||||
initialClusterArgs := []string{"--initial-cluster", strings.Join(initialCluster, ",")}
|
||||
for i := range etcdCfgs {
|
||||
etcdCfgs[i].initialCluster = strings.Join(initialCluster, ",")
|
||||
etcdCfgs[i].args = append(etcdCfgs[i].args, initialClusterArgs...)
|
||||
}
|
||||
|
||||
return etcdCfgs
|
||||
}
|
||||
|
||||
func (cfg *etcdProcessClusterConfig) tlsArgs() (args []string) {
|
||||
if cfg.clientTLS != clientNonTLS {
|
||||
if cfg.isClientAutoTLS {
|
||||
args = append(args, "--auto-tls")
|
||||
} else {
|
||||
tlsClientArgs := []string{
|
||||
"--cert-file", certPath,
|
||||
"--key-file", privateKeyPath,
|
||||
"--trusted-ca-file", caPath,
|
||||
}
|
||||
args = append(args, tlsClientArgs...)
|
||||
|
||||
if cfg.clientCertAuthEnabled {
|
||||
args = append(args, "--client-cert-auth")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.isPeerTLS {
|
||||
if cfg.isPeerAutoTLS {
|
||||
args = append(args, "--peer-auto-tls")
|
||||
} else {
|
||||
tlsPeerArgs := []string{
|
||||
"--peer-cert-file", certPath,
|
||||
"--peer-key-file", privateKeyPath,
|
||||
"--peer-trusted-ca-file", caPath,
|
||||
}
|
||||
args = append(args, tlsPeerArgs...)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.isClientCRL {
|
||||
args = append(args, "--client-crl-file", crlPath, "--client-cert-auth")
|
||||
}
|
||||
|
||||
if len(cfg.cipherSuites) > 0 {
|
||||
args = append(args, "--cipher-suites", strings.Join(cfg.cipherSuites, ","))
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func (epc *etcdProcessCluster) EndpointsV2() []string {
|
||||
return epc.endpoints(func(ep etcdProcess) []string { return ep.EndpointsV2() })
|
||||
}
|
||||
|
||||
func (epc *etcdProcessCluster) EndpointsV3() []string {
|
||||
return epc.endpoints(func(ep etcdProcess) []string { return ep.EndpointsV3() })
|
||||
}
|
||||
|
||||
func (epc *etcdProcessCluster) endpoints(f func(ep etcdProcess) []string) (ret []string) {
|
||||
for _, p := range epc.procs {
|
||||
ret = append(ret, f(p)...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (epc *etcdProcessCluster) Start() error {
|
||||
return epc.start(func(ep etcdProcess) error { return ep.Start() })
|
||||
}
|
||||
|
||||
func (epc *etcdProcessCluster) Restart() error {
|
||||
return epc.start(func(ep etcdProcess) error { return ep.Restart() })
|
||||
}
|
||||
|
||||
func (epc *etcdProcessCluster) start(f func(ep etcdProcess) error) error {
|
||||
readyC := make(chan error, len(epc.procs))
|
||||
for i := range epc.procs {
|
||||
go func(n int) { readyC <- f(epc.procs[n]) }(i)
|
||||
}
|
||||
for range epc.procs {
|
||||
if err := <-readyC; err != nil {
|
||||
epc.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (epc *etcdProcessCluster) Stop() (err error) {
|
||||
for _, p := range epc.procs {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
if curErr := p.Stop(); curErr != nil {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%v; %v", err, curErr)
|
||||
} else {
|
||||
err = curErr
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (epc *etcdProcessCluster) Close() error {
|
||||
err := epc.Stop()
|
||||
for _, p := range epc.procs {
|
||||
// p is nil when newEtcdProcess fails in the middle
|
||||
// Close still gets called to clean up test data
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
if cerr := p.Close(); cerr != nil {
|
||||
err = cerr
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (epc *etcdProcessCluster) WithStopSignal(sig os.Signal) (ret os.Signal) {
|
||||
for _, p := range epc.procs {
|
||||
ret = p.WithStopSignal(sig)
|
||||
}
|
||||
return ret
|
||||
}
|
183
tests/e2e/ctl_v2_test.go
Normal file
183
tests/e2e/ctl_v2_test.go
Normal file
@ -0,0 +1,183 @@
|
||||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/pkg/fileutil"
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
)
|
||||
|
||||
func testCtlV2Set(t *testing.T, cfg *etcdProcessClusterConfig, quorum bool) {
|
||||
os.Setenv("ETCDCTL_API", "2")
|
||||
defer os.Unsetenv("ETCDCTL_API")
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
cfg.enableV2 = true
|
||||
epc := setupEtcdctlTest(t, cfg, quorum)
|
||||
defer func() {
|
||||
if errC := epc.Close(); errC != nil {
|
||||
t.Fatalf("error closing etcd processes (%v)", errC)
|
||||
}
|
||||
}()
|
||||
|
||||
key, value := "foo", "bar"
|
||||
|
||||
if err := etcdctlSet(epc, key, value); err != nil {
|
||||
t.Fatalf("failed set (%v)", err)
|
||||
}
|
||||
|
||||
if err := etcdctlGet(epc, key, value, quorum); err != nil {
|
||||
t.Fatalf("failed get (%v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func etcdctlPrefixArgs(clus *etcdProcessCluster) []string {
|
||||
endpoints := strings.Join(clus.EndpointsV2(), ",")
|
||||
cmdArgs := []string{ctlBinPath, "--endpoints", endpoints}
|
||||
if clus.cfg.clientTLS == clientTLS {
|
||||
cmdArgs = append(cmdArgs, "--ca-file", caPath, "--cert-file", certPath, "--key-file", privateKeyPath)
|
||||
}
|
||||
return cmdArgs
|
||||
}
|
||||
|
||||
func etcdctlClusterHealth(clus *etcdProcessCluster, val string) error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "cluster-health")
|
||||
return spawnWithExpect(cmdArgs, val)
|
||||
}
|
||||
|
||||
func etcdctlSet(clus *etcdProcessCluster, key, value string) error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "set", key, value)
|
||||
return spawnWithExpect(cmdArgs, value)
|
||||
}
|
||||
|
||||
func etcdctlMk(clus *etcdProcessCluster, key, value string, first bool) error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "mk", key, value)
|
||||
if first {
|
||||
return spawnWithExpect(cmdArgs, value)
|
||||
}
|
||||
return spawnWithExpect(cmdArgs, "Error: 105: Key already exists")
|
||||
}
|
||||
|
||||
func etcdctlGet(clus *etcdProcessCluster, key, value string, quorum bool) error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "get", key)
|
||||
if quorum {
|
||||
cmdArgs = append(cmdArgs, "--quorum")
|
||||
}
|
||||
return spawnWithExpect(cmdArgs, value)
|
||||
}
|
||||
|
||||
func etcdctlRm(clus *etcdProcessCluster, key, value string, first bool) error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "rm", key)
|
||||
if first {
|
||||
return spawnWithExpect(cmdArgs, "PrevNode.Value: "+value)
|
||||
}
|
||||
return spawnWithExpect(cmdArgs, "Error: 100: Key not found")
|
||||
}
|
||||
|
||||
func etcdctlLs(clus *etcdProcessCluster, key string, quorum bool) error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "ls")
|
||||
if quorum {
|
||||
cmdArgs = append(cmdArgs, "--quorum")
|
||||
}
|
||||
return spawnWithExpect(cmdArgs, key)
|
||||
}
|
||||
|
||||
func etcdctlWatch(clus *etcdProcessCluster, key, value string, noSync bool) <-chan error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "watch", "--after-index=1", key)
|
||||
if noSync {
|
||||
cmdArgs = append(cmdArgs, "--no-sync")
|
||||
}
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
errc <- spawnWithExpect(cmdArgs, value)
|
||||
}()
|
||||
return errc
|
||||
}
|
||||
|
||||
func etcdctlRoleAdd(clus *etcdProcessCluster, role string) error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "role", "add", role)
|
||||
return spawnWithExpect(cmdArgs, role)
|
||||
}
|
||||
|
||||
func etcdctlRoleGrant(clus *etcdProcessCluster, role string, perms ...string) error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "role", "grant")
|
||||
cmdArgs = append(cmdArgs, perms...)
|
||||
cmdArgs = append(cmdArgs, role)
|
||||
return spawnWithExpect(cmdArgs, role)
|
||||
}
|
||||
|
||||
func etcdctlRoleList(clus *etcdProcessCluster, expectedRole string) error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "role", "list")
|
||||
return spawnWithExpect(cmdArgs, expectedRole)
|
||||
}
|
||||
|
||||
func etcdctlUserAdd(clus *etcdProcessCluster, user, pass string) error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "user", "add", user+":"+pass)
|
||||
return spawnWithExpect(cmdArgs, "User "+user+" created")
|
||||
}
|
||||
|
||||
func etcdctlUserGrant(clus *etcdProcessCluster, user, role string) error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "user", "grant", "--roles", role, user)
|
||||
return spawnWithExpect(cmdArgs, "User "+user+" updated")
|
||||
}
|
||||
|
||||
func etcdctlUserGet(clus *etcdProcessCluster, user string) error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "user", "get", user)
|
||||
return spawnWithExpect(cmdArgs, "User: "+user)
|
||||
}
|
||||
|
||||
func etcdctlUserList(clus *etcdProcessCluster, expectedUser string) error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "user", "list")
|
||||
return spawnWithExpect(cmdArgs, expectedUser)
|
||||
}
|
||||
|
||||
func etcdctlAuthEnable(clus *etcdProcessCluster) error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "auth", "enable")
|
||||
return spawnWithExpect(cmdArgs, "Authentication Enabled")
|
||||
}
|
||||
|
||||
func etcdctlBackup(clus *etcdProcessCluster, dataDir, backupDir string, v3 bool) error {
|
||||
cmdArgs := append(etcdctlPrefixArgs(clus), "backup", "--data-dir", dataDir, "--backup-dir", backupDir)
|
||||
if v3 {
|
||||
cmdArgs = append(cmdArgs, "--with-v3")
|
||||
}
|
||||
proc, err := spawnCmd(cmdArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return proc.Close()
|
||||
}
|
||||
|
||||
func mustEtcdctl(t *testing.T) {
|
||||
if !fileutil.Exist(binDir + "/etcdctl") {
|
||||
t.Fatalf("could not find etcdctl binary")
|
||||
}
|
||||
}
|
||||
|
||||
func setupEtcdctlTest(t *testing.T, cfg *etcdProcessClusterConfig, quorum bool) *etcdProcessCluster {
|
||||
mustEtcdctl(t)
|
||||
if !quorum {
|
||||
cfg = configStandalone(*cfg)
|
||||
}
|
||||
epc, err := newEtcdProcessCluster(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("could not start etcd process cluster (%v)", err)
|
||||
}
|
||||
return epc
|
||||
}
|
154
tests/e2e/ctl_v3_member_test.go
Normal file
154
tests/e2e/ctl_v3_member_test.go
Normal file
@ -0,0 +1,154 @@
|
||||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
)
|
||||
|
||||
func TestCtlV3MemberList(t *testing.T) { testCtl(t, memberListTest) }
|
||||
func TestCtlV3MemberListNoTLS(t *testing.T) { testCtl(t, memberListTest, withCfg(configNoTLS)) }
|
||||
func TestCtlV3MemberListClientTLS(t *testing.T) { testCtl(t, memberListTest, withCfg(configClientTLS)) }
|
||||
func TestCtlV3MemberListClientAutoTLS(t *testing.T) {
|
||||
testCtl(t, memberListTest, withCfg(configClientAutoTLS))
|
||||
}
|
||||
func TestCtlV3MemberListPeerTLS(t *testing.T) { testCtl(t, memberListTest, withCfg(configPeerTLS)) }
|
||||
func TestCtlV3MemberRemove(t *testing.T) {
|
||||
testCtl(t, memberRemoveTest, withQuorum(), withNoStrictReconfig())
|
||||
}
|
||||
func TestCtlV3MemberRemoveNoTLS(t *testing.T) {
|
||||
testCtl(t, memberRemoveTest, withQuorum(), withNoStrictReconfig(), withCfg(configNoTLS))
|
||||
}
|
||||
func TestCtlV3MemberRemoveClientTLS(t *testing.T) {
|
||||
testCtl(t, memberRemoveTest, withQuorum(), withNoStrictReconfig(), withCfg(configClientTLS))
|
||||
}
|
||||
func TestCtlV3MemberRemoveClientAutoTLS(t *testing.T) {
|
||||
testCtl(t, memberRemoveTest, withQuorum(), withNoStrictReconfig(), withCfg(
|
||||
// default clusterSize is 1
|
||||
etcdProcessClusterConfig{
|
||||
clusterSize: 3,
|
||||
isClientAutoTLS: true,
|
||||
clientTLS: clientTLS,
|
||||
initialToken: "new",
|
||||
}))
|
||||
}
|
||||
func TestCtlV3MemberRemovePeerTLS(t *testing.T) {
|
||||
testCtl(t, memberRemoveTest, withQuorum(), withNoStrictReconfig(), withCfg(configPeerTLS))
|
||||
}
|
||||
func TestCtlV3MemberAdd(t *testing.T) { testCtl(t, memberAddTest) }
|
||||
func TestCtlV3MemberAddNoTLS(t *testing.T) { testCtl(t, memberAddTest, withCfg(configNoTLS)) }
|
||||
func TestCtlV3MemberAddClientTLS(t *testing.T) { testCtl(t, memberAddTest, withCfg(configClientTLS)) }
|
||||
func TestCtlV3MemberAddClientAutoTLS(t *testing.T) {
|
||||
testCtl(t, memberAddTest, withCfg(configClientAutoTLS))
|
||||
}
|
||||
func TestCtlV3MemberAddPeerTLS(t *testing.T) { testCtl(t, memberAddTest, withCfg(configPeerTLS)) }
|
||||
func TestCtlV3MemberUpdate(t *testing.T) { testCtl(t, memberUpdateTest) }
|
||||
func TestCtlV3MemberUpdateNoTLS(t *testing.T) { testCtl(t, memberUpdateTest, withCfg(configNoTLS)) }
|
||||
func TestCtlV3MemberUpdateClientTLS(t *testing.T) {
|
||||
testCtl(t, memberUpdateTest, withCfg(configClientTLS))
|
||||
}
|
||||
func TestCtlV3MemberUpdateClientAutoTLS(t *testing.T) {
|
||||
testCtl(t, memberUpdateTest, withCfg(configClientAutoTLS))
|
||||
}
|
||||
func TestCtlV3MemberUpdatePeerTLS(t *testing.T) { testCtl(t, memberUpdateTest, withCfg(configPeerTLS)) }
|
||||
|
||||
func memberListTest(cx ctlCtx) {
|
||||
if err := ctlV3MemberList(cx); err != nil {
|
||||
cx.t.Fatalf("memberListTest ctlV3MemberList error (%v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ctlV3MemberList(cx ctlCtx) error {
|
||||
cmdArgs := append(cx.PrefixArgs(), "member", "list")
|
||||
lines := make([]string, cx.cfg.clusterSize)
|
||||
for i := range lines {
|
||||
lines[i] = "started"
|
||||
}
|
||||
return spawnWithExpects(cmdArgs, lines...)
|
||||
}
|
||||
|
||||
func getMemberList(cx ctlCtx) (etcdserverpb.MemberListResponse, error) {
|
||||
cmdArgs := append(cx.PrefixArgs(), "--write-out", "json", "member", "list")
|
||||
|
||||
proc, err := spawnCmd(cmdArgs)
|
||||
if err != nil {
|
||||
return etcdserverpb.MemberListResponse{}, err
|
||||
}
|
||||
var txt string
|
||||
txt, err = proc.Expect("members")
|
||||
if err != nil {
|
||||
return etcdserverpb.MemberListResponse{}, err
|
||||
}
|
||||
if err = proc.Close(); err != nil {
|
||||
return etcdserverpb.MemberListResponse{}, err
|
||||
}
|
||||
|
||||
resp := etcdserverpb.MemberListResponse{}
|
||||
dec := json.NewDecoder(strings.NewReader(txt))
|
||||
if err := dec.Decode(&resp); err == io.EOF {
|
||||
return etcdserverpb.MemberListResponse{}, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func memberRemoveTest(cx ctlCtx) {
|
||||
ep, memIDToRemove, clusterID := cx.memberToRemove()
|
||||
if err := ctlV3MemberRemove(cx, ep, memIDToRemove, clusterID); err != nil {
|
||||
cx.t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ctlV3MemberRemove(cx ctlCtx, ep, memberID, clusterID string) error {
|
||||
cmdArgs := append(cx.prefixArgs([]string{ep}), "member", "remove", memberID)
|
||||
return spawnWithExpect(cmdArgs, fmt.Sprintf("%s removed from cluster %s", memberID, clusterID))
|
||||
}
|
||||
|
||||
func memberAddTest(cx ctlCtx) {
|
||||
if err := ctlV3MemberAdd(cx, fmt.Sprintf("http://localhost:%d", etcdProcessBasePort+11), false); err != nil {
|
||||
cx.t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ctlV3MemberAdd(cx ctlCtx, peerURL string, isLearner bool) error {
|
||||
cmdArgs := append(cx.PrefixArgs(), "member", "add", "newmember", fmt.Sprintf("--peer-urls=%s", peerURL))
|
||||
if isLearner {
|
||||
cmdArgs = append(cmdArgs, "--learner")
|
||||
}
|
||||
return spawnWithExpect(cmdArgs, " added to cluster ")
|
||||
}
|
||||
|
||||
func memberUpdateTest(cx ctlCtx) {
|
||||
mr, err := getMemberList(cx)
|
||||
if err != nil {
|
||||
cx.t.Fatal(err)
|
||||
}
|
||||
|
||||
peerURL := fmt.Sprintf("http://localhost:%d", etcdProcessBasePort+11)
|
||||
memberID := fmt.Sprintf("%x", mr.Members[0].ID)
|
||||
if err = ctlV3MemberUpdate(cx, memberID, peerURL); err != nil {
|
||||
cx.t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ctlV3MemberUpdate(cx ctlCtx, memberID, peerURL string) error {
|
||||
cmdArgs := append(cx.PrefixArgs(), "member", "update", memberID, fmt.Sprintf("--peer-urls=%s", peerURL))
|
||||
return spawnWithExpect(cmdArgs, " updated in cluster ")
|
||||
}
|
255
tests/e2e/ctl_v3_test.go
Normal file
255
tests/e2e/ctl_v3_test.go
Normal file
@ -0,0 +1,255 @@
|
||||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/pkg/flags"
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
"github.com/coreos/etcd/version"
|
||||
)
|
||||
|
||||
func TestCtlV3Version(t *testing.T) { testCtl(t, versionTest) }
|
||||
|
||||
func versionTest(cx ctlCtx) {
|
||||
if err := ctlV3Version(cx); err != nil {
|
||||
cx.t.Fatalf("versionTest ctlV3Version error (%v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ctlV3Version(cx ctlCtx) error {
|
||||
cmdArgs := append(cx.PrefixArgs(), "version")
|
||||
return spawnWithExpect(cmdArgs, version.Version)
|
||||
}
|
||||
|
||||
// TestCtlV3DialWithHTTPScheme ensures that client handles endpoints with HTTPS scheme.
|
||||
func TestCtlV3DialWithHTTPScheme(t *testing.T) {
|
||||
testCtl(t, dialWithSchemeTest, withCfg(configClientTLS))
|
||||
}
|
||||
|
||||
func dialWithSchemeTest(cx ctlCtx) {
|
||||
cmdArgs := append(cx.prefixArgs(cx.epc.EndpointsV3()), "put", "foo", "bar")
|
||||
if err := spawnWithExpect(cmdArgs, "OK"); err != nil {
|
||||
cx.t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
type ctlCtx struct {
|
||||
t *testing.T
|
||||
apiPrefix string
|
||||
cfg etcdProcessClusterConfig
|
||||
quotaBackendBytes int64
|
||||
corruptFunc func(string) error
|
||||
noStrictReconfig bool
|
||||
|
||||
epc *etcdProcessCluster
|
||||
|
||||
envMap map[string]struct{}
|
||||
|
||||
dialTimeout time.Duration
|
||||
|
||||
quorum bool // if true, set up 3-node cluster and linearizable read
|
||||
interactive bool
|
||||
|
||||
user string
|
||||
pass string
|
||||
|
||||
initialCorruptCheck bool
|
||||
|
||||
// for compaction
|
||||
compactPhysical bool
|
||||
}
|
||||
|
||||
type ctlOption func(*ctlCtx)
|
||||
|
||||
func (cx *ctlCtx) applyOpts(opts []ctlOption) {
|
||||
for _, opt := range opts {
|
||||
opt(cx)
|
||||
}
|
||||
cx.initialCorruptCheck = true
|
||||
}
|
||||
|
||||
func withCfg(cfg etcdProcessClusterConfig) ctlOption {
|
||||
return func(cx *ctlCtx) { cx.cfg = cfg }
|
||||
}
|
||||
|
||||
func withDialTimeout(timeout time.Duration) ctlOption {
|
||||
return func(cx *ctlCtx) { cx.dialTimeout = timeout }
|
||||
}
|
||||
|
||||
func withQuorum() ctlOption {
|
||||
return func(cx *ctlCtx) { cx.quorum = true }
|
||||
}
|
||||
|
||||
func withInteractive() ctlOption {
|
||||
return func(cx *ctlCtx) { cx.interactive = true }
|
||||
}
|
||||
|
||||
func withQuota(b int64) ctlOption {
|
||||
return func(cx *ctlCtx) { cx.quotaBackendBytes = b }
|
||||
}
|
||||
|
||||
func withCompactPhysical() ctlOption {
|
||||
return func(cx *ctlCtx) { cx.compactPhysical = true }
|
||||
}
|
||||
|
||||
func withInitialCorruptCheck() ctlOption {
|
||||
return func(cx *ctlCtx) { cx.initialCorruptCheck = true }
|
||||
}
|
||||
|
||||
func withCorruptFunc(f func(string) error) ctlOption {
|
||||
return func(cx *ctlCtx) { cx.corruptFunc = f }
|
||||
}
|
||||
|
||||
func withNoStrictReconfig() ctlOption {
|
||||
return func(cx *ctlCtx) { cx.noStrictReconfig = true }
|
||||
}
|
||||
|
||||
func withApiPrefix(p string) ctlOption {
|
||||
return func(cx *ctlCtx) { cx.apiPrefix = p }
|
||||
}
|
||||
|
||||
func withFlagByEnv() ctlOption {
|
||||
return func(cx *ctlCtx) { cx.envMap = make(map[string]struct{}) }
|
||||
}
|
||||
|
||||
func testCtl(t *testing.T, testFunc func(ctlCtx), opts ...ctlOption) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
ret := ctlCtx{
|
||||
t: t,
|
||||
cfg: configAutoTLS,
|
||||
dialTimeout: 7 * time.Second,
|
||||
}
|
||||
ret.applyOpts(opts)
|
||||
|
||||
mustEtcdctl(t)
|
||||
if !ret.quorum {
|
||||
ret.cfg = *configStandalone(ret.cfg)
|
||||
}
|
||||
if ret.quotaBackendBytes > 0 {
|
||||
ret.cfg.quotaBackendBytes = ret.quotaBackendBytes
|
||||
}
|
||||
ret.cfg.noStrictReconfig = ret.noStrictReconfig
|
||||
if ret.initialCorruptCheck {
|
||||
ret.cfg.initialCorruptCheck = ret.initialCorruptCheck
|
||||
}
|
||||
|
||||
epc, err := newEtcdProcessCluster(&ret.cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("could not start etcd process cluster (%v)", err)
|
||||
}
|
||||
ret.epc = epc
|
||||
|
||||
defer func() {
|
||||
if ret.envMap != nil {
|
||||
for k := range ret.envMap {
|
||||
os.Unsetenv(k)
|
||||
}
|
||||
}
|
||||
if errC := ret.epc.Close(); errC != nil {
|
||||
t.Fatalf("error closing etcd processes (%v)", errC)
|
||||
}
|
||||
}()
|
||||
|
||||
donec := make(chan struct{})
|
||||
go func() {
|
||||
defer close(donec)
|
||||
testFunc(ret)
|
||||
}()
|
||||
|
||||
timeout := 2*ret.dialTimeout + time.Second
|
||||
if ret.dialTimeout == 0 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
testutil.FatalStack(t, fmt.Sprintf("test timed out after %v", timeout))
|
||||
case <-donec:
|
||||
}
|
||||
}
|
||||
|
||||
func (cx *ctlCtx) prefixArgs(eps []string) []string {
|
||||
fmap := make(map[string]string)
|
||||
fmap["endpoints"] = strings.Join(eps, ",")
|
||||
fmap["dial-timeout"] = cx.dialTimeout.String()
|
||||
if cx.epc.cfg.clientTLS == clientTLS {
|
||||
if cx.epc.cfg.isClientAutoTLS {
|
||||
fmap["insecure-transport"] = "false"
|
||||
fmap["insecure-skip-tls-verify"] = "true"
|
||||
} else if cx.epc.cfg.isClientCRL {
|
||||
fmap["cacert"] = caPath
|
||||
fmap["cert"] = revokedCertPath
|
||||
fmap["key"] = revokedPrivateKeyPath
|
||||
} else {
|
||||
fmap["cacert"] = caPath
|
||||
fmap["cert"] = certPath
|
||||
fmap["key"] = privateKeyPath
|
||||
}
|
||||
}
|
||||
if cx.user != "" {
|
||||
fmap["user"] = cx.user + ":" + cx.pass
|
||||
}
|
||||
|
||||
useEnv := cx.envMap != nil
|
||||
|
||||
cmdArgs := []string{ctlBinPath + "3"}
|
||||
for k, v := range fmap {
|
||||
if useEnv {
|
||||
ek := flags.FlagToEnv("ETCDCTL", k)
|
||||
os.Setenv(ek, v)
|
||||
cx.envMap[ek] = struct{}{}
|
||||
} else {
|
||||
cmdArgs = append(cmdArgs, fmt.Sprintf("--%s=%s", k, v))
|
||||
}
|
||||
}
|
||||
return cmdArgs
|
||||
}
|
||||
|
||||
// PrefixArgs prefixes etcdctl command.
|
||||
// Make sure to unset environment variables after tests.
|
||||
func (cx *ctlCtx) PrefixArgs() []string {
|
||||
return cx.prefixArgs(cx.epc.EndpointsV3())
|
||||
}
|
||||
|
||||
func isGRPCTimedout(err error) bool {
|
||||
return strings.Contains(err.Error(), "grpc: timed out trying to connect")
|
||||
}
|
||||
|
||||
func (cx *ctlCtx) memberToRemove() (ep string, memberID string, clusterID string) {
|
||||
n1 := cx.cfg.clusterSize
|
||||
if n1 < 2 {
|
||||
cx.t.Fatalf("%d-node is too small to test 'member remove'", n1)
|
||||
}
|
||||
|
||||
resp, err := getMemberList(*cx)
|
||||
if err != nil {
|
||||
cx.t.Fatal(err)
|
||||
}
|
||||
if n1 != len(resp.Members) {
|
||||
cx.t.Fatalf("expected %d, got %d", n1, len(resp.Members))
|
||||
}
|
||||
|
||||
ep = resp.Members[0].ClientURLs[0]
|
||||
clusterID = fmt.Sprintf("%x", resp.Header.ClusterId)
|
||||
memberID = fmt.Sprintf("%x", resp.Members[1].ID)
|
||||
|
||||
return ep, memberID, clusterID
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
// Copyright 2018 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build cov
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCtlV3Watch(t *testing.T) { testCtl(t, watchTest) }
|
||||
func TestCtlV3WatchNoTLS(t *testing.T) { testCtl(t, watchTest, withCfg(configNoTLS)) }
|
||||
func TestCtlV3WatchClientTLS(t *testing.T) { testCtl(t, watchTest, withCfg(configClientTLS)) }
|
||||
func TestCtlV3WatchPeerTLS(t *testing.T) { testCtl(t, watchTest, withCfg(configPeerTLS)) }
|
||||
func TestCtlV3WatchTimeout(t *testing.T) { testCtl(t, watchTest, withDialTimeout(0)) }
|
||||
|
||||
func TestCtlV3WatchInteractive(t *testing.T) {
|
||||
testCtl(t, watchTest, withInteractive())
|
||||
}
|
||||
func TestCtlV3WatchInteractiveNoTLS(t *testing.T) {
|
||||
testCtl(t, watchTest, withInteractive(), withCfg(configNoTLS))
|
||||
}
|
||||
func TestCtlV3WatchInteractiveClientTLS(t *testing.T) {
|
||||
testCtl(t, watchTest, withInteractive(), withCfg(configClientTLS))
|
||||
}
|
||||
func TestCtlV3WatchInteractivePeerTLS(t *testing.T) {
|
||||
testCtl(t, watchTest, withInteractive(), withCfg(configPeerTLS))
|
||||
}
|
||||
|
||||
func watchTest(cx ctlCtx) {
|
||||
tests := []struct {
|
||||
puts []kv
|
||||
envKey string
|
||||
envRange string
|
||||
args []string
|
||||
|
||||
wkv []kvExec
|
||||
}{
|
||||
{ // watch 1 key
|
||||
puts: []kv{{"sample", "value"}},
|
||||
args: []string{"sample", "--rev", "1"},
|
||||
wkv: []kvExec{{key: "sample", val: "value"}},
|
||||
},
|
||||
{ // watch 1 key with env
|
||||
puts: []kv{{"sample", "value"}},
|
||||
envKey: "sample",
|
||||
args: []string{"--rev", "1"},
|
||||
wkv: []kvExec{{key: "sample", val: "value"}},
|
||||
},
|
||||
|
||||
// coverage tests get extra arguments:
|
||||
// ./bin/etcdctl_test -test.coverprofile=e2e.1525392462795198897.coverprofile -test.outputdir=../..
|
||||
// do not test watch exec commands
|
||||
|
||||
{ // watch 3 keys by prefix
|
||||
puts: []kv{{"key1", "val1"}, {"key2", "val2"}, {"key3", "val3"}},
|
||||
args: []string{"key", "--rev", "1", "--prefix"},
|
||||
wkv: []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}, {key: "key3", val: "val3"}},
|
||||
},
|
||||
{ // watch 3 keys by prefix, with env
|
||||
puts: []kv{{"key1", "val1"}, {"key2", "val2"}, {"key3", "val3"}},
|
||||
envKey: "key",
|
||||
args: []string{"--rev", "1", "--prefix"},
|
||||
wkv: []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}, {key: "key3", val: "val3"}},
|
||||
},
|
||||
{ // watch by revision
|
||||
puts: []kv{{"etcd", "revision_1"}, {"etcd", "revision_2"}, {"etcd", "revision_3"}},
|
||||
args: []string{"etcd", "--rev", "2"},
|
||||
wkv: []kvExec{{key: "etcd", val: "revision_2"}, {key: "etcd", val: "revision_3"}},
|
||||
},
|
||||
{ // watch 3 keys by range
|
||||
puts: []kv{{"key1", "val1"}, {"key3", "val3"}, {"key2", "val2"}},
|
||||
args: []string{"key", "key3", "--rev", "1"},
|
||||
wkv: []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}},
|
||||
},
|
||||
{ // watch 3 keys by range, with env
|
||||
puts: []kv{{"key1", "val1"}, {"key3", "val3"}, {"key2", "val2"}},
|
||||
envKey: "key",
|
||||
envRange: "key3",
|
||||
args: []string{"--rev", "1"},
|
||||
wkv: []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
donec := make(chan struct{})
|
||||
go func(i int, puts []kv) {
|
||||
for j := range puts {
|
||||
if err := ctlV3Put(cx, puts[j].key, puts[j].val, ""); err != nil {
|
||||
cx.t.Fatalf("watchTest #%d-%d: ctlV3Put error (%v)", i, j, err)
|
||||
}
|
||||
}
|
||||
close(donec)
|
||||
}(i, tt.puts)
|
||||
|
||||
unsetEnv := func() {}
|
||||
if tt.envKey != "" || tt.envRange != "" {
|
||||
if tt.envKey != "" {
|
||||
os.Setenv("ETCDCTL_WATCH_KEY", tt.envKey)
|
||||
unsetEnv = func() { os.Unsetenv("ETCDCTL_WATCH_KEY") }
|
||||
}
|
||||
if tt.envRange != "" {
|
||||
os.Setenv("ETCDCTL_WATCH_RANGE_END", tt.envRange)
|
||||
unsetEnv = func() { os.Unsetenv("ETCDCTL_WATCH_RANGE_END") }
|
||||
}
|
||||
if tt.envKey != "" && tt.envRange != "" {
|
||||
unsetEnv = func() {
|
||||
os.Unsetenv("ETCDCTL_WATCH_KEY")
|
||||
os.Unsetenv("ETCDCTL_WATCH_RANGE_END")
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := ctlV3Watch(cx, tt.args, tt.wkv...); err != nil {
|
||||
if cx.dialTimeout > 0 && !isGRPCTimedout(err) {
|
||||
cx.t.Errorf("watchTest #%d: ctlV3Watch error (%v)", i, err)
|
||||
}
|
||||
}
|
||||
unsetEnv()
|
||||
<-donec
|
||||
}
|
||||
}
|
149
tests/e2e/ctl_v3_watch_test.go
Normal file
149
tests/e2e/ctl_v3_watch_test.go
Normal file
@ -0,0 +1,149 @@
|
||||
// Copyright 2018 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type kvExec struct {
|
||||
key, val string
|
||||
execOutput string
|
||||
}
|
||||
|
||||
func setupWatchArgs(cx ctlCtx, args []string) []string {
|
||||
cmdArgs := append(cx.PrefixArgs(), "watch")
|
||||
if cx.interactive {
|
||||
cmdArgs = append(cmdArgs, "--interactive")
|
||||
} else {
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
}
|
||||
|
||||
return cmdArgs
|
||||
}
|
||||
|
||||
func ctlV3Watch(cx ctlCtx, args []string, kvs ...kvExec) error {
|
||||
cmdArgs := setupWatchArgs(cx, args)
|
||||
|
||||
proc, err := spawnCmd(cmdArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cx.interactive {
|
||||
wl := strings.Join(append([]string{"watch"}, args...), " ") + "\r"
|
||||
if err = proc.Send(wl); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, elem := range kvs {
|
||||
if _, err = proc.Expect(elem.key); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = proc.Expect(elem.val); err != nil {
|
||||
return err
|
||||
}
|
||||
if elem.execOutput != "" {
|
||||
if _, err = proc.Expect(elem.execOutput); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return proc.Stop()
|
||||
}
|
||||
|
||||
func ctlV3WatchFailPerm(cx ctlCtx, args []string) error {
|
||||
cmdArgs := setupWatchArgs(cx, args)
|
||||
|
||||
proc, err := spawnCmd(cmdArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cx.interactive {
|
||||
wl := strings.Join(append([]string{"watch"}, args...), " ") + "\r"
|
||||
if err = proc.Send(wl); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mitake): after printing accurate error message that includes
|
||||
// "permission denied", the above string argument of proc.Expect()
|
||||
// should be updated.
|
||||
_, err = proc.Expect("watch is canceled by the server")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return proc.Close()
|
||||
}
|
||||
|
||||
func ctlV3Put(cx ctlCtx, key, value, leaseID string, flags ...string) error {
|
||||
skipValue := false
|
||||
skipLease := false
|
||||
for _, f := range flags {
|
||||
if f == "--ignore-value" {
|
||||
skipValue = true
|
||||
}
|
||||
if f == "--ignore-lease" {
|
||||
skipLease = true
|
||||
}
|
||||
}
|
||||
cmdArgs := append(cx.PrefixArgs(), "put", key)
|
||||
if !skipValue {
|
||||
cmdArgs = append(cmdArgs, value)
|
||||
}
|
||||
if leaseID != "" && !skipLease {
|
||||
cmdArgs = append(cmdArgs, "--lease", leaseID)
|
||||
}
|
||||
if len(flags) != 0 {
|
||||
cmdArgs = append(cmdArgs, flags...)
|
||||
}
|
||||
return spawnWithExpect(cmdArgs, "OK")
|
||||
}
|
||||
|
||||
type kv struct {
|
||||
key, val string
|
||||
}
|
||||
|
||||
func ctlV3Get(cx ctlCtx, args []string, kvs ...kv) error {
|
||||
cmdArgs := append(cx.PrefixArgs(), "get")
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
if !cx.quorum {
|
||||
cmdArgs = append(cmdArgs, "--consistency", "s")
|
||||
}
|
||||
var lines []string
|
||||
for _, elem := range kvs {
|
||||
lines = append(lines, elem.key, elem.val)
|
||||
}
|
||||
return spawnWithExpects(cmdArgs, lines...)
|
||||
}
|
||||
|
||||
// ctlV3GetWithErr runs "get" command expecting no output but error
|
||||
func ctlV3GetWithErr(cx ctlCtx, args []string, errs []string) error {
|
||||
cmdArgs := append(cx.PrefixArgs(), "get")
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
if !cx.quorum {
|
||||
cmdArgs = append(cmdArgs, "--consistency", "s")
|
||||
}
|
||||
return spawnWithExpects(cmdArgs, errs...)
|
||||
}
|
||||
|
||||
func ctlV3Del(cx ctlCtx, args []string, num int) error {
|
||||
cmdArgs := append(cx.PrefixArgs(), "del")
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
return spawnWithExpects(cmdArgs, fmt.Sprintf("%d", num))
|
||||
}
|
145
tests/e2e/etcd_process.go
Normal file
145
tests/e2e/etcd_process.go
Normal file
@ -0,0 +1,145 @@
|
||||
// Copyright 2017 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/coreos/etcd/pkg/expect"
|
||||
"github.com/coreos/etcd/pkg/fileutil"
|
||||
)
|
||||
|
||||
var (
|
||||
etcdServerReadyLines = []string{"enabled capabilities for version", "published"}
|
||||
binPath string
|
||||
ctlBinPath string
|
||||
)
|
||||
|
||||
// etcdProcess is a process that serves etcd requests.
|
||||
type etcdProcess interface {
|
||||
EndpointsV2() []string
|
||||
EndpointsV3() []string
|
||||
EndpointsMetrics() []string
|
||||
|
||||
Start() error
|
||||
Restart() error
|
||||
Stop() error
|
||||
Close() error
|
||||
WithStopSignal(sig os.Signal) os.Signal
|
||||
Config() *etcdServerProcessConfig
|
||||
}
|
||||
|
||||
type etcdServerProcess struct {
|
||||
cfg *etcdServerProcessConfig
|
||||
proc *expect.ExpectProcess
|
||||
donec chan struct{} // closed when Interact() terminates
|
||||
}
|
||||
|
||||
type etcdServerProcessConfig struct {
|
||||
execPath string
|
||||
args []string
|
||||
tlsArgs []string
|
||||
|
||||
dataDirPath string
|
||||
keepDataDir bool
|
||||
|
||||
name string
|
||||
|
||||
purl url.URL
|
||||
|
||||
acurl string
|
||||
murl string
|
||||
|
||||
initialToken string
|
||||
initialCluster string
|
||||
}
|
||||
|
||||
func newEtcdServerProcess(cfg *etcdServerProcessConfig) (*etcdServerProcess, error) {
|
||||
if !fileutil.Exist(cfg.execPath) {
|
||||
return nil, fmt.Errorf("could not find etcd binary")
|
||||
}
|
||||
if !cfg.keepDataDir {
|
||||
if err := os.RemoveAll(cfg.dataDirPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &etcdServerProcess{cfg: cfg, donec: make(chan struct{})}, nil
|
||||
}
|
||||
|
||||
func (ep *etcdServerProcess) EndpointsV2() []string { return []string{ep.cfg.acurl} }
|
||||
func (ep *etcdServerProcess) EndpointsV3() []string { return ep.EndpointsV2() }
|
||||
func (ep *etcdServerProcess) EndpointsMetrics() []string { return []string{ep.cfg.murl} }
|
||||
|
||||
func (ep *etcdServerProcess) Start() error {
|
||||
if ep.proc != nil {
|
||||
panic("already started")
|
||||
}
|
||||
proc, err := spawnCmd(append([]string{ep.cfg.execPath}, ep.cfg.args...))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ep.proc = proc
|
||||
return ep.waitReady()
|
||||
}
|
||||
|
||||
func (ep *etcdServerProcess) Restart() error {
|
||||
if err := ep.Stop(); err != nil {
|
||||
return err
|
||||
}
|
||||
ep.donec = make(chan struct{})
|
||||
return ep.Start()
|
||||
}
|
||||
|
||||
func (ep *etcdServerProcess) Stop() (err error) {
|
||||
if ep == nil || ep.proc == nil {
|
||||
return nil
|
||||
}
|
||||
err = ep.proc.Stop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ep.proc = nil
|
||||
<-ep.donec
|
||||
ep.donec = make(chan struct{})
|
||||
if ep.cfg.purl.Scheme == "unix" || ep.cfg.purl.Scheme == "unixs" {
|
||||
err = os.Remove(ep.cfg.purl.Host + ep.cfg.purl.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *etcdServerProcess) Close() error {
|
||||
if err := ep.Stop(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.RemoveAll(ep.cfg.dataDirPath)
|
||||
}
|
||||
|
||||
func (ep *etcdServerProcess) WithStopSignal(sig os.Signal) os.Signal {
|
||||
ret := ep.proc.StopSignal
|
||||
ep.proc.StopSignal = sig
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ep *etcdServerProcess) waitReady() error {
|
||||
defer close(ep.donec)
|
||||
return waitReadyExpectProc(ep.proc, etcdServerReadyLines)
|
||||
}
|
||||
|
||||
func (ep *etcdServerProcess) Config() *etcdServerProcessConfig { return ep.cfg }
|
33
tests/e2e/etcd_spawn_nocov.go
Normal file
33
tests/e2e/etcd_spawn_nocov.go
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright 2017 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !cov
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/coreos/etcd/pkg/expect"
|
||||
)
|
||||
|
||||
const noOutputLineCount = 0 // regular binaries emit no extra lines
|
||||
|
||||
func spawnCmd(args []string) (*expect.ExpectProcess, error) {
|
||||
if args[0] == ctlBinPath+"3" {
|
||||
env := append(os.Environ(), "ETCDCTL_API=3")
|
||||
return expect.NewExpectWithEnv(ctlBinPath, args[1:], env)
|
||||
}
|
||||
return expect.NewExpect(args[0], args[1:]...)
|
||||
}
|
63
tests/e2e/main_test.go
Normal file
63
tests/e2e/main_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
)
|
||||
|
||||
var (
|
||||
binDir string
|
||||
certDir string
|
||||
|
||||
certPath string
|
||||
privateKeyPath string
|
||||
caPath string
|
||||
|
||||
certPath2 string
|
||||
privateKeyPath2 string
|
||||
|
||||
certPath3 string
|
||||
privateKeyPath3 string
|
||||
|
||||
crlPath string
|
||||
revokedCertPath string
|
||||
revokedPrivateKeyPath string
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Setenv("ETCD_UNSUPPORTED_ARCH", runtime.GOARCH)
|
||||
os.Unsetenv("ETCDCTL_API")
|
||||
|
||||
flag.StringVar(&binDir, "bin-dir", "../../bin", "The directory for store etcd and etcdctl binaries.")
|
||||
flag.StringVar(&certDir, "cert-dir", "../../integration/fixtures", "The directory for store certificate files.")
|
||||
flag.Parse()
|
||||
|
||||
binPath = binDir + "/etcd"
|
||||
ctlBinPath = binDir + "/etcdctl"
|
||||
certPath = certDir + "/server.crt"
|
||||
privateKeyPath = certDir + "/server.key.insecure"
|
||||
caPath = certDir + "/ca.crt"
|
||||
revokedCertPath = certDir + "/server-revoked.crt"
|
||||
revokedPrivateKeyPath = certDir + "/server-revoked.key.insecure"
|
||||
crlPath = certDir + "/revoke.crl"
|
||||
|
||||
certPath2 = certDir + "/server2.crt"
|
||||
privateKeyPath2 = certDir + "/server2.key.insecure"
|
||||
|
||||
certPath3 = certDir + "/server3.crt"
|
||||
privateKeyPath3 = certDir + "/server3.key.insecure"
|
||||
|
||||
v := m.Run()
|
||||
if v == 0 && testutil.CheckLeakedGoroutine() {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(v)
|
||||
}
|
110
tests/e2e/util.go
Normal file
110
tests/e2e/util.go
Normal file
@ -0,0 +1,110 @@
|
||||
// Copyright 2017 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/pkg/expect"
|
||||
)
|
||||
|
||||
func waitReadyExpectProc(exproc *expect.ExpectProcess, readyStrs []string) error {
|
||||
c := 0
|
||||
matchSet := func(l string) bool {
|
||||
for _, s := range readyStrs {
|
||||
if strings.Contains(l, s) {
|
||||
c++
|
||||
break
|
||||
}
|
||||
}
|
||||
return c == len(readyStrs)
|
||||
}
|
||||
_, err := exproc.ExpectFunc(matchSet)
|
||||
return err
|
||||
}
|
||||
|
||||
func spawnWithExpect(args []string, expected string) error {
|
||||
return spawnWithExpects(args, []string{expected}...)
|
||||
}
|
||||
|
||||
func spawnWithExpects(args []string, xs ...string) error {
|
||||
_, err := spawnWithExpectLines(args, xs...)
|
||||
return err
|
||||
}
|
||||
|
||||
func spawnWithExpectLines(args []string, xs ...string) ([]string, error) {
|
||||
proc, err := spawnCmd(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// process until either stdout or stderr contains
|
||||
// the expected string
|
||||
var (
|
||||
lines []string
|
||||
lineFunc = func(txt string) bool { return true }
|
||||
)
|
||||
for _, txt := range xs {
|
||||
for {
|
||||
l, lerr := proc.ExpectFunc(lineFunc)
|
||||
if lerr != nil {
|
||||
proc.Close()
|
||||
return nil, fmt.Errorf("%v (expected %q, got %q)", lerr, txt, lines)
|
||||
}
|
||||
lines = append(lines, l)
|
||||
if strings.Contains(l, txt) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
perr := proc.Close()
|
||||
if len(xs) == 0 && proc.LineCount() != noOutputLineCount { // expect no output
|
||||
return nil, fmt.Errorf("unexpected output (got lines %q, line count %d)", lines, proc.LineCount())
|
||||
}
|
||||
return lines, perr
|
||||
}
|
||||
|
||||
func randomLeaseID() int64 {
|
||||
return rand.New(rand.NewSource(time.Now().UnixNano())).Int63()
|
||||
}
|
||||
|
||||
func dataMarshal(data interface{}) (d string, e error) {
|
||||
m, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(m), nil
|
||||
}
|
||||
|
||||
func closeWithTimeout(p *expect.ExpectProcess, d time.Duration) error {
|
||||
errc := make(chan error, 1)
|
||||
go func() { errc <- p.Close() }()
|
||||
select {
|
||||
case err := <-errc:
|
||||
return err
|
||||
case <-time.After(d):
|
||||
p.Stop()
|
||||
// retry close after stopping to collect SIGQUIT data, if any
|
||||
closeWithTimeout(p, time.Second)
|
||||
}
|
||||
return fmt.Errorf("took longer than %v to Close process %+v", d, p)
|
||||
}
|
||||
|
||||
func toTLS(s string) string {
|
||||
return strings.Replace(s, "http://", "https://", 1)
|
||||
}
|
19
tests/e2e/v2_test.go
Normal file
19
tests/e2e/v2_test.go
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright 2017 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !v2v3
|
||||
|
||||
package e2e
|
||||
|
||||
func addV2Args(args []string) []string { return args }
|
Loading…
x
Reference in New Issue
Block a user