mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
*: combine etcdctl and etcdctlv3
This commit is contained in:
391
etcdctl/READMEv3.md
Normal file
391
etcdctl/READMEv3.md
Normal 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
70
etcdctl/ctlv2/ctl.go
Normal 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)
|
||||
}
|
||||
55
etcdctl/ctlv3/command/auth_command.go
Normal file
55
etcdctl/ctlv3/command/auth_command.go
Normal 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)
|
||||
}
|
||||
}
|
||||
53
etcdctl/ctlv3/command/compaction_command.go
Normal file
53
etcdctl/ctlv3/command/compaction_command.go
Normal 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)
|
||||
}
|
||||
45
etcdctl/ctlv3/command/defrag_command.go
Normal file
45
etcdctl/ctlv3/command/defrag_command.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
55
etcdctl/ctlv3/command/del_command.go
Normal file
55
etcdctl/ctlv3/command/del_command.go
Normal 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
|
||||
}
|
||||
16
etcdctl/ctlv3/command/doc.go
Normal file
16
etcdctl/ctlv3/command/doc.go
Normal 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
|
||||
132
etcdctl/ctlv3/command/elect_command.go
Normal file
132
etcdctl/ctlv3/command/elect_command.go
Normal 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()
|
||||
}
|
||||
83
etcdctl/ctlv3/command/ep_health_command.go
Normal file
83
etcdctl/ctlv3/command/ep_health_command.go
Normal 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()
|
||||
}
|
||||
42
etcdctl/ctlv3/command/error.go
Normal file
42
etcdctl/ctlv3/command/error.go
Normal 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)
|
||||
}
|
||||
135
etcdctl/ctlv3/command/get_command.go
Normal file
135
etcdctl/ctlv3/command/get_command.go
Normal 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
|
||||
}
|
||||
199
etcdctl/ctlv3/command/global.go
Normal file
199
etcdctl/ctlv3/command/global.go
Normal 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
|
||||
}
|
||||
139
etcdctl/ctlv3/command/lease_command.go
Normal file
139
etcdctl/ctlv3/command/lease_command.go
Normal 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)
|
||||
}
|
||||
88
etcdctl/ctlv3/command/lock_command.go
Normal file
88
etcdctl/ctlv3/command/lock_command.go
Normal 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")
|
||||
}
|
||||
148
etcdctl/ctlv3/command/make_mirror_command.go
Normal file
148
etcdctl/ctlv3/command/make_mirror_command.go
Normal 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
|
||||
}
|
||||
176
etcdctl/ctlv3/command/member_command.go
Normal file
176
etcdctl/ctlv3/command/member_command.go
Normal 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)
|
||||
}
|
||||
183
etcdctl/ctlv3/command/printer.go
Normal file
183
etcdctl/ctlv3/command/printer.go
Normal 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))
|
||||
}
|
||||
90
etcdctl/ctlv3/command/put_command.go
Normal file
90
etcdctl/ctlv3/command/put_command.go
Normal 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
|
||||
}
|
||||
127
etcdctl/ctlv3/command/snapshot_command.go
Normal file
127
etcdctl/ctlv3/command/snapshot_command.go
Normal 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
|
||||
}
|
||||
203
etcdctl/ctlv3/command/txn_command.go
Normal file
203
etcdctl/ctlv3/command/txn_command.go
Normal 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
|
||||
}
|
||||
91
etcdctl/ctlv3/command/user_command.go
Normal file
91
etcdctl/ctlv3/command/user_command.go
Normal 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)
|
||||
}
|
||||
}
|
||||
59
etcdctl/ctlv3/command/util.go
Normal file
59
etcdctl/ctlv3/command/util.go
Normal 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)
|
||||
}
|
||||
35
etcdctl/ctlv3/command/version_command.go
Normal file
35
etcdctl/ctlv3/command/version_command.go
Normal 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)
|
||||
}
|
||||
126
etcdctl/ctlv3/command/watch_command.go
Normal file
126
etcdctl/ctlv3/command/watch_command.go
Normal 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
97
etcdctl/ctlv3/ctl.go
Normal 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
166
etcdctl/ctlv3/help.go
Normal 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
|
||||
}
|
||||
29
etcdctl/doc/mirror_maker.md
Normal file
29
etcdctl/doc/mirror_maker.md
Normal 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 cluster’s 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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user