Browse Source

etcd: Configuration file for etcd server.

Added a new command line option to etcd server to read in a YAML
based configuration file. I've also added an example configuration
file with comments and a set of test cases.
Ajit Yagaty 9 years ago
parent
commit
8bc5ab9f8d
5 changed files with 707 additions and 184 deletions
  1. 207 68
      etcdmain/config.go
  2. 311 65
      etcdmain/config_test.go
  3. 135 0
      etcdmain/etcd.conf.sample.yml
  4. 51 51
      etcdmain/etcd.go
  5. 3 0
      etcdmain/help.go

+ 207 - 68
etcdmain/config.go

@@ -19,6 +19,7 @@ package etcdmain
 import (
 	"flag"
 	"fmt"
+	"io/ioutil"
 	"net/url"
 	"os"
 	"runtime"
@@ -28,7 +29,9 @@ import (
 	"github.com/coreos/etcd/pkg/cors"
 	"github.com/coreos/etcd/pkg/flags"
 	"github.com/coreos/etcd/pkg/transport"
+	"github.com/coreos/etcd/pkg/types"
 	"github.com/coreos/etcd/version"
+	"github.com/ghodss/yaml"
 )
 
 const (
@@ -44,6 +47,9 @@ const (
 
 	defaultName                     = "default"
 	defaultInitialAdvertisePeerURLs = "http://localhost:2380,http://localhost:7001"
+	defaultAdvertiseClientURLs      = "http://localhost:2379,http://localhost:4001"
+	defaultListenPeerURLs           = "http://localhost:2380,http://localhost:7001"
+	defaultListenClientURLs         = "http://localhost:2379,http://localhost:4001"
 
 	// maxElectionMs specifies the maximum value of election timeout.
 	// More details are listed in ../Documentation/tuning.md#time-parameters.
@@ -77,49 +83,61 @@ type config struct {
 
 	// member
 	corsInfo       *cors.CORSInfo
-	dir            string
-	walDir         string
 	lpurls, lcurls []url.URL
-	maxSnapFiles   uint
-	maxWalFiles    uint
-	name           string
-	snapCount      uint64
+	Dir            string `json:"data-dir"`
+	WalDir         string `json:"wal-dir"`
+	MaxSnapFiles   uint   `json:"max-snapshots"`
+	MaxWalFiles    uint   `json:"max-wals"`
+	Name           string `json:"name"`
+	SnapCount      uint64 `json:"snapshot-count"`
+	LPUrlsCfgFile  string `json:"listen-peer-urls"`
+	LCUrlsCfgFile  string `json:"listen-client-urls"`
+	CorsCfgFile    string `json:"cors"`
+
 	// TickMs is the number of milliseconds between heartbeat ticks.
 	// TODO: decouple tickMs and heartbeat tick (current heartbeat tick = 1).
 	// make ticks a cluster wide configuration.
-	TickMs            uint
-	ElectionMs        uint
-	quotaBackendBytes int64
+	TickMs            uint  `json:"heartbeat-interval"`
+	ElectionMs        uint  `json:"election-timeout"`
+	QuotaBackendBytes int64 `json:"quota-backend-bytes"`
 
 	// clustering
 	apurls, acurls      []url.URL
 	clusterState        *flags.StringsFlag
-	dnsCluster          string
-	dproxy              string
-	durl                string
+	DnsCluster          string `json:"discovery-srv"`
+	Dproxy              string `json:"discovery-proxy"`
+	Durl                string `json:"discovery"`
 	fallback            *flags.StringsFlag
-	initialCluster      string
-	initialClusterToken string
-	strictReconfigCheck bool
+	InitialCluster      string `json:"initial-cluster"`
+	InitialClusterToken string `json:"initial-cluster-token"`
+	StrictReconfigCheck bool   `json:"strict-reconfig-check"`
+	ApurlsCfgFile       string `json:"initial-advertise-peer-urls"`
+	AcurlsCfgFile       string `json:"advertise-client-urls"`
+	ClusterStateCfgFile string `json:"initial-cluster-state"`
+	FallbackCfgFile     string `json:"discovery-fallback"`
 
 	// proxy
 	proxy                  *flags.StringsFlag
-	proxyFailureWaitMs     uint
-	proxyRefreshIntervalMs uint
-	proxyDialTimeoutMs     uint
-	proxyWriteTimeoutMs    uint
-	proxyReadTimeoutMs     uint
+	ProxyFailureWaitMs     uint   `json:"proxy-failure-wait"`
+	ProxyRefreshIntervalMs uint   `json:"proxy-refresh-interval"`
+	ProxyDialTimeoutMs     uint   `json:"proxy-dial-timeout"`
+	ProxyWriteTimeoutMs    uint   `json:"proxy-write-timeout"`
+	ProxyReadTimeoutMs     uint   `json:"proxy-read-timeout"`
+	ProxyCfgFile           string `json:"proxy"`
 
 	// security
 	clientTLSInfo, peerTLSInfo transport.TLSInfo
-	clientAutoTLS, peerAutoTLS bool
+	ClientAutoTLS              bool
+	PeerAutoTLS                bool
+	ClientSecurityCfgFile      securityConfig `json:"client-transport-security"`
+	PeerSecurityCfgFile        securityConfig `json:"peer-transport-security"`
 
-	// logging
-	debug        bool
-	logPkgLevels string
+	// Debug logging
+	Debug        bool   `json:"debug"`
+	LogPkgLevels string `json:"log-package-levels"`
 
-	// unsafe
-	forceNewCluster bool
+	// ForceNewCluster is unsafe
+	ForceNewCluster bool `json:"force-new-cluster"`
 
 	printVersion bool
 
@@ -127,9 +145,20 @@ type config struct {
 
 	enablePprof bool
 
+	configFile string
+
 	ignored []string
 }
 
+type securityConfig struct {
+	CAFile        string `json:"ca-file"`
+	CertFile      string `json:"cert-file"`
+	KeyFile       string `json:"key-file"`
+	CertAuth      bool   `json:"client-cert-auth"`
+	TrustedCAFile string `json:"trusted-ca-file"`
+	AutoTLS       bool   `json:"auto-tls"`
+}
+
 func NewConfig() *config {
 	cfg := &config{
 		corsInfo: &cors.CORSInfo{},
@@ -155,39 +184,41 @@ func NewConfig() *config {
 		fmt.Println(usageline)
 	}
 
+	fs.StringVar(&cfg.configFile, "config-file", "", "Path to the server configuration file")
+
 	// member
 	fs.Var(cfg.corsInfo, "cors", "Comma-separated white list of origins for CORS (cross-origin resource sharing).")
-	fs.StringVar(&cfg.dir, "data-dir", "", "Path to the data directory.")
-	fs.StringVar(&cfg.walDir, "wal-dir", "", "Path to the dedicated wal directory.")
-	fs.Var(flags.NewURLsValue("http://localhost:2380,http://localhost:7001"), "listen-peer-urls", "List of URLs to listen on for peer traffic.")
-	fs.Var(flags.NewURLsValue("http://localhost:2379,http://localhost:4001"), "listen-client-urls", "List of URLs to listen on for client traffic.")
-	fs.UintVar(&cfg.maxSnapFiles, "max-snapshots", defaultMaxSnapshots, "Maximum number of snapshot files to retain (0 is unlimited).")
-	fs.UintVar(&cfg.maxWalFiles, "max-wals", defaultMaxWALs, "Maximum number of wal files to retain (0 is unlimited).")
-	fs.StringVar(&cfg.name, "name", defaultName, "Human-readable name for this member.")
-	fs.Uint64Var(&cfg.snapCount, "snapshot-count", etcdserver.DefaultSnapCount, "Number of committed transactions to trigger a snapshot to disk.")
+	fs.StringVar(&cfg.Dir, "data-dir", "", "Path to the data directory.")
+	fs.StringVar(&cfg.WalDir, "wal-dir", "", "Path to the dedicated wal directory.")
+	fs.Var(flags.NewURLsValue(defaultListenPeerURLs), "listen-peer-urls", "List of URLs to listen on for peer traffic.")
+	fs.Var(flags.NewURLsValue(defaultListenClientURLs), "listen-client-urls", "List of URLs to listen on for client traffic.")
+	fs.UintVar(&cfg.MaxSnapFiles, "max-snapshots", defaultMaxSnapshots, "Maximum number of snapshot files to retain (0 is unlimited).")
+	fs.UintVar(&cfg.MaxWalFiles, "max-wals", defaultMaxWALs, "Maximum number of wal files to retain (0 is unlimited).")
+	fs.StringVar(&cfg.Name, "name", defaultName, "Human-readable name for this member.")
+	fs.Uint64Var(&cfg.SnapCount, "snapshot-count", etcdserver.DefaultSnapCount, "Number of committed transactions to trigger a snapshot to disk.")
 	fs.UintVar(&cfg.TickMs, "heartbeat-interval", 100, "Time (in milliseconds) of a heartbeat interval.")
 	fs.UintVar(&cfg.ElectionMs, "election-timeout", 1000, "Time (in milliseconds) for an election to timeout.")
-	fs.Int64Var(&cfg.quotaBackendBytes, "quota-backend-bytes", 0, "Raise alarms when backend size exceeds the given quota. 0 means use the default quota.")
+	fs.Int64Var(&cfg.QuotaBackendBytes, "quota-backend-bytes", 0, "Raise alarms when backend size exceeds the given quota. 0 means use the default quota.")
 
 	// clustering
 	fs.Var(flags.NewURLsValue(defaultInitialAdvertisePeerURLs), "initial-advertise-peer-urls", "List of this member's peer URLs to advertise to the rest of the cluster.")
-	fs.Var(flags.NewURLsValue("http://localhost:2379,http://localhost:4001"), "advertise-client-urls", "List of this member's client URLs to advertise to the public.")
-	fs.StringVar(&cfg.durl, "discovery", "", "Discovery URL used to bootstrap the cluster.")
+	fs.Var(flags.NewURLsValue(defaultAdvertiseClientURLs), "advertise-client-urls", "List of this member's client URLs to advertise to the public.")
+	fs.StringVar(&cfg.Durl, "discovery", "", "Discovery URL used to bootstrap the cluster.")
 	fs.Var(cfg.fallback, "discovery-fallback", fmt.Sprintf("Valid values include %s", strings.Join(cfg.fallback.Values, ", ")))
 	if err := cfg.fallback.Set(fallbackFlagProxy); err != nil {
 		// Should never happen.
 		plog.Panicf("unexpected error setting up discovery-fallback flag: %v", err)
 	}
-	fs.StringVar(&cfg.dproxy, "discovery-proxy", "", "HTTP proxy to use for traffic to discovery service.")
-	fs.StringVar(&cfg.dnsCluster, "discovery-srv", "", "DNS domain used to bootstrap initial cluster.")
-	fs.StringVar(&cfg.initialCluster, "initial-cluster", initialClusterFromName(defaultName), "Initial cluster configuration for bootstrapping.")
-	fs.StringVar(&cfg.initialClusterToken, "initial-cluster-token", "etcd-cluster", "Initial cluster token for the etcd cluster during bootstrap.")
+	fs.StringVar(&cfg.Dproxy, "discovery-proxy", "", "HTTP proxy to use for traffic to discovery service.")
+	fs.StringVar(&cfg.DnsCluster, "discovery-srv", "", "DNS domain used to bootstrap initial cluster.")
+	fs.StringVar(&cfg.InitialCluster, "initial-cluster", initialClusterFromName(defaultName), "Initial cluster configuration for bootstrapping.")
+	fs.StringVar(&cfg.InitialClusterToken, "initial-cluster-token", "etcd-cluster", "Initial cluster token for the etcd cluster during bootstrap.")
 	fs.Var(cfg.clusterState, "initial-cluster-state", "Initial cluster state ('new' or 'existing').")
 	if err := cfg.clusterState.Set(clusterStateFlagNew); err != nil {
 		// Should never happen.
 		plog.Panicf("unexpected error setting up clusterStateFlag: %v", err)
 	}
-	fs.BoolVar(&cfg.strictReconfigCheck, "strict-reconfig-check", false, "Reject reconfiguration requests that would cause quorum loss.")
+	fs.BoolVar(&cfg.StrictReconfigCheck, "strict-reconfig-check", false, "Reject reconfiguration requests that would cause quorum loss.")
 
 	// proxy
 	fs.Var(cfg.proxy, "proxy", fmt.Sprintf("Valid values include %s", strings.Join(cfg.proxy.Values, ", ")))
@@ -195,11 +226,11 @@ func NewConfig() *config {
 		// Should never happen.
 		plog.Panicf("unexpected error setting up proxyFlag: %v", err)
 	}
-	fs.UintVar(&cfg.proxyFailureWaitMs, "proxy-failure-wait", 5000, "Time (in milliseconds) an endpoint will be held in a failed state.")
-	fs.UintVar(&cfg.proxyRefreshIntervalMs, "proxy-refresh-interval", 30000, "Time (in milliseconds) of the endpoints refresh interval.")
-	fs.UintVar(&cfg.proxyDialTimeoutMs, "proxy-dial-timeout", 1000, "Time (in milliseconds) for a dial to timeout.")
-	fs.UintVar(&cfg.proxyWriteTimeoutMs, "proxy-write-timeout", 5000, "Time (in milliseconds) for a write to timeout.")
-	fs.UintVar(&cfg.proxyReadTimeoutMs, "proxy-read-timeout", 0, "Time (in milliseconds) for a read to timeout.")
+	fs.UintVar(&cfg.ProxyFailureWaitMs, "proxy-failure-wait", 5000, "Time (in milliseconds) an endpoint will be held in a failed state.")
+	fs.UintVar(&cfg.ProxyRefreshIntervalMs, "proxy-refresh-interval", 30000, "Time (in milliseconds) of the endpoints refresh interval.")
+	fs.UintVar(&cfg.ProxyDialTimeoutMs, "proxy-dial-timeout", 1000, "Time (in milliseconds) for a dial to timeout.")
+	fs.UintVar(&cfg.ProxyWriteTimeoutMs, "proxy-write-timeout", 5000, "Time (in milliseconds) for a write to timeout.")
+	fs.UintVar(&cfg.ProxyReadTimeoutMs, "proxy-read-timeout", 0, "Time (in milliseconds) for a read to timeout.")
 
 	// security
 	fs.StringVar(&cfg.clientTLSInfo.CAFile, "ca-file", "", "DEPRECATED: Path to the client server TLS CA file.")
@@ -207,20 +238,20 @@ func NewConfig() *config {
 	fs.StringVar(&cfg.clientTLSInfo.KeyFile, "key-file", "", "Path to the client server TLS key file.")
 	fs.BoolVar(&cfg.clientTLSInfo.ClientCertAuth, "client-cert-auth", false, "Enable client cert authentication.")
 	fs.StringVar(&cfg.clientTLSInfo.TrustedCAFile, "trusted-ca-file", "", "Path to the client server TLS trusted CA key file.")
-	fs.BoolVar(&cfg.clientAutoTLS, "auto-tls", false, "Client TLS using generated certificates")
+	fs.BoolVar(&cfg.ClientAutoTLS, "auto-tls", false, "Client TLS using generated certificates")
 	fs.StringVar(&cfg.peerTLSInfo.CAFile, "peer-ca-file", "", "DEPRECATED: Path to the peer server TLS CA file.")
 	fs.StringVar(&cfg.peerTLSInfo.CertFile, "peer-cert-file", "", "Path to the peer server TLS cert file.")
 	fs.StringVar(&cfg.peerTLSInfo.KeyFile, "peer-key-file", "", "Path to the peer server TLS key file.")
 	fs.BoolVar(&cfg.peerTLSInfo.ClientCertAuth, "peer-client-cert-auth", false, "Enable peer client cert authentication.")
 	fs.StringVar(&cfg.peerTLSInfo.TrustedCAFile, "peer-trusted-ca-file", "", "Path to the peer server TLS trusted CA file.")
-	fs.BoolVar(&cfg.peerAutoTLS, "peer-auto-tls", false, "Peer TLS using generated certificates")
+	fs.BoolVar(&cfg.PeerAutoTLS, "peer-auto-tls", false, "Peer TLS using generated certificates")
 
 	// logging
-	fs.BoolVar(&cfg.debug, "debug", false, "Enable debug-level logging for etcd.")
-	fs.StringVar(&cfg.logPkgLevels, "log-package-levels", "", "Specify a particular log level for each etcd package (eg: 'etcdmain=CRITICAL,etcdserver=DEBUG').")
+	fs.BoolVar(&cfg.Debug, "debug", false, "Enable debug-level logging for etcd.")
+	fs.StringVar(&cfg.LogPkgLevels, "log-package-levels", "", "Specify a particular log level for each etcd package (eg: 'etcdmain=CRITICAL,etcdserver=DEBUG').")
 
 	// unsafe
-	fs.BoolVar(&cfg.forceNewCluster, "force-new-cluster", false, "Force to create a new one member cluster.")
+	fs.BoolVar(&cfg.ForceNewCluster, "force-new-cluster", false, "Force to create a new one member cluster.")
 
 	// version
 	fs.BoolVar(&cfg.printVersion, "version", false, "Print the version and exit.")
@@ -268,25 +299,23 @@ func (cfg *config) Parse(arguments []string) error {
 		os.Exit(0)
 	}
 
+	var err error
+	if cfg.configFile != "" {
+		plog.Infof("Loading server configuration from %q", cfg.configFile)
+		err = cfg.configFromFile()
+	} else {
+		err = cfg.configFromCmdLine()
+	}
+
+	return err
+}
+
+func (cfg *config) configFromCmdLine() error {
 	err := flags.SetFlagsFromEnv("ETCD", cfg.FlagSet)
 	if err != nil {
 		plog.Fatalf("%v", err)
 	}
 
-	set := make(map[string]bool)
-	cfg.FlagSet.Visit(func(f *flag.Flag) {
-		set[f.Name] = true
-	})
-	nSet := 0
-	for _, v := range []bool{set["discovery"], set["initial-cluster"], set["discovery-srv"]} {
-		if v {
-			nSet += 1
-		}
-	}
-	if nSet > 1 {
-		return ErrConflictBootstrapFlags
-	}
-
 	flags.SetBindAddrFromAddr(cfg.FlagSet, "peer-bind-addr", "peer-addr")
 	flags.SetBindAddrFromAddr(cfg.FlagSet, "bind-addr", "addr")
 
@@ -307,16 +336,126 @@ func (cfg *config) Parse(arguments []string) error {
 		return err
 	}
 
+	return cfg.validateConfig(func(field string) bool {
+		return flags.IsSet(cfg.FlagSet, field)
+	})
+}
+
+func (cfg *config) configFromFile() error {
+	b, err := ioutil.ReadFile(cfg.configFile)
+	if err != nil {
+		return err
+	}
+
+	err = yaml.Unmarshal(b, cfg)
+	if err != nil {
+		return err
+	}
+
+	if cfg.LPUrlsCfgFile != "" {
+		u, err := types.NewURLs(strings.Split(cfg.LPUrlsCfgFile, ","))
+		if err != nil {
+			plog.Fatalf("unexpected error setting up listen-peer-urls: %v", err)
+		}
+		cfg.lpurls = []url.URL(u)
+	}
+
+	if cfg.LCUrlsCfgFile != "" {
+		u, err := types.NewURLs(strings.Split(cfg.LCUrlsCfgFile, ","))
+		if err != nil {
+			plog.Fatalf("unexpected error setting up listen-client-urls: %v", err)
+		}
+		cfg.lcurls = []url.URL(u)
+	}
+
+	if cfg.CorsCfgFile != "" {
+		if err := cfg.corsInfo.Set(cfg.CorsCfgFile); err != nil {
+			plog.Panicf("unexpected error setting up cors: %v", err)
+		}
+	}
+
+	if cfg.ApurlsCfgFile != "" {
+		u, err := types.NewURLs(strings.Split(cfg.ApurlsCfgFile, ","))
+		if err != nil {
+			plog.Fatalf("unexpected error setting up initial-advertise-peer-urls: %v", err)
+		}
+		cfg.apurls = []url.URL(u)
+	}
+
+	if cfg.AcurlsCfgFile != "" {
+		u, err := types.NewURLs(strings.Split(cfg.AcurlsCfgFile, ","))
+		if err != nil {
+			plog.Fatalf("unexpected error setting up advertise-peer-urls: %v", err)
+		}
+		cfg.acurls = []url.URL(u)
+	}
+
+	if cfg.ClusterStateCfgFile != "" {
+		if err := cfg.clusterState.Set(cfg.ClusterStateCfgFile); err != nil {
+			plog.Panicf("unexpected error setting up clusterStateFlag: %v", err)
+		}
+	}
+
+	if cfg.FallbackCfgFile != "" {
+		if err := cfg.fallback.Set(cfg.FallbackCfgFile); err != nil {
+			plog.Panicf("unexpected error setting up discovery-fallback flag: %v", err)
+		}
+	}
+
+	if cfg.ProxyCfgFile != "" {
+		if err := cfg.proxy.Set(cfg.ProxyCfgFile); err != nil {
+			plog.Panicf("unexpected error setting up proxyFlag: %v", err)
+		}
+	}
+
+	copySecurityDetails := func(tls *transport.TLSInfo, ysc *securityConfig) {
+		tls.CAFile = ysc.CAFile
+		tls.CertFile = ysc.CertFile
+		tls.KeyFile = ysc.KeyFile
+		tls.ClientCertAuth = ysc.CertAuth
+		tls.TrustedCAFile = ysc.TrustedCAFile
+	}
+	copySecurityDetails(&cfg.clientTLSInfo, &cfg.ClientSecurityCfgFile)
+	copySecurityDetails(&cfg.peerTLSInfo, &cfg.PeerSecurityCfgFile)
+	cfg.ClientAutoTLS = cfg.ClientSecurityCfgFile.AutoTLS
+	cfg.PeerAutoTLS = cfg.PeerSecurityCfgFile.AutoTLS
+
+	fieldsToBeChecked := map[string]bool{
+		"discovery":             (cfg.Durl != ""),
+		"listen-client-urls":    (cfg.LCUrlsCfgFile != ""),
+		"advertise-client-urls": (cfg.AcurlsCfgFile != ""),
+		"initial-cluster":       (cfg.InitialCluster != ""),
+		"discovery-srv":         (cfg.DnsCluster != ""),
+	}
+
+	return cfg.validateConfig(func(field string) bool {
+		return fieldsToBeChecked[field]
+	})
+}
+
+func (cfg *config) validateConfig(isSet func(field string) bool) error {
 	// when etcd runs in member mode user needs to set --advertise-client-urls if --listen-client-urls is set.
 	// TODO(yichengq): check this for joining through discovery service case
-	mayFallbackToProxy := flags.IsSet(cfg.FlagSet, "discovery") && cfg.fallback.String() == fallbackFlagProxy
+	mayFallbackToProxy := isSet("discovery") && cfg.fallback.String() == fallbackFlagProxy
 	mayBeProxy := cfg.proxy.String() != proxyFlagOff || mayFallbackToProxy
 	if !mayBeProxy {
-		if flags.IsSet(cfg.FlagSet, "listen-client-urls") && !flags.IsSet(cfg.FlagSet, "advertise-client-urls") {
+		if isSet("listen-client-urls") && !isSet("advertise-client-urls") {
 			return errUnsetAdvertiseClientURLsFlag
 		}
 	}
 
+	// Check if conflicting flags are passed.
+	nSet := 0
+	for _, v := range []bool{isSet("discovery"), isSet("initial-cluster"), isSet("discovery-srv")} {
+		if v {
+			nSet += 1
+		}
+	}
+
+	if nSet > 1 {
+		return ErrConflictBootstrapFlags
+	}
+
 	if 5*cfg.TickMs > cfg.ElectionMs {
 		return fmt.Errorf("--election-timeout[%vms] should be at least as 5 times as --heartbeat-interval[%vms]", cfg.ElectionMs, cfg.TickMs)
 	}

+ 311 - 65
etcdmain/config_test.go

@@ -15,9 +15,15 @@
 package etcdmain
 
 import (
+	"fmt"
+	"io/ioutil"
 	"net/url"
+	"os"
 	"reflect"
+	"strings"
 	"testing"
+
+	"github.com/ghodss/yaml"
 )
 
 func TestConfigParsingMemberFlags(t *testing.T) {
@@ -32,42 +38,56 @@ func TestConfigParsingMemberFlags(t *testing.T) {
 		// it should be set if -listen-client-urls is set
 		"-advertise-client-urls=http://localhost:7000,https://localhost:7001",
 	}
-	wcfg := &config{
-		dir:          "testdir",
-		lpurls:       []url.URL{{Scheme: "http", Host: "localhost:8000"}, {Scheme: "https", Host: "localhost:8001"}},
-		lcurls:       []url.URL{{Scheme: "http", Host: "localhost:7000"}, {Scheme: "https", Host: "localhost:7001"}},
-		maxSnapFiles: 10,
-		maxWalFiles:  10,
-		name:         "testname",
-		snapCount:    10,
-	}
 
 	cfg := NewConfig()
 	err := cfg.Parse(args)
 	if err != nil {
 		t.Fatal(err)
 	}
-	if cfg.dir != wcfg.dir {
-		t.Errorf("dir = %v, want %v", cfg.dir, wcfg.dir)
-	}
-	if cfg.maxSnapFiles != wcfg.maxSnapFiles {
-		t.Errorf("maxsnap = %v, want %v", cfg.maxSnapFiles, wcfg.maxSnapFiles)
-	}
-	if cfg.maxWalFiles != wcfg.maxWalFiles {
-		t.Errorf("maxwal = %v, want %v", cfg.maxWalFiles, wcfg.maxWalFiles)
-	}
-	if cfg.name != wcfg.name {
-		t.Errorf("name = %v, want %v", cfg.name, wcfg.name)
+
+	validateMemberFlags(t, cfg)
+}
+
+func TestConfigFileMemberFields(t *testing.T) {
+	yc := struct {
+		Dir           string `json:"data-dir"`
+		MaxSnapFiles  uint   `json:"max-snapshots"`
+		MaxWalFiles   uint   `json:"max-wals"`
+		Name          string `json:"name"`
+		SnapCount     uint64 `json:"snapshot-count"`
+		LPUrls        string `json:"listen-peer-urls"`
+		LCUrls        string `json:"listen-client-urls"`
+		AcurlsCfgFile string `json:"advertise-client-urls"`
+	}{
+		"testdir",
+		10,
+		10,
+		"testname",
+		10,
+		"http://localhost:8000,https://localhost:8001",
+		"http://localhost:7000,https://localhost:7001",
+		"http://localhost:7000,https://localhost:7001",
 	}
-	if cfg.snapCount != wcfg.snapCount {
-		t.Errorf("snapcount = %v, want %v", cfg.snapCount, wcfg.snapCount)
+
+	b, err := yaml.Marshal(&yc)
+	if err != nil {
+		t.Fatal(err)
 	}
-	if !reflect.DeepEqual(cfg.lpurls, wcfg.lpurls) {
-		t.Errorf("listen-peer-urls = %v, want %v", cfg.lpurls, wcfg.lpurls)
+
+	tmpfile := mustCreateCfgFile(t, b)
+	defer os.Remove(tmpfile.Name())
+
+	args := []string{
+		fmt.Sprintf("--config-file=%s", tmpfile.Name()),
 	}
-	if !reflect.DeepEqual(cfg.lcurls, wcfg.lcurls) {
-		t.Errorf("listen-client-urls = %v, want %v", cfg.lcurls, wcfg.lcurls)
+
+	cfg := NewConfig()
+	err = cfg.Parse(args)
+	if err != nil {
+		t.Fatal(err)
 	}
+
+	validateMemberFlags(t, cfg)
 }
 
 func TestConfigParsingClusteringFlags(t *testing.T) {
@@ -79,37 +99,51 @@ func TestConfigParsingClusteringFlags(t *testing.T) {
 		"-advertise-client-urls=http://localhost:7000,https://localhost:7001",
 		"-discovery-fallback=exit",
 	}
-	wcfg := NewConfig()
-	wcfg.apurls = []url.URL{{Scheme: "http", Host: "localhost:8000"}, {Scheme: "https", Host: "localhost:8001"}}
-	wcfg.acurls = []url.URL{{Scheme: "http", Host: "localhost:7000"}, {Scheme: "https", Host: "localhost:7001"}}
-	wcfg.clusterState.Set(clusterStateFlagExisting)
-	wcfg.fallback.Set(fallbackFlagExit)
-	wcfg.initialCluster = "0=http://localhost:8000"
-	wcfg.initialClusterToken = "etcdtest"
 
 	cfg := NewConfig()
 	err := cfg.Parse(args)
 	if err != nil {
 		t.Fatal(err)
 	}
-	if cfg.clusterState.String() != wcfg.clusterState.String() {
-		t.Errorf("clusterState = %v, want %v", cfg.clusterState, wcfg.clusterState)
-	}
-	if cfg.fallback.String() != wcfg.fallback.String() {
-		t.Errorf("fallback = %v, want %v", cfg.fallback, wcfg.fallback)
-	}
-	if cfg.initialCluster != wcfg.initialCluster {
-		t.Errorf("initialCluster = %v, want %v", cfg.initialCluster, wcfg.initialCluster)
+
+	validateClusteringFlags(t, cfg)
+}
+
+func TestConfigFileClusteringFields(t *testing.T) {
+	yc := struct {
+		InitialCluster      string `json:"initial-cluster"`
+		ClusterState        string `json:"initial-cluster-state"`
+		InitialClusterToken string `json:"initial-cluster-token"`
+		Apurls              string `json:"initial-advertise-peer-urls"`
+		Acurls              string `json:"advertise-client-urls"`
+		Fallback            string `json:"discovery-fallback"`
+	}{
+		"0=http://localhost:8000",
+		"existing",
+		"etcdtest",
+		"http://localhost:8000,https://localhost:8001",
+		"http://localhost:7000,https://localhost:7001",
+		"exit",
 	}
-	if cfg.initialClusterToken != wcfg.initialClusterToken {
-		t.Errorf("initialClusterToken = %v, want %v", cfg.initialClusterToken, wcfg.initialClusterToken)
+
+	b, err := yaml.Marshal(&yc)
+	if err != nil {
+		t.Fatal(err)
 	}
-	if !reflect.DeepEqual(cfg.apurls, wcfg.apurls) {
-		t.Errorf("initial-advertise-peer-urls = %v, want %v", cfg.lpurls, wcfg.lpurls)
+
+	tmpfile := mustCreateCfgFile(t, b)
+	defer os.Remove(tmpfile.Name())
+
+	args := []string{
+		fmt.Sprintf("--config-file=%s", tmpfile.Name()),
 	}
-	if !reflect.DeepEqual(cfg.acurls, wcfg.acurls) {
-		t.Errorf("advertise-client-urls = %v, want %v", cfg.lcurls, wcfg.lcurls)
+	cfg := NewConfig()
+	err = cfg.Parse(args)
+	if err != nil {
+		t.Fatal(err)
 	}
+
+	validateClusteringFlags(t, cfg)
 }
 
 func TestConfigParsingOtherFlags(t *testing.T) {
@@ -124,33 +158,55 @@ func TestConfigParsingOtherFlags(t *testing.T) {
 		"-force-new-cluster=true",
 	}
 
-	wcfg := NewConfig()
-	wcfg.proxy.Set(proxyFlagReadonly)
-	wcfg.clientTLSInfo.CAFile = "cafile"
-	wcfg.clientTLSInfo.CertFile = "certfile"
-	wcfg.clientTLSInfo.KeyFile = "keyfile"
-	wcfg.peerTLSInfo.CAFile = "peercafile"
-	wcfg.peerTLSInfo.CertFile = "peercertfile"
-	wcfg.peerTLSInfo.KeyFile = "peerkeyfile"
-	wcfg.forceNewCluster = true
-
 	cfg := NewConfig()
 	err := cfg.Parse(args)
 	if err != nil {
 		t.Fatal(err)
 	}
-	if cfg.proxy.String() != wcfg.proxy.String() {
-		t.Errorf("proxy = %v, want %v", cfg.proxy, wcfg.proxy)
+
+	validateOtherFlags(t, cfg)
+}
+
+func TestConfigFileOtherFields(t *testing.T) {
+	yc := struct {
+		ProxyCfgFile          string         `json:"proxy"`
+		ClientSecurityCfgFile securityConfig `json:"client-transport-security"`
+		PeerSecurityCfgFile   securityConfig `json:"peer-transport-security"`
+		ForceNewCluster       bool           `json:"force-new-cluster"`
+	}{
+		"readonly",
+		securityConfig{
+			CAFile:   "cafile",
+			CertFile: "certfile",
+			KeyFile:  "keyfile",
+		},
+		securityConfig{
+			CAFile:   "peercafile",
+			CertFile: "peercertfile",
+			KeyFile:  "peerkeyfile",
+		},
+		true,
 	}
-	if cfg.clientTLSInfo.String() != wcfg.clientTLSInfo.String() {
-		t.Errorf("clientTLS = %v, want %v", cfg.clientTLSInfo, wcfg.clientTLSInfo)
+
+	b, err := yaml.Marshal(&yc)
+	if err != nil {
+		t.Fatal(err)
 	}
-	if cfg.peerTLSInfo.String() != wcfg.peerTLSInfo.String() {
-		t.Errorf("peerTLS = %v, want %v", cfg.peerTLSInfo, wcfg.peerTLSInfo)
+
+	tmpfile := mustCreateCfgFile(t, b)
+	defer os.Remove(tmpfile.Name())
+
+	args := []string{
+		fmt.Sprintf("--config-file=%s", tmpfile.Name()),
 	}
-	if cfg.forceNewCluster != wcfg.forceNewCluster {
-		t.Errorf("forceNewCluster = %t, want %t", cfg.forceNewCluster, wcfg.forceNewCluster)
+
+	cfg := NewConfig()
+	err = cfg.Parse(args)
+	if err != nil {
+		t.Fatal(err)
 	}
+
+	validateOtherFlags(t, cfg)
 }
 
 func TestConfigParsingV1Flags(t *testing.T) {
@@ -212,6 +268,52 @@ func TestConfigParsingConflictClusteringFlags(t *testing.T) {
 	}
 }
 
+func TestConfigFileConflictClusteringFlags(t *testing.T) {
+	tests := []struct {
+		InitialCluster string `json:"initial-cluster"`
+		DnsCluster     string `json:"discovery-srv"`
+		Durl           string `json:"discovery"`
+	}{
+		{
+			InitialCluster: "0=localhost:8000",
+			Durl:           "http://example.com/abc",
+		},
+		{
+			DnsCluster: "example.com",
+			Durl:       "http://example.com/abc",
+		},
+		{
+			InitialCluster: "0=localhost:8000",
+			DnsCluster:     "example.com",
+		},
+		{
+			InitialCluster: "0=localhost:8000",
+			Durl:           "http://example.com/abc",
+			DnsCluster:     "example.com",
+		},
+	}
+
+	for i, tt := range tests {
+		b, err := yaml.Marshal(&tt)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		tmpfile := mustCreateCfgFile(t, b)
+		defer os.Remove(tmpfile.Name())
+
+		args := []string{
+			fmt.Sprintf("--config-file=%s", tmpfile.Name()),
+		}
+
+		cfg := NewConfig()
+		err = cfg.Parse(args)
+		if err != ErrConflictBootstrapFlags {
+			t.Errorf("%d: err = %v, want %v", i, err, ErrConflictBootstrapFlags)
+		}
+	}
+}
+
 func TestConfigParsingMissedAdvertiseClientURLsFlag(t *testing.T) {
 	tests := []struct {
 		args []string
@@ -354,3 +456,147 @@ func TestConfigShouldFallbackToProxy(t *testing.T) {
 		}
 	}
 }
+
+func TestConfigFileElectionTimeout(t *testing.T) {
+	tests := []struct {
+		TickMs     uint `json:"heartbeat-interval"`
+		ElectionMs uint `json:"election-timeout"`
+		errStr     string
+	}{
+		{
+			ElectionMs: 1000,
+			TickMs:     800,
+			errStr:     "should be at least as 5 times as",
+		},
+		{
+			ElectionMs: 60000,
+			errStr:     "is too long, and should be set less than",
+		},
+	}
+
+	for i, tt := range tests {
+		b, err := yaml.Marshal(&tt)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		tmpfile := mustCreateCfgFile(t, b)
+		defer os.Remove(tmpfile.Name())
+
+		args := []string{
+			fmt.Sprintf("--config-file=%s", tmpfile.Name()),
+		}
+
+		cfg := NewConfig()
+		err = cfg.Parse(args)
+		if !strings.Contains(err.Error(), tt.errStr) {
+			t.Errorf("%d: Wrong err = %v", i, err)
+		}
+	}
+}
+
+func mustCreateCfgFile(t *testing.T, b []byte) *os.File {
+	tmpfile, err := ioutil.TempFile("", "servercfg")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = tmpfile.Write(b)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = tmpfile.Close()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return tmpfile
+}
+
+func validateMemberFlags(t *testing.T, cfg *config) {
+	wcfg := &config{
+		Dir:          "testdir",
+		lpurls:       []url.URL{{Scheme: "http", Host: "localhost:8000"}, {Scheme: "https", Host: "localhost:8001"}},
+		lcurls:       []url.URL{{Scheme: "http", Host: "localhost:7000"}, {Scheme: "https", Host: "localhost:7001"}},
+		MaxSnapFiles: 10,
+		MaxWalFiles:  10,
+		Name:         "testname",
+		SnapCount:    10,
+	}
+
+	if cfg.Dir != wcfg.Dir {
+		t.Errorf("dir = %v, want %v", cfg.Dir, wcfg.Dir)
+	}
+	if cfg.MaxSnapFiles != wcfg.MaxSnapFiles {
+		t.Errorf("maxsnap = %v, want %v", cfg.MaxSnapFiles, wcfg.MaxSnapFiles)
+	}
+	if cfg.MaxWalFiles != wcfg.MaxWalFiles {
+		t.Errorf("maxwal = %v, want %v", cfg.MaxWalFiles, wcfg.MaxWalFiles)
+	}
+	if cfg.Name != wcfg.Name {
+		t.Errorf("name = %v, want %v", cfg.Name, wcfg.Name)
+	}
+	if cfg.SnapCount != wcfg.SnapCount {
+		t.Errorf("snapcount = %v, want %v", cfg.SnapCount, wcfg.SnapCount)
+	}
+	if !reflect.DeepEqual(cfg.lpurls, wcfg.lpurls) {
+		t.Errorf("listen-peer-urls = %v, want %v", cfg.lpurls, wcfg.lpurls)
+	}
+	if !reflect.DeepEqual(cfg.lcurls, wcfg.lcurls) {
+		t.Errorf("listen-client-urls = %v, want %v", cfg.lcurls, wcfg.lcurls)
+	}
+}
+
+func validateClusteringFlags(t *testing.T, cfg *config) {
+	wcfg := NewConfig()
+	wcfg.apurls = []url.URL{{Scheme: "http", Host: "localhost:8000"}, {Scheme: "https", Host: "localhost:8001"}}
+	wcfg.acurls = []url.URL{{Scheme: "http", Host: "localhost:7000"}, {Scheme: "https", Host: "localhost:7001"}}
+	wcfg.clusterState.Set(clusterStateFlagExisting)
+	wcfg.fallback.Set(fallbackFlagExit)
+	wcfg.InitialCluster = "0=http://localhost:8000"
+	wcfg.InitialClusterToken = "etcdtest"
+
+	if cfg.clusterState.String() != wcfg.clusterState.String() {
+		t.Errorf("clusterState = %v, want %v", cfg.clusterState, wcfg.clusterState)
+	}
+	if cfg.fallback.String() != wcfg.fallback.String() {
+		t.Errorf("fallback = %v, want %v", cfg.fallback, wcfg.fallback)
+	}
+	if cfg.InitialCluster != wcfg.InitialCluster {
+		t.Errorf("initialCluster = %v, want %v", cfg.InitialCluster, wcfg.InitialCluster)
+	}
+	if cfg.InitialClusterToken != wcfg.InitialClusterToken {
+		t.Errorf("initialClusterToken = %v, want %v", cfg.InitialClusterToken, wcfg.InitialClusterToken)
+	}
+	if !reflect.DeepEqual(cfg.apurls, wcfg.apurls) {
+		t.Errorf("initial-advertise-peer-urls = %v, want %v", cfg.lpurls, wcfg.lpurls)
+	}
+	if !reflect.DeepEqual(cfg.acurls, wcfg.acurls) {
+		t.Errorf("advertise-client-urls = %v, want %v", cfg.lcurls, wcfg.lcurls)
+	}
+}
+
+func validateOtherFlags(t *testing.T, cfg *config) {
+	wcfg := NewConfig()
+	wcfg.proxy.Set(proxyFlagReadonly)
+	wcfg.clientTLSInfo.CAFile = "cafile"
+	wcfg.clientTLSInfo.CertFile = "certfile"
+	wcfg.clientTLSInfo.KeyFile = "keyfile"
+	wcfg.peerTLSInfo.CAFile = "peercafile"
+	wcfg.peerTLSInfo.CertFile = "peercertfile"
+	wcfg.peerTLSInfo.KeyFile = "peerkeyfile"
+	wcfg.ForceNewCluster = true
+
+	if cfg.proxy.String() != wcfg.proxy.String() {
+		t.Errorf("proxy = %v, want %v", cfg.proxy, wcfg.proxy)
+	}
+	if cfg.clientTLSInfo.String() != wcfg.clientTLSInfo.String() {
+		t.Errorf("clientTLS = %v, want %v", cfg.clientTLSInfo, wcfg.clientTLSInfo)
+	}
+	if cfg.peerTLSInfo.String() != wcfg.peerTLSInfo.String() {
+		t.Errorf("peerTLS = %v, want %v", cfg.peerTLSInfo, wcfg.peerTLSInfo)
+	}
+	if cfg.ForceNewCluster != wcfg.ForceNewCluster {
+		t.Errorf("forceNewCluster = %t, want %t", cfg.ForceNewCluster, wcfg.ForceNewCluster)
+	}
+}

+ 135 - 0
etcdmain/etcd.conf.sample.yml

@@ -0,0 +1,135 @@
+# This is the configuration file for the etcd server.
+
+# Human-readable name for this member.
+name: 'default'
+
+# Path to the data directory.
+data-dir:
+
+# Path to the dedicated wal directory.
+wal-dir:
+
+# Number of committed transactions to trigger a snapshot to disk.
+snapshot-count: 10000
+
+# Time (in milliseconds) of a heartbeat interval.
+heartbeat-interval: 100
+
+# Time (in milliseconds) for an election to timeout.
+election-timeout: 1000
+
+# Raise alarms when backend size exceeds the given quota. 0 means use the
+# default quota.
+quota-backend-bytes: 0
+
+# List of comma separated URLs to listen on for peer traffic.
+listen-peer-urls: http://localhost:2380,http://localhost:7001
+
+# List of comma separated URLs to listen on for client traffic.
+listen-client-urls: http://localhost:2379,http://localhost:4001
+
+# Maximum number of snapshot files to retain (0 is unlimited).
+max-snapshots: 5
+
+# Maximum number of wal files to retain (0 is unlimited).
+max-wals: 5
+
+# Comma-separated white list of origins for CORS (cross-origin resource sharing).
+cors: 
+
+# List of this member's peer URLs to advertise to the rest of the cluster.
+# The URLs needed to be a comma-separated list.
+initial-advertise-peer-urls: http://localhost:2380,http://localhost:7001
+
+# List of this member's client URLs to advertise to the public.
+# The URLs needed to be a comma-separated list.
+advertise-client-urls: http://localhost:2379,http://localhost:4001
+
+# Discovery URL used to bootstrap the cluster.
+discovery: 
+
+# Valid values include 'exit', 'proxy'
+discovery-fallback: 'proxy'
+
+# HTTP proxy to use for traffic to discovery service.
+discovery-proxy: 
+
+# DNS domain used to bootstrap initial cluster.
+discovery-srv: 
+
+# Initial cluster configuration for bootstrapping.
+initial-cluster:
+
+# Initial cluster token for the etcd cluster during bootstrap.
+initial-cluster-token: 'etcd-cluster'
+
+# Initial cluster state ('new' or 'existing').
+initial-cluster-state: 'new'
+
+# Reject reconfiguration requests that would cause quorum loss.
+strict-reconfig-check: false
+
+# Valid values include 'on', 'readonly', 'off'
+proxy: 'off'
+
+# Time (in milliseconds) an endpoint will be held in a failed state.
+proxy-failure-wait: 5000
+
+# Time (in milliseconds) of the endpoints refresh interval.
+proxy-refresh-interval: 30000
+
+# Time (in milliseconds) for a dial to timeout.
+proxy-dial-timeout: 1000
+
+# Time (in milliseconds) for a write to timeout.
+proxy-write-timeout: 5000
+
+# Time (in milliseconds) for a read to timeout.
+proxy-read-timeout: 0
+
+client-transport-security: 
+  # DEPRECATED: Path to the client server TLS CA file.
+  ca-file: 
+
+  # Path to the client server TLS cert file.
+  cert-file: 
+
+  # Path to the client server TLS key file.
+  key-file: 
+
+  # Enable client cert authentication.
+  client-cert-auth: false
+
+  # Path to the client server TLS trusted CA key file.
+  trusted-ca-file: 
+
+  # Client TLS using generated certificates
+  auto-tls: false
+
+peer-transport-security: 
+  # DEPRECATED: Path to the peer server TLS CA file.
+  ca-file:
+
+  # Path to the peer server TLS cert file.
+  peer-cert-file: 
+
+  # Path to the peer server TLS key file.
+  peer-key-file: 
+
+  # Enable peer client cert authentication.
+  peer-client-cert-auth: false
+
+  # Path to the peer server TLS trusted CA key file.
+  peer-trusted-ca-file: 
+
+  # Peer TLS using generated certificates.
+  auto-tls: false
+
+# Enable debug-level logging for etcd.
+debug: false
+
+# Specify a particular log level for each etcd package (eg: 'etcdmain=CRITICAL,etcdserver=DEBUG'.
+log-package-levels: 
+
+# Force to create a new one member cluster.
+force-new-cluster: false

+ 51 - 51
etcdmain/etcd.go

@@ -101,16 +101,16 @@ func Main() {
 	plog.Infof("setting maximum number of CPUs to %d, total number of available CPUs is %d", GoMaxProcs, runtime.NumCPU())
 
 	// TODO: check whether fields are set instead of whether fields have default value
-	if cfg.name != defaultName && cfg.initialCluster == initialClusterFromName(defaultName) {
-		cfg.initialCluster = initialClusterFromName(cfg.name)
+	if cfg.Name != defaultName && cfg.InitialCluster == initialClusterFromName(defaultName) {
+		cfg.InitialCluster = initialClusterFromName(cfg.Name)
 	}
 
-	if cfg.dir == "" {
-		cfg.dir = fmt.Sprintf("%v.etcd", cfg.name)
-		plog.Warningf("no data-dir provided, using default data-dir ./%s", cfg.dir)
+	if cfg.Dir == "" {
+		cfg.Dir = fmt.Sprintf("%v.etcd", cfg.Name)
+		plog.Warningf("no data-dir provided, using default data-dir ./%s", cfg.Dir)
 	}
 
-	which := identifyDataDirOrDie(cfg.dir)
+	which := identifyDataDirOrDie(cfg.Dir)
 	if which != dirEmpty {
 		plog.Noticef("the server is already initialized as %v before, starting as etcd %v...", which, which)
 		switch which {
@@ -141,17 +141,17 @@ func Main() {
 		if derr, ok := err.(*etcdserver.DiscoveryError); ok {
 			switch derr.Err {
 			case discovery.ErrDuplicateID:
-				plog.Errorf("member %q has previously registered with discovery service token (%s).", cfg.name, cfg.durl)
-				plog.Errorf("But etcd could not find valid cluster configuration in the given data dir (%s).", cfg.dir)
+				plog.Errorf("member %q has previously registered with discovery service token (%s).", cfg.Name, cfg.Durl)
+				plog.Errorf("But etcd could not find valid cluster configuration in the given data dir (%s).", cfg.Dir)
 				plog.Infof("Please check the given data dir path if the previous bootstrap succeeded")
 				plog.Infof("or use a new discovery token if the previous bootstrap failed.")
 			case discovery.ErrDuplicateName:
-				plog.Errorf("member with duplicated name has registered with discovery service token(%s).", cfg.durl)
+				plog.Errorf("member with duplicated name has registered with discovery service token(%s).", cfg.Durl)
 				plog.Errorf("please check (cURL) the discovery token for more information.")
 				plog.Errorf("please do not reuse the discovery token and generate a new one to bootstrap the cluster.")
 			default:
 				plog.Errorf("%v", err)
-				plog.Infof("discovery token %s was used, but failed to bootstrap the cluster.", cfg.durl)
+				plog.Infof("discovery token %s was used, but failed to bootstrap the cluster.", cfg.Durl)
 				plog.Infof("please generate a new discovery token and try to bootstrap again.")
 			}
 			os.Exit(1)
@@ -159,13 +159,13 @@ func Main() {
 
 		if strings.Contains(err.Error(), "include") && strings.Contains(err.Error(), "--initial-cluster") {
 			plog.Infof("%v", err)
-			if cfg.initialCluster == initialClusterFromName(cfg.name) {
+			if cfg.InitialCluster == initialClusterFromName(cfg.Name) {
 				plog.Infof("forgot to set --initial-cluster flag?")
 			}
 			if types.URLs(cfg.apurls).String() == defaultInitialAdvertisePeerURLs {
 				plog.Infof("forgot to set --initial-advertise-peer-urls flag?")
 			}
-			if cfg.initialCluster == initialClusterFromName(cfg.name) && len(cfg.durl) == 0 {
+			if cfg.InitialCluster == initialClusterFromName(cfg.Name) && len(cfg.Durl) == 0 {
 				plog.Infof("if you want to use discovery service, please set --discovery flag.")
 			}
 			os.Exit(1)
@@ -202,16 +202,16 @@ func startEtcd(cfg *config) (<-chan struct{}, error) {
 		return nil, fmt.Errorf("error setting up initial cluster: %v", err)
 	}
 
-	if cfg.peerAutoTLS && cfg.peerTLSInfo.Empty() {
+	if cfg.PeerAutoTLS && cfg.peerTLSInfo.Empty() {
 		var phosts []string
 		for _, u := range cfg.lpurls {
 			phosts = append(phosts, u.Host)
 		}
-		cfg.peerTLSInfo, err = transport.SelfCert(path.Join(cfg.dir, "fixtures/peer"), phosts)
+		cfg.peerTLSInfo, err = transport.SelfCert(path.Join(cfg.Dir, "fixtures/peer"), phosts)
 		if err != nil {
 			plog.Fatalf("could not get certs (%v)", err)
 		}
-	} else if cfg.peerAutoTLS {
+	} else if cfg.PeerAutoTLS {
 		plog.Warningf("ignoring peer auto TLS since certs given")
 	}
 
@@ -257,16 +257,16 @@ func startEtcd(cfg *config) (<-chan struct{}, error) {
 		plns = append(plns, l)
 	}
 
-	if cfg.clientAutoTLS && cfg.clientTLSInfo.Empty() {
+	if cfg.ClientAutoTLS && cfg.clientTLSInfo.Empty() {
 		var chosts []string
 		for _, u := range cfg.lcurls {
 			chosts = append(chosts, u.Host)
 		}
-		cfg.clientTLSInfo, err = transport.SelfCert(path.Join(cfg.dir, "fixtures/client"), chosts)
+		cfg.clientTLSInfo, err = transport.SelfCert(path.Join(cfg.Dir, "fixtures/client"), chosts)
 		if err != nil {
 			plog.Fatalf("could not get certs (%v)", err)
 		}
-	} else if cfg.clientAutoTLS {
+	} else if cfg.ClientAutoTLS {
 		plog.Warningf("ignoring client auto TLS since certs given")
 	}
 
@@ -343,26 +343,26 @@ func startEtcd(cfg *config) (<-chan struct{}, error) {
 	}
 
 	srvcfg := &etcdserver.ServerConfig{
-		Name:                    cfg.name,
+		Name:                    cfg.Name,
 		ClientURLs:              cfg.acurls,
 		PeerURLs:                cfg.apurls,
-		DataDir:                 cfg.dir,
-		DedicatedWALDir:         cfg.walDir,
-		SnapCount:               cfg.snapCount,
-		MaxSnapFiles:            cfg.maxSnapFiles,
-		MaxWALFiles:             cfg.maxWalFiles,
+		DataDir:                 cfg.Dir,
+		DedicatedWALDir:         cfg.WalDir,
+		SnapCount:               cfg.SnapCount,
+		MaxSnapFiles:            cfg.MaxSnapFiles,
+		MaxWALFiles:             cfg.MaxWalFiles,
 		InitialPeerURLsMap:      urlsmap,
 		InitialClusterToken:     token,
-		DiscoveryURL:            cfg.durl,
-		DiscoveryProxy:          cfg.dproxy,
+		DiscoveryURL:            cfg.Durl,
+		DiscoveryProxy:          cfg.Dproxy,
 		NewCluster:              cfg.isNewCluster(),
-		ForceNewCluster:         cfg.forceNewCluster,
+		ForceNewCluster:         cfg.ForceNewCluster,
 		PeerTLSInfo:             cfg.peerTLSInfo,
 		TickMs:                  cfg.TickMs,
 		ElectionTicks:           cfg.electionTicks(),
 		AutoCompactionRetention: cfg.autoCompactionRetention,
-		QuotaBackendBytes:       cfg.quotaBackendBytes,
-		StrictReconfigCheck:     cfg.strictReconfigCheck,
+		QuotaBackendBytes:       cfg.QuotaBackendBytes,
+		StrictReconfigCheck:     cfg.StrictReconfigCheck,
 		EnablePprof:             cfg.enablePprof,
 	}
 	var s *etcdserver.EtcdServer
@@ -402,33 +402,33 @@ func startEtcd(cfg *config) (<-chan struct{}, error) {
 
 // startProxy launches an HTTP proxy for client communication which proxies to other etcd nodes.
 func startProxy(cfg *config) error {
-	pt, err := transport.NewTimeoutTransport(cfg.peerTLSInfo, time.Duration(cfg.proxyDialTimeoutMs)*time.Millisecond, time.Duration(cfg.proxyReadTimeoutMs)*time.Millisecond, time.Duration(cfg.proxyWriteTimeoutMs)*time.Millisecond)
+	pt, err := transport.NewTimeoutTransport(cfg.peerTLSInfo, time.Duration(cfg.ProxyDialTimeoutMs)*time.Millisecond, time.Duration(cfg.ProxyReadTimeoutMs)*time.Millisecond, time.Duration(cfg.ProxyWriteTimeoutMs)*time.Millisecond)
 	if err != nil {
 		return err
 	}
 	pt.MaxIdleConnsPerHost = httpproxy.DefaultMaxIdleConnsPerHost
 
-	tr, err := transport.NewTimeoutTransport(cfg.peerTLSInfo, time.Duration(cfg.proxyDialTimeoutMs)*time.Millisecond, time.Duration(cfg.proxyReadTimeoutMs)*time.Millisecond, time.Duration(cfg.proxyWriteTimeoutMs)*time.Millisecond)
+	tr, err := transport.NewTimeoutTransport(cfg.peerTLSInfo, time.Duration(cfg.ProxyDialTimeoutMs)*time.Millisecond, time.Duration(cfg.ProxyReadTimeoutMs)*time.Millisecond, time.Duration(cfg.ProxyWriteTimeoutMs)*time.Millisecond)
 	if err != nil {
 		return err
 	}
 
-	cfg.dir = path.Join(cfg.dir, "proxy")
-	err = os.MkdirAll(cfg.dir, privateDirMode)
+	cfg.Dir = path.Join(cfg.Dir, "proxy")
+	err = os.MkdirAll(cfg.Dir, privateDirMode)
 	if err != nil {
 		return err
 	}
 
 	var peerURLs []string
-	clusterfile := path.Join(cfg.dir, "cluster")
+	clusterfile := path.Join(cfg.Dir, "cluster")
 
 	b, err := ioutil.ReadFile(clusterfile)
 	switch {
 	case err == nil:
-		if cfg.durl != "" {
+		if cfg.Durl != "" {
 			plog.Warningf("discovery token ignored since the proxy has already been initialized. Valid cluster file found at %q", clusterfile)
 		}
-		if cfg.dnsCluster != "" {
+		if cfg.DnsCluster != "" {
 			plog.Warningf("DNS SRV discovery ignored since the proxy has already been initialized. Valid cluster file found at %q", clusterfile)
 		}
 		urls := struct{ PeerURLs []string }{}
@@ -445,9 +445,9 @@ func startProxy(cfg *config) error {
 			return fmt.Errorf("error setting up initial cluster: %v", err)
 		}
 
-		if cfg.durl != "" {
+		if cfg.Durl != "" {
 			var s string
-			s, err = discovery.GetCluster(cfg.durl, cfg.dproxy)
+			s, err = discovery.GetCluster(cfg.Durl, cfg.Dproxy)
 			if err != nil {
 				return err
 			}
@@ -499,7 +499,7 @@ func startProxy(cfg *config) error {
 
 		return clientURLs
 	}
-	ph := httpproxy.NewHandler(pt, uf, time.Duration(cfg.proxyFailureWaitMs)*time.Millisecond, time.Duration(cfg.proxyRefreshIntervalMs)*time.Millisecond)
+	ph := httpproxy.NewHandler(pt, uf, time.Duration(cfg.ProxyFailureWaitMs)*time.Millisecond, time.Duration(cfg.ProxyRefreshIntervalMs)*time.Millisecond)
 	ph = &cors.CORSHandler{
 		Handler: ph,
 		Info:    cfg.corsInfo,
@@ -541,15 +541,15 @@ func startProxy(cfg *config) error {
 // getPeerURLsMapAndToken sets up an initial peer URLsMap and cluster token for bootstrap or discovery.
 func getPeerURLsMapAndToken(cfg *config, which string) (urlsmap types.URLsMap, token string, err error) {
 	switch {
-	case cfg.durl != "":
+	case cfg.Durl != "":
 		urlsmap = types.URLsMap{}
 		// If using discovery, generate a temporary cluster based on
 		// self's advertised peer URLs
-		urlsmap[cfg.name] = cfg.apurls
-		token = cfg.durl
-	case cfg.dnsCluster != "":
+		urlsmap[cfg.Name] = cfg.apurls
+		token = cfg.Durl
+	case cfg.DnsCluster != "":
 		var clusterStr string
-		clusterStr, token, err = discovery.SRVGetCluster(cfg.name, cfg.dnsCluster, cfg.initialClusterToken, cfg.apurls)
+		clusterStr, token, err = discovery.SRVGetCluster(cfg.Name, cfg.DnsCluster, cfg.InitialClusterToken, cfg.apurls)
 		if err != nil {
 			return nil, "", err
 		}
@@ -557,14 +557,14 @@ func getPeerURLsMapAndToken(cfg *config, which string) (urlsmap types.URLsMap, t
 		// only etcd member must belong to the discovered cluster.
 		// proxy does not need to belong to the discovered cluster.
 		if which == "etcd" {
-			if _, ok := urlsmap[cfg.name]; !ok {
-				return nil, "", fmt.Errorf("cannot find local etcd member %q in SRV records", cfg.name)
+			if _, ok := urlsmap[cfg.Name]; !ok {
+				return nil, "", fmt.Errorf("cannot find local etcd member %q in SRV records", cfg.Name)
 			}
 		}
 	default:
 		// We're statically configured, and cluster has appropriately been set.
-		urlsmap, err = types.NewURLsMap(cfg.initialCluster)
-		token = cfg.initialClusterToken
+		urlsmap, err = types.NewURLsMap(cfg.InitialCluster)
+		token = cfg.InitialClusterToken
 	}
 	return urlsmap, token, err
 }
@@ -606,12 +606,12 @@ func identifyDataDirOrDie(dir string) dirType {
 
 func setupLogging(cfg *config) {
 	capnslog.SetGlobalLogLevel(capnslog.INFO)
-	if cfg.debug {
+	if cfg.Debug {
 		capnslog.SetGlobalLogLevel(capnslog.DEBUG)
 	}
-	if cfg.logPkgLevels != "" {
+	if cfg.LogPkgLevels != "" {
 		repoLog := capnslog.MustRepoLogger("github.com/coreos/etcd")
-		settings, err := repoLog.ParseLogLevelConfig(cfg.logPkgLevels)
+		settings, err := repoLog.ParseLogLevelConfig(cfg.LogPkgLevels)
 		if err != nil {
 			plog.Warningf("couldn't parse log level string: %s, continuing with default levels", err.Error())
 			return

+ 3 - 0
etcdmain/help.go

@@ -25,6 +25,9 @@ var (
 
        etcd -h | --help
        show the help information about etcd
+
+       etcd --config-file
+       path to the server configuration file
 	`
 	flagsline = `
 member flags: