mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00

The ClientConfig is a fully declarive configuration, so it makes more sense to rename it to ConfigSpec. It can also mitigate the confusion between Config and ClientConfig.
435 lines
12 KiB
Go
435 lines
12 KiB
Go
// Copyright 2015 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 command
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/bgentry/speakeasy"
|
|
"go.etcd.io/etcd/client/pkg/v3/srv"
|
|
"go.etcd.io/etcd/client/pkg/v3/transport"
|
|
"go.etcd.io/etcd/client/v3"
|
|
"go.etcd.io/etcd/pkg/v3/cobrautl"
|
|
"go.etcd.io/etcd/pkg/v3/flags"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
"go.uber.org/zap"
|
|
"google.golang.org/grpc/grpclog"
|
|
)
|
|
|
|
// GlobalFlags are flags that defined globally
|
|
// and are inherited to all sub-commands.
|
|
type GlobalFlags struct {
|
|
Insecure bool
|
|
InsecureSkipVerify bool
|
|
InsecureDiscovery bool
|
|
Endpoints []string
|
|
DialTimeout time.Duration
|
|
CommandTimeOut time.Duration
|
|
KeepAliveTime time.Duration
|
|
KeepAliveTimeout time.Duration
|
|
DNSClusterServiceName string
|
|
|
|
TLS transport.TLSInfo
|
|
|
|
OutputFormat string
|
|
IsHex bool
|
|
|
|
User string
|
|
Password string
|
|
|
|
Debug bool
|
|
}
|
|
|
|
type discoveryCfg struct {
|
|
domain string
|
|
insecure bool
|
|
serviceName string
|
|
}
|
|
|
|
var display printer = &simplePrinter{}
|
|
|
|
func initDisplayFromCmd(cmd *cobra.Command) {
|
|
isHex, err := cmd.Flags().GetBool("hex")
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitError, err)
|
|
}
|
|
outputType, err := cmd.Flags().GetString("write-out")
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitError, err)
|
|
}
|
|
if display = NewPrinter(outputType, isHex); display == nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitBadFeature, errors.New("unsupported output format"))
|
|
}
|
|
}
|
|
|
|
type discardValue struct{}
|
|
|
|
func (*discardValue) String() string { return "" }
|
|
func (*discardValue) Set(string) error { return nil }
|
|
func (*discardValue) Type() string { return "" }
|
|
|
|
func clientConfigFromCmd(cmd *cobra.Command) *clientv3.ConfigSpec {
|
|
lg, err := zap.NewProduction()
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitError, err)
|
|
}
|
|
fs := cmd.InheritedFlags()
|
|
if strings.HasPrefix(cmd.Use, "watch") {
|
|
// silence "pkg/flags: unrecognized environment variable ETCDCTL_WATCH_KEY=foo" warnings
|
|
// silence "pkg/flags: unrecognized environment variable ETCDCTL_WATCH_RANGE_END=bar" warnings
|
|
fs.AddFlag(&pflag.Flag{Name: "watch-key", Value: &discardValue{}})
|
|
fs.AddFlag(&pflag.Flag{Name: "watch-range-end", Value: &discardValue{}})
|
|
}
|
|
flags.SetPflagsFromEnv(lg, "ETCDCTL", fs)
|
|
|
|
debug, err := cmd.Flags().GetBool("debug")
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitError, err)
|
|
}
|
|
if debug {
|
|
grpclog.SetLoggerV2(grpclog.NewLoggerV2WithVerbosity(os.Stderr, os.Stderr, os.Stderr, 4))
|
|
fs.VisitAll(func(f *pflag.Flag) {
|
|
fmt.Fprintf(os.Stderr, "%s=%v\n", flags.FlagToEnv("ETCDCTL", f.Name), f.Value)
|
|
})
|
|
} else {
|
|
// WARNING logs contain important information like TLS misconfirugation, but spams
|
|
// too many routine connection disconnects to turn on by default.
|
|
//
|
|
// See https://github.com/etcd-io/etcd/pull/9623 for background
|
|
grpclog.SetLoggerV2(grpclog.NewLoggerV2(io.Discard, io.Discard, os.Stderr))
|
|
}
|
|
|
|
cfg := &clientv3.ConfigSpec{}
|
|
cfg.Endpoints, err = endpointsFromCmd(cmd)
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitError, err)
|
|
}
|
|
|
|
cfg.DialTimeout = dialTimeoutFromCmd(cmd)
|
|
cfg.KeepAliveTime = keepAliveTimeFromCmd(cmd)
|
|
cfg.KeepAliveTimeout = keepAliveTimeoutFromCmd(cmd)
|
|
|
|
cfg.Secure = secureCfgFromCmd(cmd)
|
|
cfg.Auth = authCfgFromCmd(cmd)
|
|
|
|
initDisplayFromCmd(cmd)
|
|
return cfg
|
|
}
|
|
|
|
func mustClientCfgFromCmd(cmd *cobra.Command) *clientv3.Config {
|
|
cc := clientConfigFromCmd(cmd)
|
|
cfg, err := newClientCfg(cc.Endpoints, cc.DialTimeout, cc.KeepAliveTime, cc.KeepAliveTimeout, cc.Secure, cc.Auth)
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func mustClientFromCmd(cmd *cobra.Command) *clientv3.Client {
|
|
cfg := clientConfigFromCmd(cmd)
|
|
return mustClient(cfg)
|
|
}
|
|
|
|
func mustClient(cc *clientv3.ConfigSpec) *clientv3.Client {
|
|
cfg, err := newClientCfg(cc.Endpoints, cc.DialTimeout, cc.KeepAliveTime, cc.KeepAliveTimeout, cc.Secure, cc.Auth)
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
|
|
}
|
|
|
|
client, err := clientv3.New(*cfg)
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitBadConnection, err)
|
|
}
|
|
|
|
return client
|
|
}
|
|
|
|
func newClientCfg(endpoints []string, dialTimeout, keepAliveTime, keepAliveTimeout time.Duration, scfg *clientv3.SecureConfig, acfg *clientv3.AuthConfig) (*clientv3.Config, error) {
|
|
// set tls if any one tls option set
|
|
var cfgtls *transport.TLSInfo
|
|
tlsinfo := transport.TLSInfo{}
|
|
tlsinfo.Logger, _ = zap.NewProduction()
|
|
if scfg.Cert != "" {
|
|
tlsinfo.CertFile = scfg.Cert
|
|
cfgtls = &tlsinfo
|
|
}
|
|
|
|
if scfg.Key != "" {
|
|
tlsinfo.KeyFile = scfg.Key
|
|
cfgtls = &tlsinfo
|
|
}
|
|
|
|
if scfg.Cacert != "" {
|
|
tlsinfo.TrustedCAFile = scfg.Cacert
|
|
cfgtls = &tlsinfo
|
|
}
|
|
|
|
if scfg.ServerName != "" {
|
|
tlsinfo.ServerName = scfg.ServerName
|
|
cfgtls = &tlsinfo
|
|
}
|
|
|
|
cfg := &clientv3.Config{
|
|
Endpoints: endpoints,
|
|
DialTimeout: dialTimeout,
|
|
DialKeepAliveTime: keepAliveTime,
|
|
DialKeepAliveTimeout: keepAliveTimeout,
|
|
}
|
|
|
|
if cfgtls != nil {
|
|
clientTLS, err := cfgtls.ClientConfig()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.TLS = clientTLS
|
|
}
|
|
|
|
// if key/cert is not given but user wants secure connection, we
|
|
// should still setup an empty tls configuration for gRPC to setup
|
|
// secure connection.
|
|
if cfg.TLS == nil && !scfg.InsecureTransport {
|
|
cfg.TLS = &tls.Config{}
|
|
}
|
|
|
|
// If the user wants to skip TLS verification then we should set
|
|
// the InsecureSkipVerify flag in tls configuration.
|
|
if scfg.InsecureSkipVerify && cfg.TLS != nil {
|
|
cfg.TLS.InsecureSkipVerify = true
|
|
}
|
|
|
|
if acfg != nil {
|
|
cfg.Username = acfg.Username
|
|
cfg.Password = acfg.Password
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func argOrStdin(args []string, stdin io.Reader, i int) (string, error) {
|
|
if i < len(args) {
|
|
return args[i], nil
|
|
}
|
|
bytes, err := io.ReadAll(stdin)
|
|
if string(bytes) == "" || err != nil {
|
|
return "", errors.New("no available argument and stdin")
|
|
}
|
|
return string(bytes), nil
|
|
}
|
|
|
|
func dialTimeoutFromCmd(cmd *cobra.Command) time.Duration {
|
|
dialTimeout, err := cmd.Flags().GetDuration("dial-timeout")
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitError, err)
|
|
}
|
|
return dialTimeout
|
|
}
|
|
|
|
func keepAliveTimeFromCmd(cmd *cobra.Command) time.Duration {
|
|
keepAliveTime, err := cmd.Flags().GetDuration("keepalive-time")
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitError, err)
|
|
}
|
|
return keepAliveTime
|
|
}
|
|
|
|
func keepAliveTimeoutFromCmd(cmd *cobra.Command) time.Duration {
|
|
keepAliveTimeout, err := cmd.Flags().GetDuration("keepalive-timeout")
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitError, err)
|
|
}
|
|
return keepAliveTimeout
|
|
}
|
|
|
|
func secureCfgFromCmd(cmd *cobra.Command) *clientv3.SecureConfig {
|
|
cert, key, cacert := keyAndCertFromCmd(cmd)
|
|
insecureTr := insecureTransportFromCmd(cmd)
|
|
skipVerify := insecureSkipVerifyFromCmd(cmd)
|
|
discoveryCfg := discoveryCfgFromCmd(cmd)
|
|
|
|
if discoveryCfg.insecure {
|
|
discoveryCfg.domain = ""
|
|
}
|
|
|
|
return &clientv3.SecureConfig{
|
|
Cert: cert,
|
|
Key: key,
|
|
Cacert: cacert,
|
|
ServerName: discoveryCfg.domain,
|
|
|
|
InsecureTransport: insecureTr,
|
|
InsecureSkipVerify: skipVerify,
|
|
}
|
|
}
|
|
|
|
func insecureTransportFromCmd(cmd *cobra.Command) bool {
|
|
insecureTr, err := cmd.Flags().GetBool("insecure-transport")
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitError, err)
|
|
}
|
|
return insecureTr
|
|
}
|
|
|
|
func insecureSkipVerifyFromCmd(cmd *cobra.Command) bool {
|
|
skipVerify, err := cmd.Flags().GetBool("insecure-skip-tls-verify")
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitError, err)
|
|
}
|
|
return skipVerify
|
|
}
|
|
|
|
func keyAndCertFromCmd(cmd *cobra.Command) (cert, key, cacert string) {
|
|
var err error
|
|
if cert, err = cmd.Flags().GetString("cert"); err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
|
|
} else if cert == "" && cmd.Flags().Changed("cert") {
|
|
cobrautl.ExitWithError(cobrautl.ExitBadArgs, errors.New("empty string is passed to --cert option"))
|
|
}
|
|
|
|
if key, err = cmd.Flags().GetString("key"); err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
|
|
} else if key == "" && cmd.Flags().Changed("key") {
|
|
cobrautl.ExitWithError(cobrautl.ExitBadArgs, errors.New("empty string is passed to --key option"))
|
|
}
|
|
|
|
if cacert, err = cmd.Flags().GetString("cacert"); err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
|
|
} else if cacert == "" && cmd.Flags().Changed("cacert") {
|
|
cobrautl.ExitWithError(cobrautl.ExitBadArgs, errors.New("empty string is passed to --cacert option"))
|
|
}
|
|
|
|
return cert, key, cacert
|
|
}
|
|
|
|
func authCfgFromCmd(cmd *cobra.Command) *clientv3.AuthConfig {
|
|
userFlag, err := cmd.Flags().GetString("user")
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
|
|
}
|
|
passwordFlag, err := cmd.Flags().GetString("password")
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
|
|
}
|
|
|
|
if userFlag == "" {
|
|
return nil
|
|
}
|
|
|
|
var cfg clientv3.AuthConfig
|
|
|
|
if passwordFlag == "" {
|
|
splitted := strings.SplitN(userFlag, ":", 2)
|
|
if len(splitted) < 2 {
|
|
cfg.Username = userFlag
|
|
cfg.Password, err = speakeasy.Ask("Password: ")
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitError, err)
|
|
}
|
|
} else {
|
|
cfg.Username = splitted[0]
|
|
cfg.Password = splitted[1]
|
|
}
|
|
} else {
|
|
cfg.Username = userFlag
|
|
cfg.Password = passwordFlag
|
|
}
|
|
|
|
return &cfg
|
|
}
|
|
|
|
func insecureDiscoveryFromCmd(cmd *cobra.Command) bool {
|
|
discovery, err := cmd.Flags().GetBool("insecure-discovery")
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitError, err)
|
|
}
|
|
return discovery
|
|
}
|
|
|
|
func discoverySrvFromCmd(cmd *cobra.Command) string {
|
|
domainStr, err := cmd.Flags().GetString("discovery-srv")
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
|
|
}
|
|
return domainStr
|
|
}
|
|
|
|
func discoveryDNSClusterServiceNameFromCmd(cmd *cobra.Command) string {
|
|
serviceNameStr, err := cmd.Flags().GetString("discovery-srv-name")
|
|
if err != nil {
|
|
cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
|
|
}
|
|
return serviceNameStr
|
|
}
|
|
|
|
func discoveryCfgFromCmd(cmd *cobra.Command) *discoveryCfg {
|
|
return &discoveryCfg{
|
|
domain: discoverySrvFromCmd(cmd),
|
|
insecure: insecureDiscoveryFromCmd(cmd),
|
|
serviceName: discoveryDNSClusterServiceNameFromCmd(cmd),
|
|
}
|
|
}
|
|
|
|
func endpointsFromCmd(cmd *cobra.Command) ([]string, error) {
|
|
eps, err := endpointsFromFlagValue(cmd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// If domain discovery returns no endpoints, check endpoints flag
|
|
if len(eps) == 0 {
|
|
eps, err = cmd.Flags().GetStringSlice("endpoints")
|
|
if err == nil {
|
|
for i, ip := range eps {
|
|
eps[i] = strings.TrimSpace(ip)
|
|
}
|
|
}
|
|
}
|
|
return eps, err
|
|
}
|
|
|
|
func endpointsFromFlagValue(cmd *cobra.Command) ([]string, error) {
|
|
discoveryCfg := discoveryCfgFromCmd(cmd)
|
|
|
|
// If we still don't have domain discovery, return nothing
|
|
if discoveryCfg.domain == "" {
|
|
return []string{}, nil
|
|
}
|
|
|
|
srvs, err := srv.GetClient("etcd-client", discoveryCfg.domain, discoveryCfg.serviceName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
eps := srvs.Endpoints
|
|
if discoveryCfg.insecure {
|
|
return eps, err
|
|
}
|
|
// strip insecure connections
|
|
ret := []string{}
|
|
for _, ep := range eps {
|
|
if strings.HasPrefix(ep, "http://") {
|
|
fmt.Fprintf(os.Stderr, "ignoring discovered insecure endpoint %q\n", ep)
|
|
continue
|
|
}
|
|
ret = append(ret, ep)
|
|
}
|
|
return ret, err
|
|
}
|