From 780d2f2a597542c8ed00d05f5687dae85245b3a9 Mon Sep 17 00:00:00 2001 From: Anthony Romano Date: Mon, 12 Dec 2016 17:03:09 -0800 Subject: [PATCH] etcdctl: tighten up output, reorganize README.md Documentation was far too repetitive, making it a chore to read and make changes. All commands are now organized by functionality and all repetitive bits about return values and output are in a generalized subsections. etcdctl's output handling was missing a lot of commands. Similarly, in many cases an output format could be given but fail to report an error as expected. --- etcdctl/README.md | 1128 ++++++++++----------- etcdctl/ctlv3/command/member_command.go | 7 +- etcdctl/ctlv3/command/printer.go | 444 ++------ etcdctl/ctlv3/command/printer_fields.go | 167 +++ etcdctl/ctlv3/command/printer_json.go | 41 + etcdctl/ctlv3/command/printer_protobuf.go | 64 ++ etcdctl/ctlv3/command/printer_simple.go | 227 +++++ etcdctl/ctlv3/command/printer_table.go | 53 + etcdctl/ctlv3/command/role_command.go | 63 +- etcdctl/ctlv3/command/user_command.go | 40 +- 10 files changed, 1201 insertions(+), 1033 deletions(-) create mode 100644 etcdctl/ctlv3/command/printer_fields.go create mode 100644 etcdctl/ctlv3/command/printer_json.go create mode 100644 etcdctl/ctlv3/command/printer_protobuf.go create mode 100644 etcdctl/ctlv3/command/printer_simple.go create mode 100644 etcdctl/ctlv3/command/printer_table.go diff --git a/etcdctl/README.md b/etcdctl/README.md index 9d3263b91..b49cc5d24 100644 --- a/etcdctl/README.md +++ b/etcdctl/README.md @@ -15,51 +15,23 @@ ETCDCTL_KEY=/tmp/key.pem Prefix flag strings with `ETCDCTL_`, convert all letters to upper-case, and replace dash(`-`) with underscore(`_`). -## Commands - -### VERSION - -Prints the version of etcdctl - -#### Return value - -##### Simple reply - -- Prints etcd version and API version - -#### Examples - -```bash -./etcdctl version -# etcdctl version: 3.1.0-alpha.0+git -# API version: 3.1 -``` +## Key-value commands ### PUT [options] \ \ PUT assigns the specified value with the specified key. If key already holds a value, it is overwritten. +RPC: Put + #### Options - lease -- lease ID (in hexadecimal) to attach to the key. - prev-kv -- return the previous key-value pair before modification. -#### Return value +#### Output -##### 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]. +`OK` #### Examples @@ -81,7 +53,7 @@ The protobuf encoding of the PUT [RPC response][etcdrpc]. # bar1 ``` -#### Notes +#### Remarks If \ isn't given as command line argument, this command tries to read the value from standard input. @@ -97,6 +69,8 @@ Insert '--' for workaround: GET gets the key or a range of keys [key, range_end) if `range-end` is given. +RPC: Range + #### Options - hex -- print out key and value as hex encode string @@ -119,21 +93,9 @@ GET gets the key or a range of keys [key, range_end) if `range-end` is given. - keys-only -- Get only the keys -#### Return value +#### Output -##### Simple reply - -- \\n\\n\\n\... - -- Error string if GET failed. Exit code is non-zero. - -##### JSON reply - -The JSON encoding of the [RPC response][etcdrpc] for the GET's Range request. - -##### Protobuf reply - -The protobuf encoding of the [RPC message][etcdrpc] for a key-value pair for each fetched key-value. +\\n\\n\\n\... #### Examples @@ -163,15 +125,16 @@ The protobuf encoding of the [RPC message][etcdrpc] for a key-value pair for eac # bar2 ``` -#### Notes +#### Remarks -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. +If any key or value contains non-printable characters or control characters, simple formatted output can be ambiguous due to new lines. To resolve this issue, set `--hex` to hex encode all strings. ### DEL [options] \ [range_end] Removes the specified key or range of keys [key, range_end) if `range-end` is given. +RPC: DeleteRange + #### Options - prefix -- delete keys by matching prefix @@ -180,21 +143,9 @@ Removes the specified key or range of keys [key, range_end) if `range-end` is gi - from-key -- delete keys that are greater than or equal to the given key using byte compare -#### Return value +#### Output -##### 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]. +Prints the number of keys that were removed in decimal if DEL succeeded. #### Examples @@ -245,11 +196,13 @@ The protobuf encoding of the DeleteRange [RPC response][etcdrpc]. 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. +RPC: Txn + #### Options -- hex -- print out keys and values as hex encoded string +- hex -- print out keys and values as hex encoded strings. -- interactive -- input transaction with interactive prompting +- interactive -- input transaction with interactive prompting. #### Input Format ```ebnf @@ -269,23 +222,9 @@ A transaction consists of list of conditions, a list of requests to apply if all ::= "\""[0-9]+"\"" ``` -#### Return value +#### Output -##### 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]. +`SUCCESS` if etcd processed the transaction success list, `FAILURE` if etcd processed the transaction failure list. Prints the output for each command in the executed request list, each separated by a blank line. #### Examples @@ -327,10 +266,34 @@ put key2 "some extra key" # OK ``` +### COMPACTION [options] \ + +COMPACTION discards all etcd event history prior to a given revision. Since etcd uses a multiversion concurrency control +model, it preserves all key updates as event history. When the event history up to some revision is no longer needed, +all superseded keys may be compacted away to reclaim storage space in the etcd backend database. + +RPC: Compact + +#### Options + +- physical -- 'true' to wait for compaction to physically remove all old revisions + +#### Output + +Prints the compacted revision. + +#### Example +```bash +./etcdctl compaction 1234 +# compacted revision 1234 +``` + ### WATCH [options] [key or prefix] [range_end] Watch watches events stream on keys or prefixes, [key or prefix, range_end) if `range-end` is given. The watch command runs until it encounters an error or is terminated by the user. If range_end is given, it must be lexicographically greater than key or "\x00". +RPC: Watch + #### Options - hex -- print out key and value as hex encode string @@ -343,7 +306,7 @@ Watch watches events stream on keys or prefixes, [key or prefix, range_end) if ` - rev -- the revision to start watching. Specifying a revision is useful for observing past events. -#### Input Format +#### Input format Input is only accepted for interactive mode. @@ -351,21 +314,9 @@ Input is only accepted for interactive mode. watch [options] \n ``` -#### Return value +#### Output -##### Simple reply - -- \[\n\\n\]\n\\n\\n\\n\\n\\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. +\[\n\\n\]\n\\n\\n\\n\\n\\n... #### Examples @@ -401,11 +352,11 @@ LEASE provides commands for key lease management. LEASE GRANT creates a fresh lease with a server-selected time-to-live in seconds greater than or equal to the requested TTL value. -#### Return value +RPC: LeaseGrant -- On success, prints a message with the granted lease ID. +#### Output -- On failure, prints an error message and returns with a non-zero exit code. +Prints a message with the granted lease ID. #### Example @@ -418,11 +369,11 @@ greater than or equal to the requested TTL value. LEASE REVOKE destroys a given lease, deleting all attached keys. -#### Return value +RPC: LeaseRevoke -- On success, prints a message indicating the lease is revoked. +#### Output -- On failure, prints an error message and returns with a non-zero exit code. +Prints a message indicating the lease is revoked. #### Example @@ -431,20 +382,19 @@ LEASE REVOKE destroys a given lease, deleting all attached keys. # lease 32695410dcc0ca06 revoked ``` - ### LEASE TIMETOLIVE \ [options] LEASE TIMETOLIVE retrieves the lease information with the given lease ID. +RPC: LeaseTimeToLive + #### Options - keys -- Get keys attached to this lease -#### Return value +#### Output -- On success, prints lease information. - -- On failure, prints an error message and returns with a non-zero exit code. +Prints lease information. #### Example @@ -471,16 +421,15 @@ LEASE TIMETOLIVE retrieves the lease information with the given lease ID. # {"cluster_id":17186838941855831277,"member_id":4845372305070271874,"revision":3,"raft_term":2,"id":3279279168933706764,"ttl":459,"granted-ttl":500,"keys":["Zm9vMQ==","Zm9vMg=="]} ``` - ### LEASE KEEP-ALIVE \ LEASE KEEP-ALIVE periodically refreshes a lease so it does not expire. -#### Return value +RPC: LeaseKeepAlive -- On success, prints a message for every keep alive sent. +#### Output -- On failure, returns a non-zero exit code if a keep-alive channel could not be established. Otherwise, prints a message indicating the lease is gone. +Prints a message for every keep alive sent or prints a message indicating the lease is gone. #### Example ```bash @@ -491,6 +440,7 @@ LEASE KEEP-ALIVE periodically refreshes a lease so it does not expire. ... ``` +## Cluster maintenance commands ### MEMBER \ @@ -500,15 +450,15 @@ MEMBER provides commands for managing etcd cluster membership. MEMBER ADD introduces a new member into the etcd cluster as a new peer. +RPC: MemberAdd + #### Options - peer-urls -- comma separated list of URLs to associate with the new member. -#### Return value +#### Output -- On success, prints the member ID of the new member and the cluster ID. - -- On failure, prints an error message and returns with a non-zero exit code. +Prints the member ID of the new member and the cluster ID. #### Example @@ -517,20 +467,19 @@ MEMBER ADD introduces a new member into the etcd cluster as a new peer. # Member 2be1eb8f84b7f63e added to cluster ef37ad9dc622a7c4 ``` - ### MEMBER UPDATE \ [options] MEMBER UPDATE sets the peer URLs for an existing member in the etcd cluster. +RPC: MemberUpdate + #### Options - peer-urls -- comma separated list of URLs to associate with the updated member. -#### Return value +#### Output -- On success, prints the member ID of the updated member and the cluster ID. - -- On failure, prints an error message and returns with a non-zero exit code. +Prints the member ID of the updated member and the cluster ID. #### Example @@ -539,16 +488,15 @@ MEMBER UPDATE sets the peer URLs for an existing member in the etcd cluster. # Member 2be1eb8f84b7f63e updated in cluster ef37ad9dc622a7c4 ``` - ### MEMBER REMOVE \ MEMBER REMOVE removes a member of an etcd cluster from participating in cluster consensus. -#### Return value +RPC: MemberRemove -- On success, prints the member ID of the removed member and the cluster ID. +#### Output -- On failure, prints an error message and returns with a non-zero exit code. +Prints the member ID of the removed member and the cluster ID. #### Example @@ -561,19 +509,11 @@ MEMBER REMOVE removes a member of an etcd cluster from participating in cluster MEMBER LIST prints the member details for all members associated with an etcd cluster. -#### Return value +RPC: [MemberList][member_list_rpc]. -##### Simple reply +#### Output -On success, prints a humanized table of the member IDs, statuses, names, peer addresses, and client addresses. On failure, prints an error message and returns with a non-zero exit code. - -##### JSON reply - -On success, prints a JSON listing of the member IDs, statuses, names, peer addresses, and client addresses. On failure, prints an error message and returns with a non-zero exit code. - -##### Protobuf reply - -The protobuf encoding of the MEMBER LIST [RPC response][member_list_rpc]. +Prints a humanized table of the member IDs, statuses, names, peer addresses, and client addresses. #### Examples @@ -600,8 +540,6 @@ The protobuf encoding of the MEMBER LIST [RPC response][member_list_rpc]. +------------------+---------+--------+------------------------+------------------------+ ``` -## Utility Commands - ### ENDPOINT \ ENDPOINT provides commands for querying individual endpoints. @@ -611,11 +549,9 @@ ENDPOINT provides commands for querying individual endpoints. ENDPOINT HEALTH checks the health of the list of endpoints with respect to cluster. An endpoint is unhealthy when it cannot participate in consensus with the rest of the cluster. -#### Return value +#### Output -- If an endpoint can participate in consensus, prints a message indicating the endpoint is healthy. - -- If an endpoint fails to participate in consensus, prints a message indicating the endpoint is unhealthy. +If an endpoint can participate in consensus, prints a message indicating the endpoint is healthy. If an endpoint fails to participate in consensus, prints a message indicating the endpoint is unhealthy. #### Example @@ -630,19 +566,15 @@ when it cannot participate in consensus with the rest of the cluster. ENDPOINT STATUS queries the status of each endpoint in the given endpoint list. -#### Return value +#### Output -##### Simple reply +##### Simple format -On success, prints a humanized table of each endpoint URL, ID, version, database size, leadership status, raft term, and raft status. On failure, returns with a non-zero exit code. +Prints a humanized table of each endpoint URL, ID, version, database size, leadership status, raft term, and raft status. -##### JSON reply +##### JSON format -On success, prints a line of JSON encoding each endpoint URL, ID, version, database size, leadership status, raft term, and raft status. On failure, returns with a non-zero exit code. - -##### Protobuf reply - -ENDPOINT STATUS does not support protobuf encoded output. +Prints a line of JSON encoding each endpoint URL, ID, version, database size, leadership status, raft term, and raft status. #### Examples @@ -669,81 +601,54 @@ ENDPOINT STATUS does not support protobuf encoded output. +-----------------+------------------+---------+---------+-----------+-----------+------------+ ``` -### LOCK \ +### ALARM \ -LOCK acquires a distributed named mutex with a given name. Once the lock is acquired, it will be held until etcdctl is terminated. +Provides alarm related commands -#### Return value +### ALARM DISARM -- Once the lock is acquired, the result for the GET on the unique lock holder key is displayed. +`alarm disarm` Disarms all alarms -- LOCK returns a zero exit code only if it is terminated by a signal and can release the lock. +RPC: Alarm + +#### Output + +`alarm:` if alarm is present and disarmed. + +#### Examples -#### Example ```bash -./etcdctl lock mylock -# mylock/1234534535445 - - +./etcdctl alarm disarm ``` -#### Notes +If NOSPACE alarm is present: -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] \ [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 +./etcdctl alarm disarm +# alarm:NOSPACE ``` -#### Notes +### ALARM LIST -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. +`alarm list` lists all alarms. +RPC: Alarm -### COMPACTION [options] \ +#### Output -COMPACTION discards all etcd event history prior to a given revision. Since etcd uses a multiversion concurrency control -model, it preserves all key updates as event history. When the event history up to some revision is no longer needed, -all superseded keys may be compacted away to reclaim storage space in the etcd backend database. +`alarm:` if alarm is present, empty string if no alarms present. -#### Options +#### Examples -- physical -- 'true' to wait for compaction to physically remove all old revisions - -#### Return value - -- On success, prints the compacted revision and returns a zero exit code. - -- On failure, prints an error message and returns with a non-zero exit code. - -#### Example ```bash -./etcdctl compaction 1234 -# compacted revision 1234 +./etcdctl alarm list +``` + +If NOSPACE alarm is present: + +```bash +./etcdctl alarm list +# alarm:NOSPACE ``` ### DEFRAG @@ -752,60 +657,21 @@ DEFRAG defragments the backend database file for a set of given endpoints. When from deleted and compacted keys, the space is kept in a free list and the database file remains the same size. By defragmenting the database, the etcd member releases this free space back to the file system. -#### Return value +#### Output -- If successfully defragmented an endpoint, prints a message indicating success for that endpoint. - -- If failed defragmenting an endpoint, prints a message indicating failure for that endpoint. - -- DEFRAG returns a zero exit code only if it succeeded defragmenting all given endpoints. +For each endpoints, prints a message indicating whether the endpoint was successfully defragmented. #### Example + ```bash ./etcdctl --endpoints=localhost:2379,badendpoint:2379 defrag # Finished defragmenting etcd member[localhost:2379] # Failed to defragment etcd member[badendpoint:2379] (grpc: timed out trying to connect) ``` +#### Remarks -### MAKE-MIRROR [options] \ - -[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 - -- dest-prefix -- The destination prefix to mirror a prefix to a different prefix in the destination cluster - -- no-dest-prefix -- Mirror key-values to the root of the destination cluster - -- dest-insecure-transport -- Disable transport security for client connections - -#### 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 - +DEFRAG returns a zero exit code only if it succeeded defragmenting all given endpoints. ### SNAPSHOT \ @@ -815,11 +681,9 @@ SNAPSHOT provides commands to restore a snapshot of a running etcd server into a SNAPSHOT SAVE writes a point-in-time snapshot of the etcd backend database to a file. -#### Return value +#### Output -- On success, the backend snapshot is written to the given file path. - -- Error string if snapshotting failed. Exit code is non-zero. +The backend snapshot is written to the given file path. #### Example @@ -828,7 +692,6 @@ Save a snapshot to "snapshot.db": ./etcdctl snapshot save snapshot.db ``` - ### SNAPSHOT RESTORE [options] \ SNAPSHOT RESTORE creates an etcd data directory for an etcd cluster member from a backend database snapshot and a new cluster configuration. Restoring the snapshot into each member for a new cluster configuration will initialize a new etcd cluster preloaded by the snapshot data. @@ -849,11 +712,9 @@ The snapshot restore options closely resemble to those used in the `etcd` comman - skip-hash-check -- Ignore snapshot integrity hash value (required if copied from data directory) -#### Return value +#### Output -- On success, a new etcd data directory is initialized. - -- Error string if the data directory could not be completely initialized. Exit code is non-zero. +A new etcd data directory initialized with the snapshot. #### Example @@ -876,19 +737,15 @@ bin/etcd --name sshot3 --listen-client-urls http://127.0.0.1:32379 --advertise-c SNAPSHOT STATUS lists information about a given backend database snapshot file. -#### Return value +#### Output -##### Simple Reply +##### Simple format -On success, prints a humanized table of the database hash, revision, total keys, and size. On failure, return with a non-zero exit code. +Prints a humanized table of the database hash, revision, total keys, and size. -##### JSON reply +##### JSON format -On success, prints a line of JSON encoding the database hash, revision, total keys, and size. On failure, return with a non-zero exit code. - -##### Protobuf reply - -SNAPSHOT STATUS does not support protobuf encoded output. +Prints a line of JSON encoding the database hash, revision, total keys, and size. #### Examples ```bash @@ -910,9 +767,390 @@ SNAPSHOT STATUS does not support protobuf encoded output. +----------+----------+------------+------------+ ``` +## Concurrency commands + +### LOCK \ + +LOCK acquires a distributed named mutex with a given name. Once the lock is acquired, it will be held until etcdctl is terminated. + +#### Output + +Once the lock is acquired, the result for the GET on the unique lock holder key is displayed. + +#### Example + +```bash +./etcdctl lock mylock +# mylock/1234534535445 +``` + +#### Remarks + +LOCK returns a zero exit code only if it is terminated by a signal and releases the lock. + +If LOCK is abnormally terminated or fails to contact the cluster to release the lock, the lock will remain held until the lease expires. Progress may be delayed by up to the default lease length of 60 seconds. + +### ELECT [options] \ [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. + +#### Output + +- 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. + +#### Example + +```bash +./etcdctl elect myelection foo +# myelection/1456952310051373265 +# foo +``` + +#### Remarks + +ELECT returns a zero exit code only if it is terminated by a signal and can revoke its candidacy or leadership, if any. + +If a candidate is abnormally terminated, election rogress may be delayed by up to the default lease length of 60 seconds. + +## Authentication commands + +### AUTH \ + +`auth enable` activates authentication on an etcd cluster and `auth disable` deactivates. When authentication is enabled, etcd checks all requests for appropriate authorization. + +RPC: AuthEnable/AuthDisable + +#### Output + +`Authentication Enabled`. + +#### Examples + +```bash +./etcdctl user add root +# Password of root:#type password for root +# Type password of root again for confirmation:#re-type password for root +# User root created +./etcdctl user grant-role root root +# Role root is granted to user root +./etcdctl user get root +# User: root +# Roles: root +./etcdctl role add root +# Role root created +./etcdctl role get root +# Role root +# KV Read: +# KV Write: +./etcdctl auth enable +# Authentication Enabled +``` + +### ROLE \ + +ROLE is used to specify differnt roles which can be assigned to etcd user(s). + +### ROLE ADD \ + +`role add` creates a role. + +RPC: RoleAdd + +#### Output + +`Role created`. + +#### Examples + +```bash +./etcdctl --user=root:123 role add myrole +# Role myrole created +``` + +### ROLE GET \ + +`role get` lists detailed role information. + +RPC: RoleGet + +#### Output + +Detailed role information. + +#### Examples + +```bash +./etcdctl --user=root:123 role get myrole +# Role myrole +# KV Read: +# foo +# KV Write: +# foo +``` + +### ROLE DELETE \ + +`role delete` deletes a role. + +RPC: RoleDelete + +#### Output + +`Role deleted`. + +#### Examples + +```bash +./etcdctl --user=root:123 role delete myrole +# Role myrole deleted +``` + +### ROLE LIST \ + +`role list` lists all roles in etcd. + +RPC: RoleList + +#### Output + +A role per line. + +#### Examples + +```bash +./etcdctl --user=root:123 role list +# roleA +# roleB +# myrole +``` + +### ROLE GRANT-PERMISSION [options] \ \ \ [endkey] + +`role grant-permission` grants a key to a role. + +RPC: RoleGrantPermission + +#### Options + +- prefix -- grant a prefix permission + +#### Ouptut + +`Role updated`. + +#### Examples + +```bash +./etcdctl --user=root:123 role grant-permission myrole readwrite foo +# Role myrole updated +``` + +### ROLE REVOKE-PERMISSION \ \ \ [endkey] + +`role revoke-permission` revokes a key from a role. + +RPC: RoleRevokePermission + +#### Output + +`Permission of key is revoked from role ` for single key. `Permission of range [, ) is revoked from role ` for a key range. Exit code is zero. + +#### Examples + +```bash +./etcdctl --user=root:123 role revoke-permission myrole foo +# Permission of key foo is revoked from role myrole +``` + +### USER \ + +USER provides commands for managing users of etcd. + +### USER ADD \ [options] + +`user add` creates a user. + +RPC: UserAdd + +#### Options + +- interactive -- Read password from stdin instead of interactive terminal + +#### Output + +`User created`. + +#### Examples + +```bash +./etcdctl --user=root:123 user add myuser +# Password of myuser: #type password for my user +# Type password of myuser again for confirmation:#re-type password for my user +# User myuser created +``` + +### USER GET \ [options] + +`user get` lists detailed user information. + +RPC: UserGet + +#### Options + +- detail -- Show permissions of roles granted to the user + +#### Output + +Detailed user information. + +#### Examples + +```bash +./etcdctl --user=root:123 user get myuser +# User: myuser +# Roles: +``` + +### USER DELETE \ + +`user delete` deletes a user. + +RPC: UserDelete + +#### Output + +`User deleted`. + +#### Examples + +```bash +./etcdctl --user=root:123 user delete myuser +# User myuser deleted +``` + +### USER LIST + +`user list` lists detailed user information. + +RPC: UserList + +#### Output + +- List of users, one per line. + +#### Examples + +```bash +./etcdctl --user=root:123 user list +# user1 +# user2 +# myuser +``` + +### USER PASSWD \ [options] + +`user passwd` changes a user's password. + +RPC: UserChangePassword + +#### Options + +- interactive -- if true, read password in interactive terminal + +#### Output + +`Password updated`. + +#### Examples + +```bash +./etcdctl --user=root:123 user passwd myuser +# Password of myuser: #type new password for my user +# Type password of myuser again for confirmation: #re-type the new password for my user +# Password updated +``` + +### USER GRANT-ROLE \ \ + +`user grant-role` grants a role to a user + +RPC: UserGrantRole + +#### Output + +`Role is granted to user `. + +#### Examples + +```bash +./etcdctl --user=root:123 user grant-role userA roleA +# Role roleA is granted to user userA +``` + +### USER REVOKE-ROLE \ \ + +`user revoke-role` revokes a role from a user + +RPC: UserRevokeRole + +#### Output + +`Role is revoked from user `. + +#### Examples + +```bash +./etcdctl --user=root:123 user revoke-role userA roleA +# Role roleA is revoked from user userA +``` + +## Utility commands + +### MAKE-MIRROR [options] \ + +[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 + +- dest-prefix -- The destination prefix to mirror a prefix to a different prefix in the destination cluster + +- no-dest-prefix -- Mirror key-values to the root of the destination cluster + +- dest-insecure-transport -- Disable transport security for client connections + +#### Output + +The approximate total number of keys transferred to the destination cluster, updated every 30 seconds. + +#### Examples + +``` +./etcdctl make-mirror mirror.example.com:2379 +# 10 +# 18 +``` + +[mirror]: ./doc/mirror_maker.md + ### MIGRATE [options] -Migrate migrates keys in a v2 store to a mvcc store. Users should run migration command for all members in the cluster. +Migrates keys in a v2 store to a v3 mvcc store. Users should run migration command for all members in the cluster. #### Options @@ -922,13 +1160,9 @@ Migrate migrates keys in a v2 store to a mvcc store. Users should run migration - transformer -- Path to the user-provided transformer program (default if not provided) -#### Return value +#### Output -Simple reply - -- Exit code is zero when migration is finished successfully. - -- Error string if migration failed. Exit code is non-zero. +No output on success. #### Default transformer @@ -963,344 +1197,49 @@ The provided transformer should read until EOF and flush the stdout before exiti # finished transforming keys ``` -### ROLE \ +### VERSION -ROLE is used to specify differnt roles which can be assigned to etcd user(s). +Prints the version of etcdctl. -### ROLE ADD \ +#### Output -`role add` creates a role. - -#### Return value - -##### Simple reply - -- `Role created`. Exit code is zero. - -- Error string if failed. Exit code is non-zero. +Prints etcd version and API version. #### Examples ```bash -./etcdctl --user=root:123 role add myrole -# Role myrole created +./etcdctl version +# etcdctl version: 3.1.0-alpha.0+git +# API version: 3.1 ``` -### ROLE GET \ +## Exit codes -`role get` lists detailed role information. +For all commands, a successful execution return a zero exit code. All failures will return non-zero exit codes. -#### Return value +## Output formats -##### Simple reply +All commands accept an output format by setting `-w` or `--write-out`. All commands default to the "simple" output format, which is meant to be human-readable. The simple format is listed in each command's `Output` description since it is customized for each command. If a command has a corresponding RPC, it will respect all output formats. -- Detailed role information. Exit code is zero. +If a command fails, returning a non-zero exit code, an error string will be written to standard error regardless of output format. -- Error string if failed. Exit code is non-zero. +### Simple -#### Examples +A format meant to be easy to parse and human-readable. Specific to each command. -```bash -./etcdctl --user=root:123 role get myrole -# Role myrole -# KV Read: -# foo -# KV Write: -# foo -``` +### JSON -### ROLE GRANT-PERMISSION [options] \ \ \ [endkey] +The JSON encoding of the command's [RPC response][etcdrpc]. Since etcd's RPCs use byte strings, the JSON output will encode keys and values in base64. -`role grant-permission` grants a key to a role. +Some commands without an RPC also support JSON; see the command's `Output` description. -#### Options +### Protobuf -- prefix -- grant a prefix permission +The protobuf encoding of the command's [RPC response][etcdrpc]. If an RPC is streaming, the stream messages will be concetenated. If an RPC is not given for a command, the protobuf output is not defined. -#### Return value +### Fields -##### Simple reply - -- `Role updated`. Exit code is zero. - -- Error string if failed. Exit code is non-zero. - -#### Examples - -```bash -./etcdctl --user=root:123 role grant-permission myrole readwrite foo -# Role myrole updated -``` - -### ROLE REVOKE-PERMISSION \ \ \ [endkey] - -`role revoke-permission` revokes a key from a role. - -#### Return value - -##### Simple reply - -- `Permission of key is revoked from role `. Exit code is zero. - -- Error string if failed. Exit code is non-zero. - -#### Examples - -```bash -./etcdctl --user=root:123 role revoke-permission myrole foo -# Permission of key foo is revoked from role myrole -``` - -### ROLE DELETE \ - -`role delete` deletes a role. - -#### Return value - -##### Simple reply - -- `Role deleted`. Exit code is zero. - -- Error string if failed. Exit code is non-zero. - -#### Examples - -```bash -./etcdctl --user=root:123 role delete myrole -# Role myrole deleted -``` - -### USER \ - -USER provides commands for managing users of etcd. - -### USER ADD \ [options] - -`user add` creates a user. - -#### Options - -- interactive -- Read password from stdin instead of interactive terminal - -#### Return value - -##### Simple reply - -- `User created`. Exit code is zero. - -- Error string if failed. Exit code is non-zero. - -#### Examples - -```bash -./etcdctl --user=root:123 user add myuser -# Password of myuser: #type password for my user -# Type password of myuser again for confirmation:#re-type password for my user -# User myuser created -``` - -### USER GET \ [options] - -`user get` lists detailed user information. - -#### Options - -- detail -- Show permissions of roles granted to the user - -#### Return value - -##### Simple reply - -- Detailed user information. Exit code is zero. - -- Error string if failed. Exit code is non-zero. - -#### Examples - -```bash -./etcdctl --user=root:123 user get myuser -# User: myuser -# Roles: -``` - -### USER PASSWD \ [options] - -`user passwd` changes a user's password. - -#### Options - -- interactive -- if true, read password in interactive terminal - -#### Return value - -##### Simple reply - -- `Password updated`. Exit code is zero. - -- Error string if failed. Exit code is non-zero. - -#### Examples - -```bash -./etcdctl --user=root:123 user passwd myuser -# Password of myuser: #type new password for my user -# Type password of myuser again for confirmation: #re-type the new password for my user -# Password updated -``` - -### USER GRANT-ROLE \ \ - -`user grant-role` grants a role to a user - -#### Return value - -##### Simple reply - -- `Role is granted to user `. Exit code is zero. - -- Error string if failed. Exit code is non-zero. - -#### Examples - -```bash -./etcdctl --user=root:123 user grant-role userA roleA -# Role roleA is granted to user userA -``` - -### USER REVOKE-ROLE \ \ - -`user revoke-role` revokes a role from a user - -#### Return value - -##### Simple reply - -- `Role is revoked from user `. Exit code is zero. - -- Error string if failed. Exit code is non-zero. - -#### Examples - -```bash -./etcdctl --user=root:123 user revoke-role userA roleA -# Role roleA is revoked from user userA -``` - -### USER DELETE \ - -`user delete` deletes a user. - -#### Return value - -##### Simple reply - -- `User deleted`. Exit code is zero. - -- Error string if failed. Exit code is non-zero. - -#### Examples - -```bash -./etcdctl --user=root:123 user delete myuser -# User myuser deleted -``` - -### AUTH \ - -`auth enable` activates authentication on an etcd cluster and `auth disable` deactivates. When authentication is enabled, etcd checks all requests for appropriate authorization. - -#### Return value - -##### Simple reply - -- `Authentication Enabled`. Exit code is zero. - -- Error string if AUTH failed. Exit code is non-zero. - -#### Examples - -```bash -./etcdctl user add root -# Password of root:#type password for root -# Type password of root again for confirmation:#re-type password for root -# User root created -./etcdctl user grant-role root root -# Role root is granted to user root -./etcdctl user get root -# User: root -# Roles: root -./etcdctl role add root -# Role root created -./etcdctl role get root -# Role root -# KV Read: -# KV Write: -./etcdctl auth enable -# Authentication Enabled -``` - -## ALARM \ - -Provides alarm related commands - -### ALARM DISARM - -`alarm disarm` Disarms all alarms - -#### Return value - -##### Simple reply - -- `alarm:` if alarm is present - -- `` if alarm is not present - -#### Examples - -```bash -./etcdctl alarm disarm -``` - -If NOSPACE alarm is present: - -```bash -./etcdctl alarm disarm -# alarm:NOSPACE -``` - -### ALARM LIST - -`alarm list` Lists all alarms - -#### Return value - -##### Simple reply - -- `alarm:` if alarm is present - -- `` if alarm is not present - -#### Examples - -```bash -./etcdctl alarm list -``` - -If NOSPACE alarm is present: - -```bash -./etcdctl alarm list -# alarm:NOSPACE -``` - -## Notes - -- JSON encoding for keys and values uses base64 since they are byte strings. - -- -[etcdrpc]: ../etcdserver/etcdserverpb/rpc.proto -[storagerpc]: ../mvcc/mvccpb/kv.proto -[member_list_rpc]: ../etcdserver/etcdserverpb/rpc.proto#L493-L497 +An output format similar to JSON but meant to parse with coreutils. For an integer field named `Field`, it writes a line in the format `"Field" : %d` where `%d` is go's integer formatting. For byte array fields, it writes `"Field" : %q` where `%q` is go's quoted string formatting (e.g., `[]byte{'a', '\n'}` is written as `"a\n"`). ## Compatibility Support @@ -1322,3 +1261,6 @@ backward compatibility for `JSON` format and the format in non-interactive mode. [READMEv2]: READMEv2.md [v2key]: ../store/node_extern.go#L28-L37 [v3key]: ../mvcc/mvccpb/kv.proto#L12-L29 +[etcdrpc]: ../etcdserver/etcdserverpb/rpc.proto +[storagerpc]: ../mvcc/mvccpb/kv.proto +[member_list_rpc]: ../etcdserver/etcdserverpb/rpc.proto#L493-L497 diff --git a/etcdctl/ctlv3/command/member_command.go b/etcdctl/ctlv3/command/member_command.go index e1bf4994e..24682867f 100644 --- a/etcdctl/ctlv3/command/member_command.go +++ b/etcdctl/ctlv3/command/member_command.go @@ -112,7 +112,7 @@ func memberAddCommandFunc(cmd *cobra.Command, args []string) { ExitWithError(ExitError, err) } - fmt.Printf("Member %16x added to cluster %16x\n", resp.Member.ID, resp.Header.ClusterId) + display.MemberAdd(*resp) } // memberRemoveCommandFunc executes the "member remove" command. @@ -132,8 +132,7 @@ func memberRemoveCommandFunc(cmd *cobra.Command, args []string) { if err != nil { ExitWithError(ExitError, err) } - - fmt.Printf("Member %16x removed from cluster %16x\n", id, resp.Header.ClusterId) + display.MemberRemove(id, *resp) } // memberUpdateCommandFunc executes the "member update" command. @@ -160,7 +159,7 @@ func memberUpdateCommandFunc(cmd *cobra.Command, args []string) { ExitWithError(ExitError, err) } - fmt.Printf("Member %16x updated in cluster %16x\n", id, resp.Header.ClusterId) + display.MemberUpdate(id, *resp) } // memberListCommandFunc executes the "member list" command. diff --git a/etcdctl/ctlv3/command/printer.go b/etcdctl/ctlv3/command/printer.go index f5ad15d6d..6df3bfe5c 100644 --- a/etcdctl/ctlv3/command/printer.go +++ b/etcdctl/ctlv3/command/printer.go @@ -15,17 +15,14 @@ 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/mvcc/mvccpb" "github.com/dustin/go-humanize" - "github.com/olekukonko/tablewriter" + + pb "github.com/coreos/etcd/etcdserver/etcdserverpb" ) type printer interface { @@ -37,12 +34,30 @@ type printer interface { TimeToLive(r v3.LeaseTimeToLiveResponse, keys bool) + MemberAdd(v3.MemberAddResponse) + MemberRemove(id uint64, r v3.MemberRemoveResponse) + MemberUpdate(id uint64, r v3.MemberUpdateResponse) MemberList(v3.MemberListResponse) EndpointStatus([]epStatus) Alarm(v3.AlarmResponse) DBStatus(dbstatus) + + RoleAdd(role string, r v3.AuthRoleAddResponse) + RoleGet(role string, r v3.AuthRoleGetResponse) + RoleDelete(role string, r v3.AuthRoleDeleteResponse) + RoleList(v3.AuthRoleListResponse) + RoleGrantPermission(role string, r v3.AuthRoleGrantPermissionResponse) + RoleRevokePermission(role string, key string, end string, r v3.AuthRoleRevokePermissionResponse) + + UserAdd(user string, r v3.AuthUserAddResponse) + UserGet(user string, r v3.AuthUserGetResponse) + UserList(r v3.AuthUserListResponse) + UserChangePassword(v3.AuthUserChangePasswordResponse) + UserGrantRole(user string, role string, r v3.AuthUserGrantRoleResponse) + UserRevokeRole(user string, role string, r v3.AuthUserRevokeRoleResponse) + UserDelete(user string, r v3.AuthUserDeleteResponse) } func NewPrinter(printerType string, isHex bool) printer { @@ -50,17 +65,78 @@ func NewPrinter(printerType string, isHex bool) printer { case "simple": return &simplePrinter{isHex: isHex} case "fields": - return &fieldsPrinter{} + return &fieldsPrinter{newPrinterUnsupported("fields")} case "json": - return &jsonPrinter{} + return newJSONPrinter() case "protobuf": - return &pbPrinter{} + return newPBPrinter() case "table": - return &tablePrinter{} + return &tablePrinter{newPrinterUnsupported("table")} } return nil } +type printerRPC struct { + printer + p func(interface{}) +} + +func (p *printerRPC) Del(r v3.DeleteResponse) { p.p((*pb.DeleteRangeResponse)(&r)) } +func (p *printerRPC) Get(r v3.GetResponse) { p.p((*pb.RangeResponse)(&r)) } +func (p *printerRPC) Put(r v3.PutResponse) { p.p((*pb.PutResponse)(&r)) } +func (p *printerRPC) Txn(r v3.TxnResponse) { p.p((*pb.TxnResponse)(&r)) } +func (p *printerRPC) Watch(r v3.WatchResponse) { p.p(&r) } +func (p *printerRPC) TimeToLive(r v3.LeaseTimeToLiveResponse, keys bool) { p.p(&r) } +func (p *printerRPC) MemberAdd(r v3.MemberAddResponse) { p.p((*pb.MemberAddResponse)(&r)) } +func (p *printerRPC) MemberRemove(id uint64, r v3.MemberRemoveResponse) { + p.p((*pb.MemberRemoveResponse)(&r)) +} +func (p *printerRPC) MemberUpdate(id uint64, r v3.MemberUpdateResponse) { + p.p((*pb.MemberUpdateResponse)(&r)) +} +func (p *printerRPC) MemberList(r v3.MemberListResponse) { p.p((*pb.MemberListResponse)(&r)) } +func (p *printerRPC) Alarm(r v3.AlarmResponse) { p.p((*pb.AlarmResponse)(&r)) } + +func (p *printerRPC) RoleAdd(_ string, r v3.AuthRoleAddResponse) { p.p((*pb.AuthRoleAddResponse)(&r)) } +func (p *printerRPC) RoleGet(_ string, r v3.AuthRoleGetResponse) { p.p((*pb.AuthRoleGetResponse)(&r)) } +func (p *printerRPC) RoleDelete(_ string, r v3.AuthRoleDeleteResponse) { + p.p((*pb.AuthRoleDeleteResponse)(&r)) +} +func (p *printerRPC) RoleList(r v3.AuthRoleListResponse) { p.p((*pb.AuthRoleListResponse)(&r)) } +func (p *printerRPC) RoleGrantPermission(_ string, r v3.AuthRoleGrantPermissionResponse) { + p.p((*pb.AuthRoleGrantPermissionResponse)(&r)) +} +func (p *printerRPC) RoleRevokePermission(_ string, _ string, _ string, r v3.AuthRoleRevokePermissionResponse) { + p.p((*pb.AuthRoleRevokePermissionResponse)(&r)) +} +func (p *printerRPC) UserAdd(_ string, r v3.AuthUserAddResponse) { p.p((*pb.AuthUserAddResponse)(&r)) } +func (p *printerRPC) UserGet(_ string, r v3.AuthUserGetResponse) { p.p((*pb.AuthUserGetResponse)(&r)) } +func (p *printerRPC) UserList(r v3.AuthUserListResponse) { p.p((*pb.AuthUserListResponse)(&r)) } +func (p *printerRPC) UserChangePassword(r v3.AuthUserChangePasswordResponse) { + p.p((*pb.AuthUserChangePasswordResponse)(&r)) +} +func (p *printerRPC) UserGrantRole(_ string, _ string, r v3.AuthUserGrantRoleResponse) { + p.p((*pb.AuthUserGrantRoleResponse)(&r)) +} +func (p *printerRPC) UserRevokeRole(_ string, _ string, r v3.AuthUserRevokeRoleResponse) { + p.p((*pb.AuthUserRevokeRoleResponse)(&r)) +} +func (p *printerRPC) UserDelete(_ string, r v3.AuthUserDeleteResponse) { + p.p((*pb.AuthUserDeleteResponse)(&r)) +} + +type printerUnsupported struct{ printerRPC } + +func newPrinterUnsupported(n string) printer { + f := func(interface{}) { + ExitWithError(ExitBadFeature, errors.New(n+" not supported as output format")) + } + return &printerUnsupported{printerRPC{nil, f}} +} + +func (p *printerUnsupported) EndpointStatus([]epStatus) { p.p(nil) } +func (p *printerUnsupported) DBStatus(dbstatus) { p.p(nil) } + func makeMemberListTable(r v3.MemberListResponse) (hdr []string, rows [][]string) { hdr = []string{"ID", "Status", "Name", "Peer Addrs", "Client Addrs"} for _, m := range r.Members { @@ -105,353 +181,3 @@ func makeDBStatusTable(ds dbstatus) (hdr []string, rows [][]string) { }) return } - -type simplePrinter struct { - isHex bool - valueOnly bool -} - -func (s *simplePrinter) Del(resp v3.DeleteResponse) { - fmt.Println(resp.Deleted) - for _, kv := range resp.PrevKvs { - printKV(s.isHex, s.valueOnly, kv) - } -} - -func (s *simplePrinter) Get(resp v3.GetResponse) { - for _, kv := range resp.Kvs { - printKV(s.isHex, s.valueOnly, kv) - } -} - -func (s *simplePrinter) Put(r v3.PutResponse) { - fmt.Println("OK") - if r.PrevKv != nil { - printKV(s.isHex, s.valueOnly, r.PrevKv) - } -} - -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.ResponseOp_ResponseDeleteRange: - s.Del((v3.DeleteResponse)(*v.ResponseDeleteRange)) - case *pb.ResponseOp_ResponsePut: - s.Put((v3.PutResponse)(*v.ResponsePut)) - case *pb.ResponseOp_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) - if e.PrevKv != nil { - printKV(s.isHex, s.valueOnly, e.PrevKv) - } - printKV(s.isHex, s.valueOnly, e.Kv) - } -} - -func (s *simplePrinter) TimeToLive(resp v3.LeaseTimeToLiveResponse, keys bool) { - txt := fmt.Sprintf("lease %016x granted with TTL(%ds), remaining(%ds)", resp.ID, resp.GrantedTTL, resp.TTL) - if keys { - ks := make([]string, len(resp.Keys)) - for i := range resp.Keys { - ks[i] = string(resp.Keys[i]) - } - txt += fmt.Sprintf(", attached keys(%v)", ks) - } - fmt.Println(txt) -} - -func (s *simplePrinter) Alarm(resp v3.AlarmResponse) { - for _, e := range resp.Alarms { - fmt.Printf("%+v\n", e) - } -} - -func (s *simplePrinter) MemberList(resp v3.MemberListResponse) { - _, rows := makeMemberListTable(resp) - for _, row := range rows { - fmt.Println(strings.Join(row, ", ")) - } -} - -func (s *simplePrinter) EndpointStatus(statusList []epStatus) { - _, rows := makeEndpointStatusTable(statusList) - for _, row := range rows { - fmt.Println(strings.Join(row, ", ")) - } -} - -func (s *simplePrinter) DBStatus(ds dbstatus) { - _, rows := makeDBStatusTable(ds) - for _, row := range rows { - fmt.Println(strings.Join(row, ", ")) - } -} - -type tablePrinter struct{} - -func (tp *tablePrinter) Del(r v3.DeleteResponse) { - ExitWithError(ExitBadFeature, errors.New("table is not supported as output format")) -} -func (tp *tablePrinter) Get(r v3.GetResponse) { - ExitWithError(ExitBadFeature, errors.New("table is not supported as output format")) -} -func (tp *tablePrinter) Put(r v3.PutResponse) { - ExitWithError(ExitBadFeature, errors.New("table is not supported as output format")) -} -func (tp *tablePrinter) Txn(r v3.TxnResponse) { - ExitWithError(ExitBadFeature, errors.New("table is not supported as output format")) -} -func (tp *tablePrinter) Watch(r v3.WatchResponse) { - ExitWithError(ExitBadFeature, errors.New("table is not supported as output format")) -} -func (tp *tablePrinter) TimeToLive(r v3.LeaseTimeToLiveResponse, keys bool) { - ExitWithError(ExitBadFeature, errors.New("table is not supported as output format")) -} -func (tp *tablePrinter) Alarm(r v3.AlarmResponse) { - ExitWithError(ExitBadFeature, errors.New("table is not supported as output format")) -} -func (tp *tablePrinter) MemberList(r v3.MemberListResponse) { - hdr, rows := makeMemberListTable(r) - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader(hdr) - for _, row := range rows { - table.Append(row) - } - table.Render() -} -func (tp *tablePrinter) EndpointStatus(r []epStatus) { - hdr, rows := makeEndpointStatusTable(r) - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader(hdr) - for _, row := range rows { - table.Append(row) - } - table.Render() -} -func (tp *tablePrinter) DBStatus(r dbstatus) { - hdr, rows := makeDBStatusTable(r) - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader(hdr) - for _, row := range rows { - table.Append(row) - } - table.Render() -} - -type jsonPrinter struct{} - -func (p *jsonPrinter) Del(r v3.DeleteResponse) { printJSON(r) } -func (p *jsonPrinter) Get(r v3.GetResponse) { printJSON(r) } -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) TimeToLive(r v3.LeaseTimeToLiveResponse, keys bool) { printJSON(r) } -func (p *jsonPrinter) Alarm(r v3.AlarmResponse) { printJSON(r) } -func (p *jsonPrinter) MemberList(r v3.MemberListResponse) { printJSON(r) } -func (p *jsonPrinter) EndpointStatus(r []epStatus) { printJSON(r) } -func (p *jsonPrinter) DBStatus(r dbstatus) { 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 (p *pbPrinter) TimeToLive(r v3.LeaseTimeToLiveResponse, keys bool) { - ExitWithError(ExitBadFeature, errors.New("only support simple or json as output format")) -} - -func (p *pbPrinter) Alarm(r v3.AlarmResponse) { - printPB((*pb.AlarmResponse)(&r)) -} - -func (p *pbPrinter) MemberList(r v3.MemberListResponse) { - printPB((*pb.MemberListResponse)(&r)) -} - -func (p *pbPrinter) EndpointStatus(statusList []epStatus) { - ExitWithError(ExitBadFeature, errors.New("only support simple or json as output format")) -} - -func (p *pbPrinter) DBStatus(r dbstatus) { - 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)) -} - -type fieldsPrinter struct{} - -func (p *fieldsPrinter) kv(pfx string, kv *spb.KeyValue) { - fmt.Printf("\"%sKey\" : %q\n", pfx, string(kv.Key)) - fmt.Printf("\"%sCreateRevision\" : %d\n", pfx, kv.CreateRevision) - fmt.Printf("\"%sModRevision\" : %d\n", pfx, kv.ModRevision) - fmt.Printf("\"%sVersion\" : %d\n", pfx, kv.Version) - fmt.Printf("\"%sValue\" : %q\n", pfx, string(kv.Value)) - fmt.Printf("\"%sLease\" : %d\n", pfx, string(kv.Lease)) -} - -func (p *fieldsPrinter) hdr(h *pb.ResponseHeader) { - fmt.Println(`"ClusterID" :`, h.ClusterId) - fmt.Println(`"MemberID" :`, h.MemberId) - fmt.Println(`"Revision" :`, h.Revision) - fmt.Println(`"RaftTerm" :`, h.RaftTerm) -} - -func (p *fieldsPrinter) Del(r v3.DeleteResponse) { - p.hdr(r.Header) - fmt.Println(`"Deleted" :`, r.Deleted) - for _, kv := range r.PrevKvs { - p.kv("Prev", kv) - } -} - -func (p *fieldsPrinter) Get(r v3.GetResponse) { - p.hdr(r.Header) - for _, kv := range r.Kvs { - p.kv("", kv) - } - fmt.Println(`"More" :`, r.More) - fmt.Println(`"Count" :`, r.Count) -} - -func (p *fieldsPrinter) Put(r v3.PutResponse) { - p.hdr(r.Header) - if r.PrevKv != nil { - p.kv("Prev", r.PrevKv) - } -} - -func (p *fieldsPrinter) Txn(r v3.TxnResponse) { - p.hdr(r.Header) - fmt.Println(`"Succeeded" :`, r.Succeeded) - for _, resp := range r.Responses { - switch v := resp.Response.(type) { - case *pb.ResponseOp_ResponseDeleteRange: - p.Del((v3.DeleteResponse)(*v.ResponseDeleteRange)) - case *pb.ResponseOp_ResponsePut: - p.Put((v3.PutResponse)(*v.ResponsePut)) - case *pb.ResponseOp_ResponseRange: - p.Get((v3.GetResponse)(*v.ResponseRange)) - default: - fmt.Printf("\"Unknown\" : %q\n", fmt.Sprintf("%+v", v)) - } - } -} - -func (p *fieldsPrinter) Watch(resp v3.WatchResponse) { - p.hdr(&resp.Header) - for _, e := range resp.Events { - fmt.Println(`"Type" : `, e.Type) - if e.PrevKv != nil { - p.kv("Prev", e.PrevKv) - } - p.kv("", e.Kv) - } -} - -func (p *fieldsPrinter) TimeToLive(r v3.LeaseTimeToLiveResponse, keys bool) { - p.hdr(r.ResponseHeader) - fmt.Println(`"ID" :`, r.ID) - fmt.Println(`"TTL" :`, r.TTL) - fmt.Println(`"GrantedTTL" :`, r.GrantedTTL) - for _, k := range r.Keys { - fmt.Printf("\"Key\" : %q\n", string(k)) - } -} - -func (p *fieldsPrinter) MemberList(r v3.MemberListResponse) { - p.hdr(r.Header) - for _, m := range r.Members { - fmt.Println(`"ID" :`, m.ID) - fmt.Printf("\"Name\" : %q\n", m.Name) - for _, u := range m.PeerURLs { - fmt.Printf("\"PeerURL\" : %q\n", u) - } - for _, u := range m.ClientURLs { - fmt.Printf("\"ClientURL\" : %q\n", u) - } - fmt.Println() - } -} - -func (p *fieldsPrinter) EndpointStatus(eps []epStatus) { - for _, ep := range eps { - p.hdr(ep.Resp.Header) - fmt.Printf("\"Version\" : %q\n", ep.Resp.Version) - fmt.Println(`"DBSize" :"`, ep.Resp.DbSize) - fmt.Println(`"Leader" :"`, ep.Resp.Leader) - fmt.Println(`"RaftIndex" :"`, ep.Resp.RaftIndex) - fmt.Println(`"RaftTerm" :"`, ep.Resp.RaftTerm) - fmt.Printf("\"Endpoint\" : %q\n", ep.Ep) - fmt.Println() - } -} - -func (p *fieldsPrinter) Alarm(r v3.AlarmResponse) { - p.hdr(r.Header) - for _, a := range r.Alarms { - fmt.Println(`"MemberID" :`, a.MemberID) - fmt.Println(`"AlarmType" :`, a.Alarm) - fmt.Println() - } -} - -func (p *fieldsPrinter) DBStatus(r dbstatus) { - fmt.Println(`"Hash" :`, r.Hash) - fmt.Println(`"Revision" :`, r.Revision) - fmt.Println(`"Keys" :`, r.TotalKey) - fmt.Println(`"Size" :`, r.TotalSize) -} diff --git a/etcdctl/ctlv3/command/printer_fields.go b/etcdctl/ctlv3/command/printer_fields.go new file mode 100644 index 000000000..c67fa484c --- /dev/null +++ b/etcdctl/ctlv3/command/printer_fields.go @@ -0,0 +1,167 @@ +// Copyright 2016 The etcd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "fmt" + + v3 "github.com/coreos/etcd/clientv3" + pb "github.com/coreos/etcd/etcdserver/etcdserverpb" + spb "github.com/coreos/etcd/mvcc/mvccpb" +) + +type fieldsPrinter struct{ printer } + +func (p *fieldsPrinter) kv(pfx string, kv *spb.KeyValue) { + fmt.Printf("\"%sKey\" : %q\n", pfx, string(kv.Key)) + fmt.Printf("\"%sCreateRevision\" : %d\n", pfx, kv.CreateRevision) + fmt.Printf("\"%sModRevision\" : %d\n", pfx, kv.ModRevision) + fmt.Printf("\"%sVersion\" : %d\n", pfx, kv.Version) + fmt.Printf("\"%sValue\" : %q\n", pfx, string(kv.Value)) + fmt.Printf("\"%sLease\" : %d\n", pfx, string(kv.Lease)) +} + +func (p *fieldsPrinter) hdr(h *pb.ResponseHeader) { + fmt.Println(`"ClusterID" :`, h.ClusterId) + fmt.Println(`"MemberID" :`, h.MemberId) + fmt.Println(`"Revision" :`, h.Revision) + fmt.Println(`"RaftTerm" :`, h.RaftTerm) +} + +func (p *fieldsPrinter) Del(r v3.DeleteResponse) { + p.hdr(r.Header) + fmt.Println(`"Deleted" :`, r.Deleted) + for _, kv := range r.PrevKvs { + p.kv("Prev", kv) + } +} + +func (p *fieldsPrinter) Get(r v3.GetResponse) { + p.hdr(r.Header) + for _, kv := range r.Kvs { + p.kv("", kv) + } + fmt.Println(`"More" :`, r.More) + fmt.Println(`"Count" :`, r.Count) +} + +func (p *fieldsPrinter) Put(r v3.PutResponse) { + p.hdr(r.Header) + if r.PrevKv != nil { + p.kv("Prev", r.PrevKv) + } +} + +func (p *fieldsPrinter) Txn(r v3.TxnResponse) { + p.hdr(r.Header) + fmt.Println(`"Succeeded" :`, r.Succeeded) + for _, resp := range r.Responses { + switch v := resp.Response.(type) { + case *pb.ResponseOp_ResponseDeleteRange: + p.Del((v3.DeleteResponse)(*v.ResponseDeleteRange)) + case *pb.ResponseOp_ResponsePut: + p.Put((v3.PutResponse)(*v.ResponsePut)) + case *pb.ResponseOp_ResponseRange: + p.Get((v3.GetResponse)(*v.ResponseRange)) + default: + fmt.Printf("\"Unknown\" : %q\n", fmt.Sprintf("%+v", v)) + } + } +} + +func (p *fieldsPrinter) Watch(resp v3.WatchResponse) { + p.hdr(&resp.Header) + for _, e := range resp.Events { + fmt.Println(`"Type" :`, e.Type) + if e.PrevKv != nil { + p.kv("Prev", e.PrevKv) + } + p.kv("", e.Kv) + } +} + +func (p *fieldsPrinter) TimeToLive(r v3.LeaseTimeToLiveResponse, keys bool) { + p.hdr(r.ResponseHeader) + fmt.Println(`"ID" :`, r.ID) + fmt.Println(`"TTL" :`, r.TTL) + fmt.Println(`"GrantedTTL" :`, r.GrantedTTL) + for _, k := range r.Keys { + fmt.Printf("\"Key\" : %q\n", string(k)) + } +} + +func (p *fieldsPrinter) MemberList(r v3.MemberListResponse) { + p.hdr(r.Header) + for _, m := range r.Members { + fmt.Println(`"ID" :`, m.ID) + fmt.Printf("\"Name\" : %q\n", m.Name) + for _, u := range m.PeerURLs { + fmt.Printf("\"PeerURL\" : %q\n", u) + } + for _, u := range m.ClientURLs { + fmt.Printf("\"ClientURL\" : %q\n", u) + } + fmt.Println() + } +} + +func (p *fieldsPrinter) EndpointStatus(eps []epStatus) { + for _, ep := range eps { + p.hdr(ep.Resp.Header) + fmt.Printf("\"Version\" : %q\n", ep.Resp.Version) + fmt.Println(`"DBSize" :"`, ep.Resp.DbSize) + fmt.Println(`"Leader" :"`, ep.Resp.Leader) + fmt.Println(`"RaftIndex" :"`, ep.Resp.RaftIndex) + fmt.Println(`"RaftTerm" :"`, ep.Resp.RaftTerm) + fmt.Printf("\"Endpoint\" : %q\n", ep.Ep) + fmt.Println() + } +} + +func (p *fieldsPrinter) Alarm(r v3.AlarmResponse) { + p.hdr(r.Header) + for _, a := range r.Alarms { + fmt.Println(`"MemberID" :`, a.MemberID) + fmt.Println(`"AlarmType" :`, a.Alarm) + fmt.Println() + } +} + +func (p *fieldsPrinter) DBStatus(r dbstatus) { + fmt.Println(`"Hash" :`, r.Hash) + fmt.Println(`"Revision" :`, r.Revision) + fmt.Println(`"Keys" :`, r.TotalKey) + fmt.Println(`"Size" :`, r.TotalSize) +} + +func (p *fieldsPrinter) RoleAdd(role string, r v3.AuthRoleAddResponse) { p.hdr(r.Header) } +func (p *fieldsPrinter) RoleGet(role string, r v3.AuthRoleGetResponse) { p.hdr(r.Header) } +func (p *fieldsPrinter) RoleDelete(role string, r v3.AuthRoleDeleteResponse) { p.hdr(r.Header) } +func (p *fieldsPrinter) RoleList(r v3.AuthRoleListResponse) { p.hdr(r.Header) } +func (p *fieldsPrinter) RoleGrantPermission(role string, r v3.AuthRoleGrantPermissionResponse) { + p.hdr(r.Header) +} +func (p *fieldsPrinter) RoleRevokePermission(role string, key string, end string, r v3.AuthRoleRevokePermissionResponse) { + p.hdr(r.Header) +} +func (p *fieldsPrinter) UserAdd(user string, r v3.AuthUserAddResponse) { p.hdr(r.Header) } +func (p *fieldsPrinter) UserChangePassword(r v3.AuthUserChangePasswordResponse) { p.hdr(r.Header) } +func (p *fieldsPrinter) UserGrantRole(user string, role string, r v3.AuthUserGrantRoleResponse) { + p.hdr(r.Header) +} +func (p *fieldsPrinter) UserRevokeRole(user string, role string, r v3.AuthUserRevokeRoleResponse) { + p.hdr(r.Header) +} +func (p *fieldsPrinter) UserDelete(user string, r v3.AuthUserDeleteResponse) { p.hdr(r.Header) } diff --git a/etcdctl/ctlv3/command/printer_json.go b/etcdctl/ctlv3/command/printer_json.go new file mode 100644 index 000000000..d5d884e59 --- /dev/null +++ b/etcdctl/ctlv3/command/printer_json.go @@ -0,0 +1,41 @@ +// Copyright 2016 The etcd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "encoding/json" + "fmt" + "os" +) + +type jsonPrinter struct{ printer } + +func newJSONPrinter() printer { + return &jsonPrinter{ + &printerRPC{newPrinterUnsupported("json"), printJSON}, + } +} + +func (p *jsonPrinter) EndpointStatus(r []epStatus) { printJSON(r) } +func (p *jsonPrinter) DBStatus(r dbstatus) { 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)) +} diff --git a/etcdctl/ctlv3/command/printer_protobuf.go b/etcdctl/ctlv3/command/printer_protobuf.go new file mode 100644 index 000000000..d9acd4582 --- /dev/null +++ b/etcdctl/ctlv3/command/printer_protobuf.go @@ -0,0 +1,64 @@ +// Copyright 2016 The etcd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "fmt" + "os" + + v3 "github.com/coreos/etcd/clientv3" + pb "github.com/coreos/etcd/etcdserver/etcdserverpb" + mvccpb "github.com/coreos/etcd/mvcc/mvccpb" +) + +type pbPrinter struct{ printer } + +type pbMarshal interface { + Marshal() ([]byte, error) +} + +func newPBPrinter() printer { + return &pbPrinter{ + &printerRPC{newPrinterUnsupported("protobuf"), printPB}, + } +} + +func (p *pbPrinter) Watch(r v3.WatchResponse) { + evs := make([]*mvccpb.Event, len(r.Events)) + for i, ev := range r.Events { + evs[i] = (*mvccpb.Event)(ev) + } + wr := pb.WatchResponse{ + Header: &r.Header, + Events: evs, + CompactRevision: r.CompactRevision, + Canceled: r.Canceled, + Created: r.Created, + } + printPB(&wr) +} + +func printPB(v interface{}) { + m, ok := v.(pbMarshal) + if !ok { + ExitWithError(ExitBadFeature, fmt.Errorf("marshal unsupported for type %T (%v)", v, v)) + } + b, err := m.Marshal() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + return + } + fmt.Printf(string(b)) +} diff --git a/etcdctl/ctlv3/command/printer_simple.go b/etcdctl/ctlv3/command/printer_simple.go new file mode 100644 index 000000000..2d0bf1a3e --- /dev/null +++ b/etcdctl/ctlv3/command/printer_simple.go @@ -0,0 +1,227 @@ +// Copyright 2016 The etcd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "fmt" + "strings" + + v3 "github.com/coreos/etcd/clientv3" + pb "github.com/coreos/etcd/etcdserver/etcdserverpb" +) + +type simplePrinter struct { + isHex bool + valueOnly bool +} + +func (s *simplePrinter) Del(resp v3.DeleteResponse) { + fmt.Println(resp.Deleted) + for _, kv := range resp.PrevKvs { + printKV(s.isHex, s.valueOnly, kv) + } +} + +func (s *simplePrinter) Get(resp v3.GetResponse) { + for _, kv := range resp.Kvs { + printKV(s.isHex, s.valueOnly, kv) + } +} + +func (s *simplePrinter) Put(r v3.PutResponse) { + fmt.Println("OK") + if r.PrevKv != nil { + printKV(s.isHex, s.valueOnly, r.PrevKv) + } +} + +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.ResponseOp_ResponseDeleteRange: + s.Del((v3.DeleteResponse)(*v.ResponseDeleteRange)) + case *pb.ResponseOp_ResponsePut: + s.Put((v3.PutResponse)(*v.ResponsePut)) + case *pb.ResponseOp_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) + if e.PrevKv != nil { + printKV(s.isHex, s.valueOnly, e.PrevKv) + } + printKV(s.isHex, s.valueOnly, e.Kv) + } +} + +func (s *simplePrinter) TimeToLive(resp v3.LeaseTimeToLiveResponse, keys bool) { + txt := fmt.Sprintf("lease %016x granted with TTL(%ds), remaining(%ds)", resp.ID, resp.GrantedTTL, resp.TTL) + if keys { + ks := make([]string, len(resp.Keys)) + for i := range resp.Keys { + ks[i] = string(resp.Keys[i]) + } + txt += fmt.Sprintf(", attached keys(%v)", ks) + } + fmt.Println(txt) +} + +func (s *simplePrinter) Alarm(resp v3.AlarmResponse) { + for _, e := range resp.Alarms { + fmt.Printf("%+v\n", e) + } +} + +func (s *simplePrinter) MemberAdd(r v3.MemberAddResponse) { + fmt.Printf("Member %16x added to cluster %16x\n", r.Member.ID, r.Header.ClusterId) +} + +func (s *simplePrinter) MemberRemove(id uint64, r v3.MemberRemoveResponse) { + fmt.Printf("Member %16x removed from cluster %16x\n", id, r.Header.ClusterId) +} + +func (s *simplePrinter) MemberUpdate(id uint64, r v3.MemberUpdateResponse) { + fmt.Printf("Member %16x updated in cluster %16x\n", id, r.Header.ClusterId) +} + +func (s *simplePrinter) MemberList(resp v3.MemberListResponse) { + _, rows := makeMemberListTable(resp) + for _, row := range rows { + fmt.Println(strings.Join(row, ", ")) + } +} + +func (s *simplePrinter) EndpointStatus(statusList []epStatus) { + _, rows := makeEndpointStatusTable(statusList) + for _, row := range rows { + fmt.Println(strings.Join(row, ", ")) + } +} + +func (s *simplePrinter) DBStatus(ds dbstatus) { + _, rows := makeDBStatusTable(ds) + for _, row := range rows { + fmt.Println(strings.Join(row, ", ")) + } +} + +func (s *simplePrinter) RoleAdd(role string, r v3.AuthRoleAddResponse) { + fmt.Printf("Role %s created\n", role) +} + +func (s *simplePrinter) RoleGet(role string, r v3.AuthRoleGetResponse) { + fmt.Printf("Role %s\n", role) + fmt.Println("KV Read:") + + printRange := func(perm *v3.Permission) { + sKey := string(perm.Key) + sRangeEnd := string(perm.RangeEnd) + fmt.Printf("\t[%s, %s)", sKey, sRangeEnd) + if strings.Compare(v3.GetPrefixRangeEnd(sKey), sRangeEnd) == 0 { + fmt.Printf(" (prefix %s)", sKey) + } + fmt.Printf("\n") + } + + for _, perm := range r.Perm { + if perm.PermType == v3.PermRead || perm.PermType == v3.PermReadWrite { + if len(perm.RangeEnd) == 0 { + fmt.Printf("\t%s\n", string(perm.Key)) + } else { + printRange((*v3.Permission)(perm)) + } + } + } + fmt.Println("KV Write:") + for _, perm := range r.Perm { + if perm.PermType == v3.PermWrite || perm.PermType == v3.PermReadWrite { + if len(perm.RangeEnd) == 0 { + fmt.Printf("\t%s\n", string(perm.Key)) + } else { + printRange((*v3.Permission)(perm)) + } + } + } +} + +func (s *simplePrinter) RoleList(r v3.AuthRoleListResponse) { + for _, role := range r.Roles { + fmt.Printf("%s\n", role) + } +} + +func (s *simplePrinter) RoleDelete(role string, r v3.AuthRoleDeleteResponse) { + fmt.Printf("Role %s deleted\n", role) +} + +func (s *simplePrinter) RoleGrantPermission(role string, r v3.AuthRoleGrantPermissionResponse) { + fmt.Printf("Role %s updated\n", role) +} + +func (s *simplePrinter) RoleRevokePermission(role string, key string, end string, r v3.AuthRoleRevokePermissionResponse) { + if len(end) == 0 { + fmt.Printf("Permission of key %s is revoked from role %s\n", key, role) + return + } + fmt.Printf("Permission of range [%s, %s) is revoked from role %s\n", key, end, role) +} + +func (s *simplePrinter) UserAdd(name string, r v3.AuthUserAddResponse) { + fmt.Printf("User %s created\n", name) +} + +func (s *simplePrinter) UserGet(name string, r v3.AuthUserGetResponse) { + fmt.Printf("User: %s\n", name) + fmt.Printf("Roles:") + for _, role := range r.Roles { + fmt.Printf(" %s", role) + } + fmt.Printf("\n") +} + +func (s *simplePrinter) UserChangePassword(v3.AuthUserChangePasswordResponse) { + fmt.Println("Password updated") +} + +func (s *simplePrinter) UserGrantRole(user string, role string, r v3.AuthUserGrantRoleResponse) { + fmt.Printf("Role %s is granted to user %s\n", role, user) +} + +func (s *simplePrinter) UserRevokeRole(user string, role string, r v3.AuthUserRevokeRoleResponse) { + fmt.Printf("Role %s is revoked from user %s\n", role, user) +} + +func (s *simplePrinter) UserDelete(user string, r v3.AuthUserDeleteResponse) { + fmt.Printf("User %s deleted\n", user) +} + +func (s *simplePrinter) UserList(r v3.AuthUserListResponse) { + for _, user := range r.Users { + fmt.Printf("%s\n", user) + } +} diff --git a/etcdctl/ctlv3/command/printer_table.go b/etcdctl/ctlv3/command/printer_table.go new file mode 100644 index 000000000..5bcf8763d --- /dev/null +++ b/etcdctl/ctlv3/command/printer_table.go @@ -0,0 +1,53 @@ +// Copyright 2016 The etcd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "os" + + "github.com/olekukonko/tablewriter" + + v3 "github.com/coreos/etcd/clientv3" +) + +type tablePrinter struct{ printer } + +func (tp *tablePrinter) MemberList(r v3.MemberListResponse) { + hdr, rows := makeMemberListTable(r) + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader(hdr) + for _, row := range rows { + table.Append(row) + } + table.Render() +} +func (tp *tablePrinter) EndpointStatus(r []epStatus) { + hdr, rows := makeEndpointStatusTable(r) + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader(hdr) + for _, row := range rows { + table.Append(row) + } + table.Render() +} +func (tp *tablePrinter) DBStatus(r dbstatus) { + hdr, rows := makeDBStatusTable(r) + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader(hdr) + for _, row := range rows { + table.Append(row) + } + table.Render() +} diff --git a/etcdctl/ctlv3/command/role_command.go b/etcdctl/ctlv3/command/role_command.go index 46234e58f..28d12f245 100644 --- a/etcdctl/ctlv3/command/role_command.go +++ b/etcdctl/ctlv3/command/role_command.go @@ -16,7 +16,6 @@ package command import ( "fmt" - "strings" "github.com/coreos/etcd/clientv3" "github.com/spf13/cobra" @@ -102,12 +101,12 @@ func roleAddCommandFunc(cmd *cobra.Command, args []string) { ExitWithError(ExitBadArgs, fmt.Errorf("role add command requires role name as its argument.")) } - _, err := mustClientFromCmd(cmd).Auth.RoleAdd(context.TODO(), args[0]) + resp, err := mustClientFromCmd(cmd).Auth.RoleAdd(context.TODO(), args[0]) if err != nil { ExitWithError(ExitError, err) } - fmt.Printf("Role %s created\n", args[0]) + display.RoleAdd(args[0], *resp) } // roleDeleteCommandFunc executes the "role delete" command. @@ -116,47 +115,12 @@ func roleDeleteCommandFunc(cmd *cobra.Command, args []string) { ExitWithError(ExitBadArgs, fmt.Errorf("role delete command requires role name as its argument.")) } - _, err := mustClientFromCmd(cmd).Auth.RoleDelete(context.TODO(), args[0]) + resp, err := mustClientFromCmd(cmd).Auth.RoleDelete(context.TODO(), args[0]) if err != nil { ExitWithError(ExitError, err) } - fmt.Printf("Role %s deleted\n", args[0]) -} - -func printRolePermissions(name string, resp *clientv3.AuthRoleGetResponse) { - fmt.Printf("Role %s\n", name) - fmt.Println("KV Read:") - - printRange := func(perm *clientv3.Permission) { - sKey := string(perm.Key) - sRangeEnd := string(perm.RangeEnd) - fmt.Printf("\t[%s, %s)", sKey, sRangeEnd) - if strings.Compare(clientv3.GetPrefixRangeEnd(sKey), sRangeEnd) == 0 { - fmt.Printf(" (prefix %s)", sKey) - } - fmt.Printf("\n") - } - - for _, perm := range resp.Perm { - if perm.PermType == clientv3.PermRead || perm.PermType == clientv3.PermReadWrite { - if len(perm.RangeEnd) == 0 { - fmt.Printf("\t%s\n", string(perm.Key)) - } else { - printRange((*clientv3.Permission)(perm)) - } - } - } - fmt.Println("KV Write:") - for _, perm := range resp.Perm { - if perm.PermType == clientv3.PermWrite || perm.PermType == clientv3.PermReadWrite { - if len(perm.RangeEnd) == 0 { - fmt.Printf("\t%s\n", string(perm.Key)) - } else { - printRange((*clientv3.Permission)(perm)) - } - } - } + display.RoleDelete(args[0], *resp) } // roleGetCommandFunc executes the "role get" command. @@ -171,7 +135,7 @@ func roleGetCommandFunc(cmd *cobra.Command, args []string) { ExitWithError(ExitError, err) } - printRolePermissions(name, resp) + display.RoleGet(name, *resp) } // roleListCommandFunc executes the "role list" command. @@ -185,9 +149,7 @@ func roleListCommandFunc(cmd *cobra.Command, args []string) { ExitWithError(ExitError, err) } - for _, role := range resp.Roles { - fmt.Printf("%s\n", role) - } + display.RoleList(*resp) } // roleGrantPermissionCommandFunc executes the "role grant-permission" command. @@ -211,12 +173,12 @@ func roleGrantPermissionCommandFunc(cmd *cobra.Command, args []string) { rangeEnd = clientv3.GetPrefixRangeEnd(args[2]) } - _, err = mustClientFromCmd(cmd).Auth.RoleGrantPermission(context.TODO(), args[0], args[2], rangeEnd, perm) + resp, err := mustClientFromCmd(cmd).Auth.RoleGrantPermission(context.TODO(), args[0], args[2], rangeEnd, perm) if err != nil { ExitWithError(ExitError, err) } - fmt.Printf("Role %s updated\n", args[0]) + display.RoleGrantPermission(args[0], *resp) } // roleRevokePermissionCommandFunc executes the "role revoke-permission" command. @@ -230,14 +192,9 @@ func roleRevokePermissionCommandFunc(cmd *cobra.Command, args []string) { rangeEnd = args[2] } - _, err := mustClientFromCmd(cmd).Auth.RoleRevokePermission(context.TODO(), args[0], args[1], rangeEnd) + resp, err := mustClientFromCmd(cmd).Auth.RoleRevokePermission(context.TODO(), args[0], args[1], rangeEnd) if err != nil { ExitWithError(ExitError, err) } - - if len(rangeEnd) == 0 { - fmt.Printf("Permission of key %s is revoked from role %s\n", args[1], args[0]) - } else { - fmt.Printf("Permission of range [%s, %s) is revoked from role %s\n", args[1], rangeEnd, args[0]) - } + display.RoleRevokePermission(args[0], args[1], rangeEnd, *resp) } diff --git a/etcdctl/ctlv3/command/user_command.go b/etcdctl/ctlv3/command/user_command.go index b9fa3958d..8a157e2a6 100644 --- a/etcdctl/ctlv3/command/user_command.go +++ b/etcdctl/ctlv3/command/user_command.go @@ -142,12 +142,12 @@ func userAddCommandFunc(cmd *cobra.Command, args []string) { } } - _, err := mustClientFromCmd(cmd).Auth.UserAdd(context.TODO(), user, password) + resp, err := mustClientFromCmd(cmd).Auth.UserAdd(context.TODO(), user, password) if err != nil { ExitWithError(ExitError, err) } - fmt.Printf("User %s created\n", user) + display.UserAdd(user, *resp) } // userDeleteCommandFunc executes the "user delete" command. @@ -156,12 +156,11 @@ func userDeleteCommandFunc(cmd *cobra.Command, args []string) { ExitWithError(ExitBadArgs, fmt.Errorf("user delete command requires user name as its argument.")) } - _, err := mustClientFromCmd(cmd).Auth.UserDelete(context.TODO(), args[0]) + resp, err := mustClientFromCmd(cmd).Auth.UserDelete(context.TODO(), args[0]) if err != nil { ExitWithError(ExitError, err) } - - fmt.Printf("User %s deleted\n", args[0]) + display.UserDelete(args[0], *resp) } // userGetCommandFunc executes the "user get" command. @@ -177,23 +176,18 @@ func userGetCommandFunc(cmd *cobra.Command, args []string) { ExitWithError(ExitError, err) } - fmt.Printf("User: %s\n", name) - if !userShowDetail { - fmt.Printf("Roles:") - for _, role := range resp.Roles { - fmt.Printf(" %s", role) - } - fmt.Printf("\n") - } else { + if userShowDetail { + fmt.Printf("User: %s\n", name) for _, role := range resp.Roles { fmt.Printf("\n") roleResp, err := client.Auth.RoleGet(context.TODO(), role) if err != nil { ExitWithError(ExitError, err) } - - printRolePermissions(role, roleResp) + display.RoleGet(role, *roleResp) } + } else { + display.UserGet(name, *resp) } } @@ -208,9 +202,7 @@ func userListCommandFunc(cmd *cobra.Command, args []string) { ExitWithError(ExitError, err) } - for _, user := range resp.Users { - fmt.Printf("%s\n", user) - } + display.UserList(*resp) } // userChangePasswordCommandFunc executes the "user passwd" command. @@ -227,12 +219,12 @@ func userChangePasswordCommandFunc(cmd *cobra.Command, args []string) { password = readPasswordInteractive(args[0]) } - _, err := mustClientFromCmd(cmd).Auth.UserChangePassword(context.TODO(), args[0], password) + resp, err := mustClientFromCmd(cmd).Auth.UserChangePassword(context.TODO(), args[0], password) if err != nil { ExitWithError(ExitError, err) } - fmt.Println("Password updated") + display.UserChangePassword(*resp) } // userGrantRoleCommandFunc executes the "user grant-role" command. @@ -241,12 +233,12 @@ func userGrantRoleCommandFunc(cmd *cobra.Command, args []string) { ExitWithError(ExitBadArgs, fmt.Errorf("user grant command requires user name and role name as its argument.")) } - _, err := mustClientFromCmd(cmd).Auth.UserGrantRole(context.TODO(), args[0], args[1]) + resp, err := mustClientFromCmd(cmd).Auth.UserGrantRole(context.TODO(), args[0], args[1]) if err != nil { ExitWithError(ExitError, err) } - fmt.Printf("Role %s is granted to user %s\n", args[1], args[0]) + display.UserGrantRole(args[0], args[1], *resp) } // userRevokeRoleCommandFunc executes the "user revoke-role" command. @@ -255,12 +247,12 @@ func userRevokeRoleCommandFunc(cmd *cobra.Command, args []string) { ExitWithError(ExitBadArgs, fmt.Errorf("user revoke-role requires user name and role name as its argument.")) } - _, err := mustClientFromCmd(cmd).Auth.UserRevokeRole(context.TODO(), args[0], args[1]) + resp, err := mustClientFromCmd(cmd).Auth.UserRevokeRole(context.TODO(), args[0], args[1]) if err != nil { ExitWithError(ExitError, err) } - fmt.Printf("Role %s is revoked from user %s\n", args[1], args[0]) + display.UserRevokeRole(args[0], args[1], *resp) } func readPasswordInteractive(name string) string {