*: combine etcdctl and etcdctlv3

This commit is contained in:
Xiang Li
2016-03-28 10:49:03 -07:00
parent 83ada7232a
commit 87d9f06a45
57 changed files with 108 additions and 67 deletions

391
etcdctl/READMEv3.md Normal file
View File

@@ -0,0 +1,391 @@
etcdctl
========
TODO: merge into README.md.
## Commands
### PUT [options] \<key\> \<value\>
PUT assigns the specified value with the specified key. If key already holds a value, it is overwritten.
#### Options
- lease -- lease ID (in hexadecimal) to attach to the key.
#### Return value
##### Simple reply
- OK if PUT executed correctly. Exit code is zero.
- Error string if PUT failed. Exit code is non-zero.
##### JSON reply
The JSON encoding of the PUT [RPC response][etcdrpc].
##### Protobuf reply
The protobuf encoding of the PUT [RPC response][etcdrpc].
#### Examples
``` bash
./etcdctl PUT foo bar --lease=0x1234abcd
OK
./etcdctl range foo
bar
```
#### Notes
If \<value\> isn't given as command line argument, this command tries to read the value from standard input.
When \<value\> begins with '-', \<value\> is interpreted as a flag.
Insert '--' for workaround:
``` bash
./etcdctl put <key> -- <value>
./etcdctl put -- <key> <value>
```
### GET [options] \<key\> [range_end]
GET gets the key or a range of keys [key, range_end) if `range-end` is given.
#### Options
- hex -- print out key and value as hex encode string
- limit -- maximum number of results
- order -- order of results; ASCEND or DESCEND
- sort-by -- sort target; CREATE, KEY, MODIFY, VALUE, or VERSION
TODO: add consistency, from, prefix
#### Return value
##### Simple reply
- \<key\>\n\<value\>\n\<next_key\>\n\<next_value\>...
- Error string if GET failed. Exit code is non-zero.
##### JSON reply
The JSON encoding of the [RPC message][etcdrpc] for a key-value pair for each fetched key-value.
##### Protobuf reply
The protobuf encoding of the [RPC message][etcdrpc] for a key-value pair for each fetched key-value.
#### Examples
``` bash
./etcdctl get foo
foo
bar
```
#### Notes
If any key or value contains non-printable characters or control characters, the output in text format (e.g. simple reply) might be ambiguous.
Adding `--hex` to print key or value as hex encode string in text format can resolve this issue.
### DEL [options] \<key\> [range_end]
Removes the specified key or range of keys [key, range_end) if `range-end` is given.
#### Options
TODO: --prefix, --from
#### Return value
##### Simple reply
- The number of keys that were removed in decimal if DEL executed correctly. Exit code is zero.
- Error string if DEL failed. Exit code is non-zero.
##### JSON reply
The JSON encoding of the DeleteRange [RPC response][etcdrpc].
##### Protobuf reply
The protobuf encoding of the DeleteRange [RPC response][etcdrpc].
#### Examples
``` bash
./etcdctl put foo bar
OK
./etcdctl del foo
1
./etcdctl range foo
```
### TXN [options]
TXN reads multiple etcd requests from standard input and applies them as a single atomic transaction.
A transaction consists of list of conditions, a list of requests to apply if all the conditions are true, and a list of requests to apply if any condition is false.
#### Options
- hex -- print out keys and values as hex encoded string
- interactive -- input transaction with interactive prompting
#### Input Format
```ebnf
<Txn> ::= <CMP>* "\n" <THEN> "\n" <ELSE> "\n"
<CMP> ::= (<CMPCREATE>|<CMPMOD>|<CMPVAL>|<CMPVER>) "\n"
<CMPOP> ::= "<" | "=" | ">"
<CMPCREATE> := ("c"|"create")"("<KEY>")" <REVISION>
<CMPMOD> ::= ("m"|"mod")"("<KEY>")" <CMPOP> <REVISION>
<CMPVAL> ::= ("val"|"value")"("<KEY>")" <CMPOP> <VALUE>
<CMPVER> ::= ("ver"|"version")"("<KEY>")" <CMPOP> <VERSION>
<THEN> ::= <OP>*
<ELSE> ::= <OP>*
<OP> ::= ((see put, get, del etcdctl command syntax)) "\n"
<KEY> ::= (%q formatted string)
<VALUE> ::= (%q formatted string)
<REVISION> ::= "\""[0-9]+"\""
<VERSION> ::= "\""[0-9]+"\""
```
#### Return value
##### Simple reply
- SUCCESS if etcd processed the transaction success list, FAILURE if etcd processed the transaction failure list.
- Simple reply for each command executed request list, each separated by a blank line.
- Additional error string if TXN failed. Exit code is non-zero.
##### JSON reply
The JSON encoding of the Txn [RPC response][etcdrpc].
##### Protobuf reply
The protobuf encoding of the Txn [RPC response][etcdrpc].
#### Examples
txn in interactive mode:
``` bash
./etcdctl txn -i
mod("key1") > "0"
put key1 "overwrote-key1"
put key1 "created-key1"
put key2 "some extra key"
FAILURE
OK
OK
```
txn in non-interactive mode:
```
./etcdctl txn <<<'mod("key1") > "0"
put key1 "overwrote-key1"
put key1 "created-key1"
put key2 "some extra key"
'
FAILURE
OK
OK
````
### WATCH [options] [key or prefix]
Watch watches events stream on keys or prefixes. The watch command runs until it encounters an error or is terminated by the user.
#### Options
- hex -- print out key and value as hex encode string
- interactive -- begins an interactive watch session
- prefix -- watch on a prefix if prefix is set.
- rev -- the revision to start watching. Specifying a revision is useful for observing past events.
#### Input Format
Input is only accepted for interactive mode.
```
watch [options] <key or prefix>\n
```
#### Return value
##### Simple reply
- \<event\>\n\<key\>\n\<value\>\n\<event\>\n\<next_key\>\n\<next_value\>\n...
- Additional error string if WATCH failed. Exit code is non-zero.
##### JSON reply
The JSON encoding of the [RPC message][storagerpc] for each received Event.
##### Protobuf reply
The protobuf encoding of the [RPC message][storagerpc] for each received Event.
#### Examples
##### Non-interactive
``` bash
./etcdctl watch foo
PUT
foo
bar
```
##### Interactive
``` bash
./etcdctl watch -i
watch foo
watch foo
PUT
foo
bar
PUT
foo
bar
```
## Utility Commands
### LOCK \<lockname\>
LOCK acquires a distributed named mutex with a given name. Once the lock is acquired, it will be held until etcdctl is terminated.
#### Return value
- Once the lock is acquired, the result for the GET on the unique lock holder key is displayed.
- LOCK returns a zero exit code only if it is terminated by a signal and can release the lock.
#### Example
```bash
./etcdctl lock mylock
mylock/1234534535445
```
### Notes
The lease length of a lock defaults to 60 seconds. If LOCK is abnormally terminated, lock progress may be delayed
by up to 60 seconds.
### ELECT [options] \<election-name\> [proposal]
ELECT participates on a named election. A node announces its candidacy in the election by providing
a proposal value. If a node wishes to observe the election, ELECT listens for new leaders values.
Whenever a leader is elected, its proposal is given as output.
#### Options
- listen -- observe the election
#### Return value
- If a candidate, ELECT displays the GET on the leader key once the node is elected election.
- If observing, ELECT streams the result for a GET on the leader key for the current election and all future elections.
- ELECT returns a zero exit code only if it is terminated by a signal and can revoke its candidacy or leadership, if any.
#### Example
```bash
./etcdctl elect myelection foo
myelection/1456952310051373265
foo
```
### Notes
The lease length of a leader defaults to 60 seconds. If a candidate is abnormally terminated, election
progress may be delayed by up to 60 seconds.
### MAKE-MIRROR [options] \<destination\>
[make-mirror][mirror] mirrors a key prefix in an etcd cluster to a destination etcd cluster.
#### Options
- dest-cacert -- TLS certificate authority file for destination cluster
- dest-cert -- TLS certificate file for destination cluster
- dest-key -- TLS key file for destination cluster
- prefix -- The key-value prefix to mirror
#### Return value
Simple reply
- The approximate total number of keys transferred to the destination cluster, updated every 30 seconds.
- Error string if mirroring failed. Exit code is non-zero.
#### Examples
```
./etcdctl make-mirror mirror.example.com:2379
10
18
```
[mirror]: ./doc/mirror_maker.md
## Notes
- JSON encoding for keys and values uses base64 since they are byte strings.
[etcdrpc]: ../etcdserver/etcdserverpb/rpc.proto
[storagerpc]: ../storage/storagepb/kv.proto
## Compatibility Support
etcdctl is still in its early stage. We try out best to ensure fully compatible releases, however we might break compatibility to fix bugs or improve commands. If we intend to release a version of etcdctl with backward incompatibilities, we will provide notice prior to release and have instructions on how to upgrade.
### Input Compatibility
Input includes the command name, its flags, and its arguments. We ensure backward compatibility of the input of normal commands in non-interactive mode.
### Output Compatibility
Output includes output from etcdctl and its exit code. etcdctl provides `simple` output format by default.
We ensure compatibility for the `simple` output format of normal commands in non-interactive mode. Currently, we do not ensure
backward compatibility for `JSON` format and the format in non-interactive mode. Currently, we do not ensure backward compatibility of utility commands.
### TODO: compatibility with etcd server

70
etcdctl/ctlv2/ctl.go Normal file
View File

@@ -0,0 +1,70 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 ctlv2 contains the main entry point for the etcdctl for v2 API.
package ctlv2
import (
"os"
"time"
"github.com/codegangsta/cli"
"github.com/coreos/etcd/etcdctl/ctlv2/command"
"github.com/coreos/etcd/version"
)
func Start() {
app := cli.NewApp()
app.Name = "etcdctl"
app.Version = version.Version
app.Usage = "A simple command line client for etcd."
app.Flags = []cli.Flag{
cli.BoolFlag{Name: "debug", Usage: "output cURL commands which can be used to reproduce the request"},
cli.BoolFlag{Name: "no-sync", Usage: "don't synchronize cluster information before sending request"},
cli.StringFlag{Name: "output, o", Value: "simple", Usage: "output response in the given format (`simple`, `extended` or `json`)"},
cli.StringFlag{Name: "discovery-srv, D", Usage: "domain name to query for SRV records describing cluster endpoints"},
cli.StringFlag{Name: "peers, C", Value: "", Usage: "DEPRECATED - \"--endpoints\" should be used instead"},
cli.StringFlag{Name: "endpoint", Value: "", Usage: "DEPRECATED - \"--endpoints\" should be used instead"},
cli.StringFlag{Name: "endpoints", Value: "", Usage: "a comma-delimited list of machine addresses in the cluster (default: \"http://127.0.0.1:2379,http://127.0.0.1:4001\")"},
cli.StringFlag{Name: "cert-file", Value: "", Usage: "identify HTTPS client using this SSL certificate file"},
cli.StringFlag{Name: "key-file", Value: "", Usage: "identify HTTPS client using this SSL key file"},
cli.StringFlag{Name: "ca-file", Value: "", Usage: "verify certificates of HTTPS-enabled servers using this CA bundle"},
cli.StringFlag{Name: "username, u", Value: "", Usage: "provide username[:password] and prompt if password is not supplied."},
cli.DurationFlag{Name: "timeout", Value: time.Second, Usage: "connection timeout per request"},
cli.DurationFlag{Name: "total-timeout", Value: 5 * time.Second, Usage: "timeout for the command execution (except watch)"},
}
app.Commands = []cli.Command{
command.NewBackupCommand(),
command.NewClusterHealthCommand(),
command.NewMakeCommand(),
command.NewMakeDirCommand(),
command.NewRemoveCommand(),
command.NewRemoveDirCommand(),
command.NewGetCommand(),
command.NewLsCommand(),
command.NewSetCommand(),
command.NewSetDirCommand(),
command.NewUpdateCommand(),
command.NewUpdateDirCommand(),
command.NewWatchCommand(),
command.NewExecWatchCommand(),
command.NewMemberCommand(),
command.NewImportSnapCommand(),
command.NewUserCommands(),
command.NewRoleCommands(),
command.NewAuthCommands(),
}
app.Run(os.Args)
}

View File

@@ -0,0 +1,55 @@
// Copyright 2016 Nippon Telegraph and Telephone Corporation.
//
// 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 (
"fmt"
"github.com/spf13/cobra"
)
// NewAuthCommand returns the cobra command for "auth".
func NewAuthCommand() *cobra.Command {
ac := &cobra.Command{
Use: "auth <enable or disable>",
Short: "Enable or disable authentication.",
}
ac.AddCommand(NewAuthEnableCommand())
return ac
}
func NewAuthEnableCommand() *cobra.Command {
return &cobra.Command{
Use: "enable",
Short: "enable authentication",
Run: authEnableCommandFunc,
}
}
// authEnableCommandFunc executes the "auth enable" command.
func authEnableCommandFunc(cmd *cobra.Command, args []string) {
if len(args) != 0 {
ExitWithError(ExitBadArgs, fmt.Errorf("auth enable command does not accept argument."))
}
ctx, cancel := commandCtx(cmd)
_, err := mustClientFromCmd(cmd).Auth.AuthEnable(ctx)
cancel()
if err != nil {
ExitWithError(ExitError, err)
}
}

View File

@@ -0,0 +1,53 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"fmt"
"strconv"
"github.com/spf13/cobra"
)
// NewCompactionCommand returns the cobra command for "compaction".
func NewCompactionCommand() *cobra.Command {
return &cobra.Command{
Use: "compaction <revision>",
Short: "Compaction compacts the event history in etcd.",
Run: compactionCommandFunc,
}
}
// compactionCommandFunc executes the "compaction" command.
func compactionCommandFunc(cmd *cobra.Command, args []string) {
if len(args) != 1 {
ExitWithError(ExitBadArgs, fmt.Errorf("compaction command needs 1 argument."))
}
rev, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
ExitWithError(ExitError, err)
}
c := mustClientFromCmd(cmd)
ctx, cancel := commandCtx(cmd)
cerr := c.Compact(ctx, rev)
cancel()
if cerr != nil {
ExitWithError(ExitError, cerr)
return
}
fmt.Println("compacted revision", rev)
}

View File

@@ -0,0 +1,45 @@
// Copyright 2016 CoreOS, Inc.
//
// 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 (
"fmt"
"os"
"github.com/spf13/cobra"
)
// NewDefragCommand returns the cobra command for "Defrag".
func NewDefragCommand() *cobra.Command {
return &cobra.Command{
Use: "defrag",
Short: "defrag defragments the storage of the etcd members with given endpoints.",
Run: defragCommandFunc,
}
}
func defragCommandFunc(cmd *cobra.Command, args []string) {
c := mustClientFromCmd(cmd)
for _, ep := range c.Endpoints() {
ctx, cancel := commandCtx(cmd)
_, err := c.Defragment(ctx, ep)
cancel()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to defragment etcd member[%s] (%v)\n", ep, err)
} else {
fmt.Printf("Finished defragmenting etcd member[%s]\n", ep)
}
}
}

View File

@@ -0,0 +1,55 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"fmt"
"github.com/coreos/etcd/clientv3"
"github.com/spf13/cobra"
)
// NewDelCommand returns the cobra command for "del".
func NewDelCommand() *cobra.Command {
return &cobra.Command{
Use: "del [options] <key> [range_end]",
Short: "Removes the specified key or range of keys [key, range_end).",
Run: delCommandFunc,
}
}
// delCommandFunc executes the "del" command.
func delCommandFunc(cmd *cobra.Command, args []string) {
key, opts := getDelOp(cmd, args)
ctx, cancel := commandCtx(cmd)
resp, err := mustClientFromCmd(cmd).Delete(ctx, key, opts...)
cancel()
if err != nil {
ExitWithError(ExitError, err)
}
display.Del(*resp)
}
func getDelOp(cmd *cobra.Command, args []string) (string, []clientv3.OpOption) {
if len(args) == 0 || len(args) > 2 {
ExitWithError(ExitBadArgs, fmt.Errorf("del command needs one argument as key and an optional argument as range_end."))
}
opts := []clientv3.OpOption{}
key := args[0]
if len(args) > 1 {
opts = append(opts, clientv3.WithRange(args[1]))
}
return key, opts
}

View File

@@ -0,0 +1,16 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 is a set of libraries for etcd v3 commands.
package command

View File

@@ -0,0 +1,132 @@
// Copyright 2016 CoreOS, Inc.
//
// 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 (
"errors"
"os"
"os/signal"
"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/clientv3/concurrency"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
var (
electListen bool
)
// NewElectCommand returns the cobra command for "elect".
func NewElectCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "elect <election-name> [proposal]",
Short: "elect observes and participates in leader election",
Run: electCommandFunc,
}
cmd.Flags().BoolVarP(&electListen, "listen", "l", false, "observation mode")
return cmd
}
func electCommandFunc(cmd *cobra.Command, args []string) {
if len(args) != 1 && len(args) != 2 {
ExitWithError(ExitBadArgs, errors.New("elect takes one election name argument and an optional proposal argument."))
}
c := mustClientFromCmd(cmd)
var err error
if len(args) == 1 {
if !electListen {
ExitWithError(ExitBadArgs, errors.New("no proposal argument but -l not set"))
}
err = observe(c, args[0])
} else {
if electListen {
ExitWithError(ExitBadArgs, errors.New("proposal given but -l is set"))
}
err = campaign(c, args[0], args[1])
}
if err != nil {
ExitWithError(ExitError, err)
}
}
func observe(c *clientv3.Client, election string) error {
e := concurrency.NewElection(c, election)
ctx, cancel := context.WithCancel(context.TODO())
donec := make(chan struct{})
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, os.Kill)
go func() {
<-sigc
cancel()
}()
go func() {
for resp := range e.Observe(ctx) {
display.Get(resp)
}
close(donec)
}()
<-donec
select {
case <-ctx.Done():
default:
return errors.New("elect: observer lost")
}
return nil
}
func campaign(c *clientv3.Client, election string, prop string) error {
e := concurrency.NewElection(c, election)
ctx, cancel := context.WithCancel(context.TODO())
donec := make(chan struct{})
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, os.Kill)
go func() {
<-sigc
cancel()
close(donec)
}()
s, serr := concurrency.NewSession(c)
if serr != nil {
return serr
}
if err := e.Campaign(ctx, prop); err != nil {
return err
}
// print key since elected
resp, err := c.Get(ctx, e.Key())
if err != nil {
return err
}
display.Get(*resp)
select {
case <-donec:
case <-s.Done():
return errors.New("elect: session expired")
}
return e.Resign()
}

View File

@@ -0,0 +1,83 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"fmt"
"sync"
"time"
"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/pkg/flags"
"github.com/spf13/cobra"
)
// NewEpHealthCommand returns the cobra command for "endpoint-health".
func NewEpHealthCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "endpoint-health",
Short: "endpoint-health checks the healthiness of endpoints specified in `--endpoints` flag",
Run: epHealthCommandFunc,
}
return cmd
}
// epHealthCommandFunc executes the "endpoint-health" command.
func epHealthCommandFunc(cmd *cobra.Command, args []string) {
flags.SetPflagsFromEnv("ETCDCTL", cmd.InheritedFlags())
endpoints, err := cmd.Flags().GetStringSlice("endpoints")
if err != nil {
ExitWithError(ExitError, err)
}
sec := secureCfgFromCmd(cmd)
dt := dialTimeoutFromCmd(cmd)
cfgs := []*clientv3.Config{}
for _, ep := range endpoints {
cfg, err := newClientCfg([]string{ep}, dt, sec)
if err != nil {
ExitWithError(ExitBadArgs, err)
}
cfgs = append(cfgs, cfg)
}
var wg sync.WaitGroup
for _, cfg := range cfgs {
wg.Add(1)
go func(cfg *clientv3.Config) {
defer wg.Done()
ep := cfg.Endpoints[0]
cli, err := clientv3.New(*cfg)
if err != nil {
fmt.Printf("%s is unhealthy: failed to connect: %v\n", ep, err)
return
}
st := time.Now()
// get a random key. As long as we can get the response without an error, the
// endpoint is health.
ctx, cancel := commandCtx(cmd)
_, err = cli.Get(ctx, "health")
cancel()
if err != nil {
fmt.Printf("%s is unhealthy: failed to commit proposal: %v\n", ep, err)
} else {
fmt.Printf("%s is healthy: successfully committed proposal: took = %v\n", ep, time.Since(st))
}
}(cfg)
}
wg.Wait()
}

View File

@@ -0,0 +1,42 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"fmt"
"os"
"github.com/coreos/etcd/client"
)
const (
// http://tldp.org/LDP/abs/html/exitcodes.html
ExitSuccess = iota
ExitError
ExitBadConnection
ExitInvalidInput // for txn, watch command
ExitBadFeature // provided a valid flag with an unsupported value
ExitInterrupted
ExitIO
ExitBadArgs = 128
)
func ExitWithError(code int, err error) {
fmt.Fprintln(os.Stderr, "Error: ", err)
if cerr, ok := err.(*client.ClusterError); ok {
fmt.Fprintln(os.Stderr, cerr.Detail())
}
os.Exit(code)
}

View File

@@ -0,0 +1,135 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"fmt"
"strings"
"github.com/coreos/etcd/clientv3"
"github.com/spf13/cobra"
)
var (
getConsistency string
getLimit int64
getSortOrder string
getSortTarget string
getPrefix bool
getFromKey bool
)
// NewGetCommand returns the cobra command for "get".
func NewGetCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "get [options] <key> [range_end]",
Short: "Get gets the key or a range of keys.",
Run: getCommandFunc,
}
cmd.Flags().StringVar(&getConsistency, "consistency", "l", "Linearizable(l) or Serializable(s)")
cmd.Flags().StringVar(&getSortOrder, "order", "", "order of results; ASCEND or DESCEND")
cmd.Flags().StringVar(&getSortTarget, "sort-by", "", "sort target; CREATE, KEY, MODIFY, VALUE, or VERSION")
cmd.Flags().Int64Var(&getLimit, "limit", 0, "maximum number of results")
cmd.Flags().BoolVar(&getPrefix, "prefix", false, "get keys with matching prefix")
cmd.Flags().BoolVar(&getFromKey, "from-key", false, "get keys that are greater than or equal to the given key")
return cmd
}
// getCommandFunc executes the "get" command.
func getCommandFunc(cmd *cobra.Command, args []string) {
key, opts := getGetOp(cmd, args)
ctx, cancel := commandCtx(cmd)
resp, err := mustClientFromCmd(cmd).Get(ctx, key, opts...)
cancel()
if err != nil {
ExitWithError(ExitError, err)
}
display.Get(*resp)
}
func getGetOp(cmd *cobra.Command, args []string) (string, []clientv3.OpOption) {
if len(args) == 0 {
ExitWithError(ExitBadArgs, fmt.Errorf("range command needs arguments."))
}
if getPrefix && getFromKey {
ExitWithError(ExitBadArgs, fmt.Errorf("`--prefix` and `--from-key` cannot be set at the same time, choose one."))
}
opts := []clientv3.OpOption{}
switch getConsistency {
case "s":
opts = append(opts, clientv3.WithSerializable())
case "l":
default:
ExitWithError(ExitBadFeature, fmt.Errorf("unknown consistency flag %q", getConsistency))
}
key := args[0]
if len(args) > 1 {
if getPrefix || getFromKey {
ExitWithError(ExitBadArgs, fmt.Errorf("too many arguments, only accept one arguement when `--prefix` or `--from-key` is set."))
}
opts = append(opts, clientv3.WithRange(args[1]))
}
opts = append(opts, clientv3.WithLimit(getLimit))
sortByOrder := clientv3.SortNone
sortOrder := strings.ToUpper(getSortOrder)
switch {
case sortOrder == "ASCEND":
sortByOrder = clientv3.SortAscend
case sortOrder == "DESCEND":
sortByOrder = clientv3.SortDescend
case sortOrder == "":
// nothing
default:
ExitWithError(ExitBadFeature, fmt.Errorf("bad sort order %v", getSortOrder))
}
sortByTarget := clientv3.SortByKey
sortTarget := strings.ToUpper(getSortTarget)
switch {
case sortTarget == "CREATE":
sortByTarget = clientv3.SortByCreateRevision
case sortTarget == "KEY":
sortByTarget = clientv3.SortByKey
case sortTarget == "MODIFY":
sortByTarget = clientv3.SortByModRevision
case sortTarget == "VALUE":
sortByTarget = clientv3.SortByValue
case sortTarget == "VERSION":
sortByTarget = clientv3.SortByVersion
case sortTarget == "":
// nothing
default:
ExitWithError(ExitBadFeature, fmt.Errorf("bad sort target %v", getSortTarget))
}
opts = append(opts, clientv3.WithSort(sortByTarget, sortByOrder))
if getPrefix {
opts = append(opts, clientv3.WithPrefix())
}
if getFromKey {
opts = append(opts, clientv3.WithFromKey())
}
return key, opts
}

View File

@@ -0,0 +1,199 @@
// Copyright 2015 CoreOS, Inc.
//
// 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"
"io"
"io/ioutil"
"time"
"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/pkg/flags"
"github.com/coreos/etcd/pkg/transport"
"github.com/spf13/cobra"
)
// GlobalFlags are flags that defined globally
// and are inherited to all sub-commands.
type GlobalFlags struct {
Insecure bool
InsecureSkipVerify bool
Endpoints []string
DialTimeout time.Duration
CommandTimeOut time.Duration
TLS transport.TLSInfo
OutputFormat string
IsHex bool
}
type secureCfg struct {
cert string
key string
cacert string
insecureTransport bool
insecureSkipVerify bool
}
var display printer = &simplePrinter{}
func mustClientFromCmd(cmd *cobra.Command) *clientv3.Client {
flags.SetPflagsFromEnv("ETCDCTL", cmd.InheritedFlags())
endpoints, err := cmd.Flags().GetStringSlice("endpoints")
if err != nil {
ExitWithError(ExitError, err)
}
dialTimeout := dialTimeoutFromCmd(cmd)
sec := secureCfgFromCmd(cmd)
return mustClient(endpoints, dialTimeout, sec)
}
func mustClient(endpoints []string, dialTimeout time.Duration, scfg *secureCfg) *clientv3.Client {
cfg, err := newClientCfg(endpoints, dialTimeout, scfg)
if err != nil {
ExitWithError(ExitBadArgs, err)
}
client, err := clientv3.New(*cfg)
if err != nil {
ExitWithError(ExitBadConnection, err)
}
return client
}
func newClientCfg(endpoints []string, dialTimeout time.Duration, scfg *secureCfg) (*clientv3.Config, error) {
// set tls if any one tls option set
var cfgtls *transport.TLSInfo
tlsinfo := transport.TLSInfo{}
if scfg.cert != "" {
tlsinfo.CertFile = scfg.cert
cfgtls = &tlsinfo
}
if scfg.key != "" {
tlsinfo.KeyFile = scfg.key
cfgtls = &tlsinfo
}
if scfg.cacert != "" {
tlsinfo.CAFile = scfg.cacert
cfgtls = &tlsinfo
}
cfg := &clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
}
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
}
return cfg, nil
}
func argOrStdin(args []string, stdin io.Reader, i int) (string, error) {
if i < len(args) {
return args[i], nil
}
bytes, err := ioutil.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 {
ExitWithError(ExitError, err)
}
return dialTimeout
}
func secureCfgFromCmd(cmd *cobra.Command) *secureCfg {
cert, key, cacert := keyAndCertFromCmd(cmd)
insecureTr := insecureTransportFromCmd(cmd)
skipVerify := insecureSkipVerifyFromCmd(cmd)
return &secureCfg{
cert: cert,
key: key,
cacert: cacert,
insecureTransport: insecureTr,
insecureSkipVerify: skipVerify,
}
}
func insecureTransportFromCmd(cmd *cobra.Command) bool {
insecureTr, err := cmd.Flags().GetBool("insecure-transport")
if err != nil {
ExitWithError(ExitError, err)
}
return insecureTr
}
func insecureSkipVerifyFromCmd(cmd *cobra.Command) bool {
skipVerify, err := cmd.Flags().GetBool("insecure-skip-tls-verify")
if err != nil {
ExitWithError(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 {
ExitWithError(ExitBadArgs, err)
} else if cert == "" && cmd.Flags().Changed("cert") {
ExitWithError(ExitBadArgs, errors.New("empty string is passed to --cert option"))
}
if key, err = cmd.Flags().GetString("key"); err != nil {
ExitWithError(ExitBadArgs, err)
} else if key == "" && cmd.Flags().Changed("key") {
ExitWithError(ExitBadArgs, errors.New("empty string is passed to --key option"))
}
if cacert, err = cmd.Flags().GetString("cacert"); err != nil {
ExitWithError(ExitBadArgs, err)
} else if cacert == "" && cmd.Flags().Changed("cacert") {
ExitWithError(ExitBadArgs, errors.New("empty string is passed to --cacert option"))
}
return cert, key, cacert
}

View File

@@ -0,0 +1,139 @@
// Copyright 2016 CoreOS, Inc.
//
// 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 (
"fmt"
"os"
"strconv"
v3 "github.com/coreos/etcd/clientv3"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
// NewLeaseCommand returns the cobra command for "lease".
func NewLeaseCommand() *cobra.Command {
lc := &cobra.Command{
Use: "lease",
Short: "lease is used to manage leases.",
}
lc.AddCommand(NewLeaseCreateCommand())
lc.AddCommand(NewLeaseRevokeCommand())
lc.AddCommand(NewLeaseKeepAliveCommand())
return lc
}
// NewLeaseCreateCommand returns the cobra command for "lease create".
func NewLeaseCreateCommand() *cobra.Command {
lc := &cobra.Command{
Use: "create",
Short: "create is used to create leases.",
Run: leaseCreateCommandFunc,
}
return lc
}
// leaseCreateCommandFunc executes the "lease create" command.
func leaseCreateCommandFunc(cmd *cobra.Command, args []string) {
if len(args) != 1 {
ExitWithError(ExitBadArgs, fmt.Errorf("lease create command needs TTL argument."))
}
ttl, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
ExitWithError(ExitBadArgs, fmt.Errorf("bad TTL (%v)", err))
}
ctx, cancel := commandCtx(cmd)
resp, err := mustClientFromCmd(cmd).Create(ctx, ttl)
cancel()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to create lease (%v)\n", err)
return
}
fmt.Printf("lease %016x created with TTL(%ds)\n", resp.ID, resp.TTL)
}
// NewLeaseRevokeCommand returns the cobra command for "lease revoke".
func NewLeaseRevokeCommand() *cobra.Command {
lc := &cobra.Command{
Use: "revoke",
Short: "revoke is used to revoke leases.",
Run: leaseRevokeCommandFunc,
}
return lc
}
// leaseRevokeCommandFunc executes the "lease create" command.
func leaseRevokeCommandFunc(cmd *cobra.Command, args []string) {
if len(args) != 1 {
ExitWithError(ExitBadArgs, fmt.Errorf("lease revoke command needs 1 argument"))
}
id, err := strconv.ParseInt(args[0], 16, 64)
if err != nil {
ExitWithError(ExitBadArgs, fmt.Errorf("bad lease ID arg (%v), expecting ID in Hex", err))
}
ctx, cancel := commandCtx(cmd)
_, err = mustClientFromCmd(cmd).Revoke(ctx, v3.LeaseID(id))
cancel()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to revoke lease (%v)\n", err)
return
}
fmt.Printf("lease %016x revoked\n", id)
}
// NewLeaseKeepAliveCommand returns the cobra command for "lease keep-alive".
func NewLeaseKeepAliveCommand() *cobra.Command {
lc := &cobra.Command{
Use: "keep-alive",
Short: "keep-alive is used to keep leases alive.",
Run: leaseKeepAliveCommandFunc,
}
return lc
}
// leaseKeepAliveCommandFunc executes the "lease keep-alive" command.
func leaseKeepAliveCommandFunc(cmd *cobra.Command, args []string) {
if len(args) != 1 {
ExitWithError(ExitBadArgs, fmt.Errorf("lease keep-alive command needs lease ID as argument"))
}
id, err := strconv.ParseInt(args[0], 16, 64)
if err != nil {
ExitWithError(ExitBadArgs, fmt.Errorf("bad lease ID arg (%v), expecting ID in Hex", err))
}
respc, kerr := mustClientFromCmd(cmd).KeepAlive(context.TODO(), v3.LeaseID(id))
if kerr != nil {
ExitWithError(ExitBadConnection, kerr)
}
for resp := range respc {
fmt.Printf("lease %016x keepalived with TTL(%d)\n", resp.ID, resp.TTL)
}
fmt.Printf("lease %016x expired or revoked.\n", id)
}

View File

@@ -0,0 +1,88 @@
// Copyright 2016 CoreOS, Inc.
//
// 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 (
"errors"
"os"
"os/signal"
"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/clientv3/concurrency"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
// NewLockCommand returns the cobra command for "lock".
func NewLockCommand() *cobra.Command {
c := &cobra.Command{
Use: "lock <lockname>",
Short: "lock acquires a named lock",
Run: lockCommandFunc,
}
return c
}
func lockCommandFunc(cmd *cobra.Command, args []string) {
if len(args) != 1 {
ExitWithError(ExitBadArgs, errors.New("lock takes one lock name arguement."))
}
c := mustClientFromCmd(cmd)
if err := lockUntilSignal(c, args[0]); err != nil {
ExitWithError(ExitError, err)
}
}
func lockUntilSignal(c *clientv3.Client, lockname string) error {
m := concurrency.NewMutex(c, lockname)
ctx, cancel := context.WithCancel(context.TODO())
// unlock in case of ordinary shutdown
donec := make(chan struct{})
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, os.Kill)
go func() {
<-sigc
cancel()
close(donec)
}()
s, serr := concurrency.NewSession(c)
if serr != nil {
return serr
}
if err := m.Lock(ctx); err != nil {
return err
}
k, kerr := c.Get(ctx, m.Key())
if kerr != nil {
return kerr
}
if len(k.Kvs) == 0 {
return errors.New("lock lost on init")
}
display.Get(*k)
select {
case <-donec:
return m.Unlock()
case <-s.Done():
}
return errors.New("session expired")
}

View File

@@ -0,0 +1,148 @@
// Copyright 2016 CoreOS, Inc.
//
// 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 (
"errors"
"fmt"
"sync/atomic"
"time"
"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/clientv3/mirror"
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
"github.com/coreos/etcd/storage/storagepb"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
var (
mminsecureTr bool
mmcert string
mmkey string
mmcacert string
mmprefix string
)
// NewMakeMirrorCommand returns the cobra command for "makeMirror".
func NewMakeMirrorCommand() *cobra.Command {
c := &cobra.Command{
Use: "make-mirror [options] <destination>",
Short: "make-mirror makes a mirror at the destination etcd cluster",
Run: makeMirrorCommandFunc,
}
c.Flags().StringVar(&mmprefix, "prefix", "", "the key-value prefix to mirror")
// TODO: add dest-prefix to mirror a prefix to a different prefix in the destionation cluster?
c.Flags().StringVar(&mmcert, "dest-cert", "", "identify secure client using this TLS certificate file for the destination cluster")
c.Flags().StringVar(&mmkey, "dest-key", "", "identify secure client using this TLS key file")
c.Flags().StringVar(&mmcacert, "dest-cacert", "", "verify certificates of TLS enabled secure servers using this CA bundle")
// TODO: secure by default when etcd enables secure gRPC by default.
c.Flags().BoolVar(&mminsecureTr, "dest-insecure-transport", true, "disable transport security for client connections")
return c
}
func makeMirrorCommandFunc(cmd *cobra.Command, args []string) {
if len(args) != 1 {
ExitWithError(ExitBadArgs, errors.New("make-mirror takes one destination arguement."))
}
dialTimeout := dialTimeoutFromCmd(cmd)
sec := &secureCfg{
cert: mmcert,
key: mmkey,
cacert: mmcacert,
insecureTransport: mminsecureTr,
}
dc := mustClient([]string{args[0]}, dialTimeout, sec)
c := mustClientFromCmd(cmd)
err := makeMirror(context.TODO(), c, dc)
ExitWithError(ExitError, err)
}
func makeMirror(ctx context.Context, c *clientv3.Client, dc *clientv3.Client) error {
total := int64(0)
go func() {
for {
time.Sleep(30 * time.Second)
fmt.Println(atomic.LoadInt64(&total))
}
}()
// TODO: remove the prefix of the destination cluster?
s := mirror.NewSyncer(c, mmprefix, 0)
rc, errc := s.SyncBase(ctx)
for r := range rc {
for _, kv := range r.Kvs {
_, err := dc.Put(ctx, string(kv.Key), string(kv.Value))
if err != nil {
return err
}
atomic.AddInt64(&total, 1)
}
}
err := <-errc
if err != nil {
return err
}
wc := s.SyncUpdates(ctx)
for wr := range wc {
if wr.CompactRevision != 0 {
return rpctypes.ErrCompacted
}
var rev int64
ops := []clientv3.Op{}
for _, ev := range wr.Events {
nrev := ev.Kv.ModRevision
if rev != 0 && nrev > rev {
_, err := dc.Txn(ctx).Then(ops...).Commit()
if err != nil {
return err
}
ops = []clientv3.Op{}
}
switch ev.Type {
case storagepb.PUT:
ops = append(ops, clientv3.OpPut(string(ev.Kv.Key), string(ev.Kv.Value)))
atomic.AddInt64(&total, 1)
case storagepb.DELETE, storagepb.EXPIRE:
ops = append(ops, clientv3.OpDelete(string(ev.Kv.Key)))
atomic.AddInt64(&total, 1)
default:
panic("unexpected event type")
}
}
if len(ops) != 0 {
_, err := dc.Txn(ctx).Then(ops...).Commit()
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,176 @@
// Copyright 2016 CoreOS, Inc.
//
// 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 (
"fmt"
"strconv"
"strings"
"github.com/spf13/cobra"
)
var (
memberID uint64
memberPeerURLs string
)
// NewMemberCommand returns the cobra command for "member".
func NewMemberCommand() *cobra.Command {
mc := &cobra.Command{
Use: "member",
Short: "member is used to manage membership in an etcd cluster.",
}
mc.AddCommand(NewMemberAddCommand())
mc.AddCommand(NewMemberRemoveCommand())
mc.AddCommand(NewMemberUpdateCommand())
mc.AddCommand(NewMemberListCommand())
return mc
}
// NewMemberAddCommand returns the cobra command for "member add".
func NewMemberAddCommand() *cobra.Command {
cc := &cobra.Command{
Use: "add",
Short: "add is used to add a member into the cluster",
Run: memberAddCommandFunc,
}
cc.Flags().StringVar(&memberPeerURLs, "peerURLs", "", "comma separated peer URLs for the new member.")
return cc
}
// NewMemberRemoveCommand returns the cobra command for "member remove".
func NewMemberRemoveCommand() *cobra.Command {
cc := &cobra.Command{
Use: "remove",
Short: "remove is used to remove a member from the cluster",
Run: memberRemoveCommandFunc,
}
return cc
}
// NewMemberUpdateCommand returns the cobra command for "member update".
func NewMemberUpdateCommand() *cobra.Command {
cc := &cobra.Command{
Use: "update",
Short: "update is used to update a member in the cluster",
Run: memberUpdateCommandFunc,
}
cc.Flags().StringVar(&memberPeerURLs, "peerURLs", "", "comma separated peer URLs for the updated member.")
return cc
}
// NewMemberListCommand returns the cobra command for "member list".
func NewMemberListCommand() *cobra.Command {
cc := &cobra.Command{
Use: "list",
Short: "list is used to list all members in the cluster",
Run: memberListCommandFunc,
}
return cc
}
// memberAddCommandFunc executes the "member add" command.
func memberAddCommandFunc(cmd *cobra.Command, args []string) {
if len(args) != 1 {
ExitWithError(ExitBadArgs, fmt.Errorf("member name not provided."))
}
if len(memberPeerURLs) == 0 {
ExitWithError(ExitBadArgs, fmt.Errorf("member peer urls not provided."))
}
urls := strings.Split(memberPeerURLs, ",")
ctx, cancel := commandCtx(cmd)
resp, err := mustClientFromCmd(cmd).MemberAdd(ctx, urls)
cancel()
if err != nil {
ExitWithError(ExitError, err)
}
fmt.Printf("Member %16x added to cluster %16x\n", resp.Member.ID, resp.Header.ClusterId)
}
// memberRemoveCommandFunc executes the "member remove" command.
func memberRemoveCommandFunc(cmd *cobra.Command, args []string) {
if len(args) != 1 {
ExitWithError(ExitBadArgs, fmt.Errorf("member ID is not provided"))
}
id, err := strconv.ParseUint(args[0], 16, 64)
if err != nil {
ExitWithError(ExitBadArgs, fmt.Errorf("bad member ID arg (%v), expecting ID in Hex", err))
}
ctx, cancel := commandCtx(cmd)
resp, err := mustClientFromCmd(cmd).MemberRemove(ctx, id)
cancel()
if err != nil {
ExitWithError(ExitError, err)
}
fmt.Printf("Member %16x removed from cluster %16x\n", id, resp.Header.ClusterId)
}
// memberUpdateCommandFunc executes the "member update" command.
func memberUpdateCommandFunc(cmd *cobra.Command, args []string) {
if len(args) != 1 {
ExitWithError(ExitBadArgs, fmt.Errorf("member ID is not provided"))
}
id, err := strconv.ParseUint(args[0], 16, 64)
if err != nil {
ExitWithError(ExitBadArgs, fmt.Errorf("bad member ID arg (%v), expecting ID in Hex", err))
}
if len(memberPeerURLs) == 0 {
ExitWithError(ExitBadArgs, fmt.Errorf("member peer urls not provided."))
}
urls := strings.Split(memberPeerURLs, ",")
ctx, cancel := commandCtx(cmd)
resp, err := mustClientFromCmd(cmd).MemberUpdate(ctx, id, urls)
cancel()
if err != nil {
ExitWithError(ExitError, err)
}
fmt.Printf("Member %16x updated in cluster %16x\n", id, resp.Header.ClusterId)
}
// memberListCommandFunc executes the "member list" command.
func memberListCommandFunc(cmd *cobra.Command, args []string) {
ctx, cancel := commandCtx(cmd)
resp, err := mustClientFromCmd(cmd).MemberList(ctx)
cancel()
if err != nil {
ExitWithError(ExitError, err)
}
display.MemberList(*resp)
}

View File

@@ -0,0 +1,183 @@
// Copyright 2016 CoreOS, Inc.
//
// 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 (
"encoding/json"
"errors"
"fmt"
"os"
"strings"
v3 "github.com/coreos/etcd/clientv3"
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
spb "github.com/coreos/etcd/storage/storagepb"
"github.com/olekukonko/tablewriter"
)
type printer interface {
Del(v3.DeleteResponse)
Get(v3.GetResponse)
Put(v3.PutResponse)
Txn(v3.TxnResponse)
Watch(v3.WatchResponse)
MemberList(v3.MemberListResponse)
}
func NewPrinter(printerType string, isHex bool) printer {
switch printerType {
case "simple":
return &simplePrinter{isHex: isHex}
case "json":
return &jsonPrinter{}
case "protobuf":
return &pbPrinter{}
}
return nil
}
type simplePrinter struct {
isHex bool
}
func (s *simplePrinter) Del(v3.DeleteResponse) {
// TODO: add number of key removed into the response of delete.
// TODO: print out the number of removed keys.
fmt.Println(0)
}
func (s *simplePrinter) Get(resp v3.GetResponse) {
for _, kv := range resp.Kvs {
printKV(s.isHex, kv)
}
}
func (s *simplePrinter) Put(r v3.PutResponse) { fmt.Println("OK") }
func (s *simplePrinter) Txn(resp v3.TxnResponse) {
if resp.Succeeded {
fmt.Println("SUCCESS")
} else {
fmt.Println("FAILURE")
}
for _, r := range resp.Responses {
fmt.Println("")
switch v := r.Response.(type) {
case *pb.ResponseUnion_ResponseDeleteRange:
s.Del((v3.DeleteResponse)(*v.ResponseDeleteRange))
case *pb.ResponseUnion_ResponsePut:
s.Put((v3.PutResponse)(*v.ResponsePut))
case *pb.ResponseUnion_ResponseRange:
s.Get(((v3.GetResponse)(*v.ResponseRange)))
default:
fmt.Printf("unexpected response %+v\n", r)
}
}
}
func (s *simplePrinter) Watch(resp v3.WatchResponse) {
for _, e := range resp.Events {
fmt.Println(e.Type)
printKV(s.isHex, e.Kv)
}
}
func (s *simplePrinter) MemberList(resp v3.MemberListResponse) {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "Status", "Name", "Peer Addrs", "Client Addrs", "Is Leader"})
for _, m := range resp.Members {
status := "started"
if len(m.Name) == 0 {
status = "unstarted"
}
table.Append([]string{
fmt.Sprintf("%x", m.ID),
status,
m.Name,
strings.Join(m.PeerURLs, ","),
strings.Join(m.ClientURLs, ","),
fmt.Sprint(m.IsLeader),
})
}
table.Render()
}
type jsonPrinter struct{}
func (p *jsonPrinter) Del(r v3.DeleteResponse) { printJSON(r) }
func (p *jsonPrinter) Get(r v3.GetResponse) {
for _, kv := range r.Kvs {
printJSON(kv)
}
}
func (p *jsonPrinter) Put(r v3.PutResponse) { printJSON(r) }
func (p *jsonPrinter) Txn(r v3.TxnResponse) { printJSON(r) }
func (p *jsonPrinter) Watch(r v3.WatchResponse) { printJSON(r) }
func (p *jsonPrinter) MemberList(r v3.MemberListResponse) { printJSON(r) }
func printJSON(v interface{}) {
b, err := json.Marshal(v)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return
}
fmt.Println(string(b))
}
type pbPrinter struct{}
type pbMarshal interface {
Marshal() ([]byte, error)
}
func (p *pbPrinter) Del(r v3.DeleteResponse) {
printPB((*pb.DeleteRangeResponse)(&r))
}
func (p *pbPrinter) Get(r v3.GetResponse) {
printPB((*pb.RangeResponse)(&r))
}
func (p *pbPrinter) Put(r v3.PutResponse) {
printPB((*pb.PutResponse)(&r))
}
func (p *pbPrinter) Txn(r v3.TxnResponse) {
printPB((*pb.TxnResponse)(&r))
}
func (p *pbPrinter) Watch(r v3.WatchResponse) {
for _, ev := range r.Events {
printPB((*spb.Event)(ev))
}
}
func (pb *pbPrinter) MemberList(r v3.MemberListResponse) {
ExitWithError(ExitBadFeature, errors.New("only support simple or json as output format"))
}
func printPB(m pbMarshal) {
b, err := m.Marshal()
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return
}
fmt.Printf(string(b))
}

View File

@@ -0,0 +1,90 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"fmt"
"os"
"strconv"
"github.com/coreos/etcd/clientv3"
"github.com/spf13/cobra"
)
var (
leaseStr string
)
// NewPutCommand returns the cobra command for "put".
func NewPutCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "put [options] <key> <value> (<value> can also be given from stdin)",
Short: "Put puts the given key into the store.",
Long: `
Put puts the given key into the store.
When <value> begins with '-', <value> is interpreted as a flag.
Insert '--' for workaround:
$ put <key> -- <value>
$ put -- <key> <value>
If <value> isn't given as command line arguement, this command tries to read the value from standard input.
For example,
$ cat file | put <key>
will store the content of the file to <key>.
`,
Run: putCommandFunc,
}
cmd.Flags().StringVar(&leaseStr, "lease", "0", "lease ID (in hexadecimal) to attach to the key")
return cmd
}
// putCommandFunc executes the "put" command.
func putCommandFunc(cmd *cobra.Command, args []string) {
key, value, opts := getPutOp(cmd, args)
ctx, cancel := commandCtx(cmd)
resp, err := mustClientFromCmd(cmd).Put(ctx, key, value, opts...)
cancel()
if err != nil {
ExitWithError(ExitError, err)
}
display.Put(*resp)
}
func getPutOp(cmd *cobra.Command, args []string) (string, string, []clientv3.OpOption) {
if len(args) == 0 {
ExitWithError(ExitBadArgs, fmt.Errorf("put command needs 1 argument and input from stdin or 2 arguments."))
}
key := args[0]
value, err := argOrStdin(args, os.Stdin, 1)
if err != nil {
ExitWithError(ExitBadArgs, fmt.Errorf("put command needs 1 argument and input from stdin or 2 arguments."))
}
id, err := strconv.ParseInt(leaseStr, 16, 64)
if err != nil {
ExitWithError(ExitBadArgs, fmt.Errorf("bad lease ID (%v), expecting ID in Hex", err))
}
opts := []clientv3.OpOption{}
if id != 0 {
opts = append(opts, clientv3.WithLease(clientv3.LeaseID(id)))
}
return key, value, opts
}

View File

@@ -0,0 +1,127 @@
// Copyright 2016 CoreOS, Inc.
//
// 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 (
"fmt"
"io"
"os"
"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/clientv3/mirror"
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
// NewSnapshotCommand returns the cobra command for "snapshot".
func NewSnapshotCommand() *cobra.Command {
return &cobra.Command{
Use: "snapshot [filename]",
Short: "Snapshot streams a point-in-time snapshot of the store",
Run: snapshotCommandFunc,
}
}
// snapshotCommandFunc watches for the length of the entire store and records
// to a file.
func snapshotCommandFunc(cmd *cobra.Command, args []string) {
switch {
case len(args) == 0:
snapshotToStdout(mustClientFromCmd(cmd))
case len(args) == 1:
snapshotToFile(mustClientFromCmd(cmd), args[0])
default:
err := fmt.Errorf("snapshot takes at most one argument")
ExitWithError(ExitBadArgs, err)
}
}
// snapshotToStdout streams a snapshot over stdout
func snapshotToStdout(c *clientv3.Client) {
// must explicitly fetch first revision since no retry on stdout
wr := <-c.Watch(context.TODO(), "", clientv3.WithPrefix(), clientv3.WithRev(1))
if wr.Err() == nil {
wr.CompactRevision = 1
}
if rev := snapshot(os.Stdout, c, wr.CompactRevision+1); rev != 0 {
err := fmt.Errorf("snapshot interrupted by compaction %v", rev)
ExitWithError(ExitInterrupted, err)
}
os.Stdout.Sync()
}
// snapshotToFile atomically writes a snapshot to a file
func snapshotToFile(c *clientv3.Client, path string) {
partpath := path + ".part"
f, err := os.Create(partpath)
defer f.Close()
if err != nil {
exiterr := fmt.Errorf("could not open %s (%v)", partpath, err)
ExitWithError(ExitBadArgs, exiterr)
}
rev := int64(1)
for rev != 0 {
f.Seek(0, 0)
f.Truncate(0)
rev = snapshot(f, c, rev)
}
f.Sync()
if err := os.Rename(partpath, path); err != nil {
exiterr := fmt.Errorf("could not rename %s to %s (%v)", partpath, path, err)
ExitWithError(ExitIO, exiterr)
}
}
// snapshot reads all of a watcher; returns compaction revision if incomplete
// TODO: stabilize snapshot format
func snapshot(w io.Writer, c *clientv3.Client, rev int64) int64 {
s := mirror.NewSyncer(c, "", rev)
rc, errc := s.SyncBase(context.TODO())
for r := range rc {
for _, kv := range r.Kvs {
fmt.Fprintln(w, kv)
}
}
err := <-errc
if err != nil {
if err == rpctypes.ErrCompacted {
// will get correct compact revision on retry
return rev + 1
}
// failed for some unknown reason, retry on same revision
return rev
}
wc := s.SyncUpdates(context.TODO())
for wr := range wc {
if wr.Err() != nil {
return wr.CompactRevision
}
for _, ev := range wr.Events {
fmt.Fprintln(w, ev)
}
rev := wr.Events[len(wr.Events)-1].Kv.ModRevision
if rev >= wr.Header.Revision {
break
}
}
return 0
}

View File

@@ -0,0 +1,203 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"github.com/coreos/etcd/clientv3"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
var (
txnInteractive bool
)
// NewTxnCommand returns the cobra command for "txn".
func NewTxnCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "txn [options]",
Short: "Txn processes all the requests in one transaction.",
Run: txnCommandFunc,
}
cmd.Flags().BoolVarP(&txnInteractive, "interactive", "i", false, "input transaction in interactive mode")
return cmd
}
// txnCommandFunc executes the "txn" command.
func txnCommandFunc(cmd *cobra.Command, args []string) {
if len(args) != 0 {
ExitWithError(ExitBadArgs, fmt.Errorf("txn command does not accept argument."))
}
reader := bufio.NewReader(os.Stdin)
txn := mustClientFromCmd(cmd).Txn(context.Background())
promptInteractive("compares:")
txn.If(readCompares(reader)...)
promptInteractive("success requests (get, put, delete):")
txn.Then(readOps(reader)...)
promptInteractive("failure requests (get, put, delete):")
txn.Else(readOps(reader)...)
resp, err := txn.Commit()
if err != nil {
ExitWithError(ExitError, err)
}
display.Txn(*resp)
}
func promptInteractive(s string) {
if txnInteractive {
fmt.Println(s)
}
}
func readCompares(r *bufio.Reader) (cmps []clientv3.Cmp) {
for {
line, err := r.ReadString('\n')
if err != nil {
ExitWithError(ExitInvalidInput, err)
}
if len(line) == 1 {
break
}
// remove trialling \n
line = line[:len(line)-1]
cmp, err := parseCompare(line)
if err != nil {
ExitWithError(ExitInvalidInput, err)
}
cmps = append(cmps, *cmp)
}
return cmps
}
func readOps(r *bufio.Reader) (ops []clientv3.Op) {
for {
line, err := r.ReadString('\n')
if err != nil {
ExitWithError(ExitInvalidInput, err)
}
if len(line) == 1 {
break
}
// remove trialling \n
line = line[:len(line)-1]
op, err := parseRequestUnion(line)
if err != nil {
ExitWithError(ExitInvalidInput, err)
}
ops = append(ops, *op)
}
return ops
}
func parseRequestUnion(line string) (*clientv3.Op, error) {
args := argify(line)
if len(args) < 2 {
return nil, fmt.Errorf("invalid txn compare request: %s", line)
}
opc := make(chan clientv3.Op, 1)
put := NewPutCommand()
put.Run = func(cmd *cobra.Command, args []string) {
key, value, opts := getPutOp(cmd, args)
opc <- clientv3.OpPut(key, value, opts...)
}
get := NewGetCommand()
get.Run = func(cmd *cobra.Command, args []string) {
key, opts := getGetOp(cmd, args)
opc <- clientv3.OpGet(key, opts...)
}
del := NewDelCommand()
del.Run = func(cmd *cobra.Command, args []string) {
key, opts := getDelOp(cmd, args)
opc <- clientv3.OpDelete(key, opts...)
}
cmds := &cobra.Command{SilenceErrors: true}
cmds.AddCommand(put, get, del)
cmds.SetArgs(args)
if err := cmds.Execute(); err != nil {
return nil, fmt.Errorf("invalid txn request: %s", line)
}
op := <-opc
return &op, nil
}
func parseCompare(line string) (*clientv3.Cmp, error) {
var (
key string
op string
val string
)
lparenSplit := strings.SplitN(line, "(", 2)
if len(lparenSplit) != 2 {
return nil, fmt.Errorf("malformed comparison: %s", line)
}
target := lparenSplit[0]
n, serr := fmt.Sscanf(lparenSplit[1], "%q) %s %q", &key, &op, &val)
if n != 3 {
return nil, fmt.Errorf("malformed comparison: %s; got %s(%q) %s %q", line, target, key, op, val)
}
if serr != nil {
return nil, fmt.Errorf("malformed comparison: %s (%v)", line, serr)
}
var (
v int64
err error
cmp clientv3.Cmp
)
switch target {
case "ver", "version":
if v, err = strconv.ParseInt(val, 10, 64); err == nil {
cmp = clientv3.Compare(clientv3.Version(key), op, v)
}
case "c", "create":
if v, err = strconv.ParseInt(val, 10, 64); err == nil {
cmp = clientv3.Compare(clientv3.CreateRevision(key), op, v)
}
case "m", "mod":
if v, err = strconv.ParseInt(val, 10, 64); err == nil {
cmp = clientv3.Compare(clientv3.ModRevision(key), op, v)
}
case "val", "value":
cmp = clientv3.Compare(clientv3.Value(key), op, val)
default:
return nil, fmt.Errorf("malformed comparison: %s (unknown target %s)", line, target)
}
if err != nil {
return nil, fmt.Errorf("invalid txn compare request: %s", line)
}
return &cmp, nil
}

View File

@@ -0,0 +1,91 @@
// Copyright 2016 Nippon Telegraph and Telephone Corporation.
//
// 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 (
"fmt"
"strings"
"github.com/bgentry/speakeasy"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
// NewUserCommand returns the cobra command for "user".
func NewUserCommand() *cobra.Command {
ac := &cobra.Command{
Use: "user <subcommand>",
Short: "user related command",
}
ac.AddCommand(NewUserAddCommand())
return ac
}
var (
passwordInteractive bool
)
func NewUserAddCommand() *cobra.Command {
cmd := cobra.Command{
Use: "add <user name>",
Short: "add a new user",
Run: userAddCommandFunc,
}
cmd.Flags().BoolVar(&passwordInteractive, "interactive", true, "read password from stdin instead of interactive terminal")
return &cmd
}
// userAddCommandFunc executes the "user add" command.
func userAddCommandFunc(cmd *cobra.Command, args []string) {
if len(args) != 1 {
ExitWithError(ExitBadArgs, fmt.Errorf("user add command requires user name as its argument."))
}
var password string
if !passwordInteractive {
fmt.Scanf("%s", &password)
} else {
prompt1 := fmt.Sprintf("Password of %s: ", args[0])
password1, err1 := speakeasy.Ask(prompt1)
if err1 != nil {
ExitWithError(ExitBadArgs, fmt.Errorf("failed to ask password: %s.", err1))
}
if len(password1) == 0 {
ExitWithError(ExitBadArgs, fmt.Errorf("empty password"))
}
prompt2 := fmt.Sprintf("Type password of %s again for confirmation: ", args[0])
password2, err2 := speakeasy.Ask(prompt2)
if err2 != nil {
ExitWithError(ExitBadArgs, fmt.Errorf("failed to ask password: %s.", err2))
}
if strings.Compare(password1, password2) != 0 {
ExitWithError(ExitBadArgs, fmt.Errorf("given passwords are different."))
}
password = password1
}
_, err := mustClientFromCmd(cmd).Auth.UserAdd(context.TODO(), args[0], password)
if err != nil {
ExitWithError(ExitError, err)
}
}

View File

@@ -0,0 +1,59 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"encoding/hex"
"fmt"
"regexp"
pb "github.com/coreos/etcd/storage/storagepb"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
func printKV(isHex bool, kv *pb.KeyValue) {
k, v := string(kv.Key), string(kv.Value)
if isHex {
k = addHexPrefix(hex.EncodeToString(kv.Key))
v = addHexPrefix(hex.EncodeToString(kv.Value))
}
fmt.Println(k)
fmt.Println(v)
}
func addHexPrefix(s string) string {
ns := make([]byte, len(s)*2)
for i := 0; i < len(s); i += 2 {
ns[i*2] = '\\'
ns[i*2+1] = 'x'
ns[i*2+2] = s[i]
ns[i*2+3] = s[i+1]
}
return string(ns)
}
func argify(s string) []string {
r := regexp.MustCompile("'.+'|\".+\"|\\S+")
return r.FindAllString(s, -1)
}
func commandCtx(cmd *cobra.Command) (context.Context, context.CancelFunc) {
timeOut, err := cmd.Flags().GetDuration("command-timeout")
if err != nil {
ExitWithError(ExitError, err)
}
return context.WithTimeout(context.Background(), timeOut)
}

View File

@@ -0,0 +1,35 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"fmt"
"github.com/coreos/etcd/version"
"github.com/spf13/cobra"
)
// NewVersionCommand prints out the version of etcd.
func NewVersionCommand() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Print the version of etcdctl.",
Run: versionCommandFunc,
}
}
func versionCommandFunc(cmd *cobra.Command, args []string) {
fmt.Println(version.Version)
}

View File

@@ -0,0 +1,126 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 (
"bufio"
"fmt"
"os"
"strings"
"github.com/coreos/etcd/clientv3"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
var (
watchRev int64
watchPrefix bool
watchInteractive bool
)
// NewWatchCommand returns the cobra command for "watch".
func NewWatchCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "watch [key or prefix]",
Short: "Watch watches events stream on keys or prefixes.",
Run: watchCommandFunc,
}
cmd.Flags().BoolVarP(&watchInteractive, "interactive", "i", false, "interactive mode")
cmd.Flags().BoolVar(&watchPrefix, "prefix", false, "watch on a prefix if prefix is set")
cmd.Flags().Int64Var(&watchRev, "rev", 0, "revision to start watching")
return cmd
}
// watchCommandFunc executes the "watch" command.
func watchCommandFunc(cmd *cobra.Command, args []string) {
if watchInteractive {
watchInteractiveFunc(cmd, args)
return
}
if len(args) != 1 {
ExitWithError(ExitBadArgs, fmt.Errorf("watch in non-interactive mode requires an argument as key or prefix"))
}
opts := []clientv3.OpOption{clientv3.WithRev(watchRev)}
if watchPrefix {
opts = append(opts, clientv3.WithPrefix())
}
c := mustClientFromCmd(cmd)
wc := c.Watch(context.TODO(), args[0], opts...)
printWatchCh(wc)
err := c.Close()
if err == nil {
ExitWithError(ExitInterrupted, fmt.Errorf("watch is canceled by the server"))
}
ExitWithError(ExitBadConnection, err)
}
func watchInteractiveFunc(cmd *cobra.Command, args []string) {
c := mustClientFromCmd(cmd)
reader := bufio.NewReader(os.Stdin)
for {
l, err := reader.ReadString('\n')
if err != nil {
ExitWithError(ExitInvalidInput, fmt.Errorf("Error reading watch request line: %v", err))
}
l = strings.TrimSuffix(l, "\n")
args := argify(l)
if len(args) < 2 {
fmt.Fprintf(os.Stderr, "Invalid command %s (command type or key is not provided)\n", l)
continue
}
if args[0] != "watch" {
fmt.Fprintf(os.Stderr, "Invalid command %s (only support watch)\n", l)
continue
}
flagset := NewWatchCommand().Flags()
err = flagset.Parse(args[1:])
if err != nil {
fmt.Fprintf(os.Stderr, "Invalid command %s (%v)\n", l, err)
continue
}
moreargs := flagset.Args()
if len(moreargs) != 1 {
fmt.Fprintf(os.Stderr, "Invalid command %s (Too many arguments)\n", l)
continue
}
var key string
_, err = fmt.Sscanf(moreargs[0], "%q", &key)
if err != nil {
key = moreargs[0]
}
opts := []clientv3.OpOption{clientv3.WithRev(watchRev)}
if watchPrefix {
opts = append(opts, clientv3.WithPrefix())
}
ch := c.Watch(context.TODO(), key, opts...)
go printWatchCh(ch)
}
}
func printWatchCh(ch clientv3.WatchChan) {
for resp := range ch {
display.Watch(resp)
}
}

97
etcdctl/ctlv3/ctl.go Normal file
View File

@@ -0,0 +1,97 @@
// Copyright 2015 CoreOS, Inc.
//
// 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 ctlv3 contains the main entry point for the etcdctl for v3 API.
package ctlv3
import (
"text/tabwriter"
"time"
"github.com/coreos/etcd/etcdctl/ctlv3/command"
"github.com/spf13/cobra"
)
const (
cliName = "etcdctl"
cliDescription = "A simple command line client for etcd3."
defaultDialTimeout = 2 * time.Second
defaultCommandTimeOut = 5 * time.Second
)
var (
tabOut *tabwriter.Writer
globalFlags = command.GlobalFlags{}
)
var (
rootCmd = &cobra.Command{
Use: cliName,
Short: cliDescription,
SuggestFor: []string{"etcdctl"},
}
)
func init() {
rootCmd.PersistentFlags().StringSliceVar(&globalFlags.Endpoints, "endpoints", []string{"127.0.0.1:2379", "127.0.0.1:22379", "127.0.0.1:32379"}, "gRPC endpoints")
rootCmd.PersistentFlags().StringVarP(&globalFlags.OutputFormat, "write-out", "w", "simple", "set the output format (simple, json, protobuf)")
rootCmd.PersistentFlags().BoolVar(&globalFlags.IsHex, "hex", false, "print byte strings as hex encoded strings")
rootCmd.PersistentFlags().DurationVar(&globalFlags.DialTimeout, "dial-timeout", defaultDialTimeout, "dial timeout for client connections")
rootCmd.PersistentFlags().DurationVar(&globalFlags.CommandTimeOut, "command-timeout", defaultCommandTimeOut, "timeout for short running command (excluding dial timeout)")
// TODO: secure by default when etcd enables secure gRPC by default.
rootCmd.PersistentFlags().BoolVar(&globalFlags.Insecure, "insecure-transport", true, "disable transport security for client connections")
rootCmd.PersistentFlags().BoolVar(&globalFlags.InsecureSkipVerify, "insecure-skip-tls-verify", false, "skip server certificate verification")
rootCmd.PersistentFlags().StringVar(&globalFlags.TLS.CertFile, "cert", "", "identify secure client using this TLS certificate file")
rootCmd.PersistentFlags().StringVar(&globalFlags.TLS.KeyFile, "key", "", "identify secure client using this TLS key file")
rootCmd.PersistentFlags().StringVar(&globalFlags.TLS.CAFile, "cacert", "", "verify certificates of TLS-enabled secure servers using this CA bundle")
rootCmd.AddCommand(
command.NewGetCommand(),
command.NewPutCommand(),
command.NewDelCommand(),
command.NewTxnCommand(),
command.NewCompactionCommand(),
command.NewDefragCommand(),
command.NewWatchCommand(),
command.NewVersionCommand(),
command.NewLeaseCommand(),
command.NewMemberCommand(),
command.NewEpHealthCommand(),
command.NewSnapshotCommand(),
command.NewMakeMirrorCommand(),
command.NewLockCommand(),
command.NewAuthCommand(),
command.NewElectCommand(),
command.NewUserCommand(),
)
}
func init() {
cobra.EnablePrefixMatching = true
}
func Start() {
rootCmd.SetUsageFunc(usageFunc)
// Make help just show the usage
rootCmd.SetHelpTemplate(`{{.UsageString}}`)
if err := rootCmd.Execute(); err != nil {
command.ExitWithError(command.ExitError, err)
}
}

166
etcdctl/ctlv3/help.go Normal file
View File

@@ -0,0 +1,166 @@
// Copyright 2015 CoreOS, Inc.
//
// 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.
// copied from https://github.com/coreos/rkt/blob/master/rkt/help.go
package ctlv3
import (
"bytes"
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"text/template"
"github.com/coreos/etcd/version"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
var (
commandUsageTemplate *template.Template
templFuncs = template.FuncMap{
"descToLines": func(s string) []string {
// trim leading/trailing whitespace and split into slice of lines
return strings.Split(strings.Trim(s, "\n\t "), "\n")
},
"cmdName": func(cmd *cobra.Command, startCmd *cobra.Command) string {
parts := []string{cmd.Name()}
for cmd.HasParent() && cmd.Parent().Name() != startCmd.Name() {
cmd = cmd.Parent()
parts = append([]string{cmd.Name()}, parts...)
}
return strings.Join(parts, " ")
},
}
)
func init() {
commandUsage := `
{{ $cmd := .Cmd }}\
{{ $cmdname := cmdName .Cmd .Cmd.Root }}\
NAME:
{{ if not .Cmd.HasParent }}\
{{printf "\t%s - %s" .Cmd.Name .Cmd.Short}}
{{else}}\
{{printf "\t%s - %s" $cmdname .Cmd.Short}}
{{end}}\
USAGE:
{{printf "\t%s" .Cmd.UseLine}}
{{ if not .Cmd.HasParent }}\
VERSION:
{{printf "\t%s" .Version}}
{{end}}\
{{if .Cmd.HasSubCommands}}\
COMMANDS:
{{range .SubCommands}}\
{{ $cmdname := cmdName . $cmd }}\
{{ if .Runnable }}\
{{printf "\t%s\t%s" $cmdname .Short}}
{{end}}\
{{end}}\
{{end}}\
{{ if .Cmd.Long }}\
DESCRIPTION:
{{range $line := descToLines .Cmd.Long}}{{printf "\t%s" $line}}
{{end}}\
{{end}}\
{{if .Cmd.HasLocalFlags}}\
OPTIONS:
{{.LocalFlags}}\
{{end}}\
{{if .Cmd.HasInheritedFlags}}\
GLOBAL OPTIONS:
{{.GlobalFlags}}\
{{end}}
`[1:]
commandUsageTemplate = template.Must(template.New("command_usage").Funcs(templFuncs).Parse(strings.Replace(commandUsage, "\\\n", "", -1)))
}
func etcdFlagUsages(flagSet *pflag.FlagSet) string {
x := new(bytes.Buffer)
flagSet.VisitAll(func(flag *pflag.Flag) {
if len(flag.Deprecated) > 0 {
return
}
format := ""
if len(flag.Shorthand) > 0 {
format = " -%s, --%s"
} else {
format = " %s --%s"
}
if len(flag.NoOptDefVal) > 0 {
format = format + "["
}
if flag.Value.Type() == "string" {
// put quotes on the value
format = format + "=%q"
} else {
format = format + "=%s"
}
if len(flag.NoOptDefVal) > 0 {
format = format + "]"
}
format = format + "\t%s\n"
shorthand := flag.Shorthand
fmt.Fprintf(x, format, shorthand, flag.Name, flag.DefValue, flag.Usage)
})
return x.String()
}
func getSubCommands(cmd *cobra.Command) []*cobra.Command {
var subCommands []*cobra.Command
for _, subCmd := range cmd.Commands() {
subCommands = append(subCommands, subCmd)
subCommands = append(subCommands, getSubCommands(subCmd)...)
}
return subCommands
}
func usageFunc(cmd *cobra.Command) error {
subCommands := getSubCommands(cmd)
tabOut := getTabOutWithWriter(os.Stdout)
commandUsageTemplate.Execute(tabOut, struct {
Cmd *cobra.Command
LocalFlags string
GlobalFlags string
SubCommands []*cobra.Command
Version string
}{
cmd,
etcdFlagUsages(cmd.LocalFlags()),
etcdFlagUsages(cmd.InheritedFlags()),
subCommands,
version.Version,
})
tabOut.Flush()
return nil
}
func getTabOutWithWriter(writer io.Writer) *tabwriter.Writer {
aTabOut := new(tabwriter.Writer)
aTabOut.Init(writer, 0, 8, 1, '\t', 0)
return aTabOut
}

View File

@@ -0,0 +1,29 @@
## Mirror Maker
Mirror maker mirrors a prefix in the key-value space of an etcd cluster into another prefix in another cluster. Mirroring is designed for copying configuration to various clusters distributed around the world. Mirroring usually has very low latency once it completes synchronizing with the initial state. Mirror maker utilizes the etcd watcher facility to immediately inform the mirror of any key modifications. Based on our experiments, the network latency between the mirror maker and the two clusters accounts for most of the latency. If the network is healthy, copying configuration held in etcd to the mirror should take under one second even for a world-wide deployment.
If the mirror maker fails to connect to one of the clusters, the mirroring will pause. Mirroring can be resumed automatically once connectivity is reestablished.
The mirroring mechanism is unidirectional. Data under the destination clusters mirroring prefix should be treated as read only. The mirror maker only mirrors key-value pairs; metadata, such as version number or modification revision, is discarded. However, mirror maker still attempts to preserve update ordering during normal operation, but there is no ordering guarantee during initial sync nor during failure recovery following network interruption. As a rule of thumb, the ordering of the updates on the mirror should not be considered reliable.
```
+-------------+
| |
| source | +-----------+
| cluster +----> | mirror |
| | | maker |
+-------------+ +---+-------+
|
v
+-------------+
| |
| mirror |
| cluster |
| |
+-------------+
```
Mirror-maker is a built-in feature of [etcdctl][etcdctl].
[etcdctl]: ../README.md

View File

@@ -1,4 +1,4 @@
// Copyright 2015 CoreOS, Inc.
// Copyright 2016 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -16,55 +16,31 @@
package main
import (
"fmt"
"os"
"time"
"github.com/codegangsta/cli"
"github.com/coreos/etcd/etcdctl/command"
"github.com/coreos/etcd/version"
"github.com/coreos/etcd/etcdctl/ctlv2"
"github.com/coreos/etcd/etcdctl/ctlv3"
)
const (
apiEnv = "ETCDCTL_API"
)
func main() {
app := cli.NewApp()
app.Name = "etcdctl"
app.Version = version.Version
app.Usage = "A simple command line client for etcd."
app.Flags = []cli.Flag{
cli.BoolFlag{Name: "debug", Usage: "output cURL commands which can be used to reproduce the request"},
cli.BoolFlag{Name: "no-sync", Usage: "don't synchronize cluster information before sending request"},
cli.StringFlag{Name: "output, o", Value: "simple", Usage: "output response in the given format (`simple`, `extended` or `json`)"},
cli.StringFlag{Name: "discovery-srv, D", Usage: "domain name to query for SRV records describing cluster endpoints"},
cli.StringFlag{Name: "peers, C", Value: "", Usage: "DEPRECATED - \"--endpoints\" should be used instead"},
cli.StringFlag{Name: "endpoint", Value: "", Usage: "DEPRECATED - \"--endpoints\" should be used instead"},
cli.StringFlag{Name: "endpoints", Value: "", Usage: "a comma-delimited list of machine addresses in the cluster (default: \"http://127.0.0.1:2379,http://127.0.0.1:4001\")"},
cli.StringFlag{Name: "cert-file", Value: "", Usage: "identify HTTPS client using this SSL certificate file"},
cli.StringFlag{Name: "key-file", Value: "", Usage: "identify HTTPS client using this SSL key file"},
cli.StringFlag{Name: "ca-file", Value: "", Usage: "verify certificates of HTTPS-enabled servers using this CA bundle"},
cli.StringFlag{Name: "username, u", Value: "", Usage: "provide username[:password] and prompt if password is not supplied."},
cli.DurationFlag{Name: "timeout", Value: time.Second, Usage: "connection timeout per request"},
cli.DurationFlag{Name: "total-timeout", Value: 5 * time.Second, Usage: "timeout for the command execution (except watch)"},
}
app.Commands = []cli.Command{
command.NewBackupCommand(),
command.NewClusterHealthCommand(),
command.NewMakeCommand(),
command.NewMakeDirCommand(),
command.NewRemoveCommand(),
command.NewRemoveDirCommand(),
command.NewGetCommand(),
command.NewLsCommand(),
command.NewSetCommand(),
command.NewSetDirCommand(),
command.NewUpdateCommand(),
command.NewUpdateDirCommand(),
command.NewWatchCommand(),
command.NewExecWatchCommand(),
command.NewMemberCommand(),
command.NewImportSnapCommand(),
command.NewUserCommands(),
command.NewRoleCommands(),
command.NewAuthCommands(),
apiv := os.Getenv(apiEnv)
// unset apiEnv to avoid side-effect for future env and flag parsing.
os.Unsetenv(apiv)
if len(apiv) == 0 || apiv == "2" {
ctlv2.Start()
return
}
app.Run(os.Args)
if apiv == "3" {
ctlv3.Start()
return
}
fmt.Fprintln(os.Stderr, "unsupported API version", apiv)
os.Exit(1)
}