Compare commits

...

439 Commits

Author SHA1 Message Date
a817ca705b Merge pull request #1726 from xiang90/fix_sender
etcdserver: add buffer to the sender queue
2014-11-14 16:13:32 -08:00
fd512ffe6d version: bump to alpha.3 2014-11-14 15:51:01 -08:00
7c4b84a6cd etcdserver: add buffer to the sender queue 2014-11-14 15:18:16 -08:00
ac5a282003 Merge pull request #1720 from xiang90/sender_stop
*: gracefully stop etcdserver
2014-11-14 14:16:39 -08:00
8bf71d796e *: gracefully stop etcdserver 2014-11-14 14:12:24 -08:00
4e251f8624 Merge pull request #1578 from barakmich/bcm_migrate
migrate: Add a migration tool to go from etcd v0.4 -> v0.5 data directories
2014-11-14 16:58:22 -05:00
192f200d9e Fix up migration tool, add snapshot migration
Fixes all updates since bcwaldon sketched the original, with cleanup and
into an acutal working state. The commit log follows:

fix pb reference and remove unused file post rebase

unbreak the migrate folder

correctly detect node IDs

fix snapshotting

Fix previous broken snapshot

Add raft log entries to the translation; fix test for all timezones. (Still in progress, but passing)

Fix etcd:join and etcd:remove

print more data when dumping the log

Cleanup based on yichengq's comments

more comments

Fix the commited index based on the snapshot, if one exists

detect nodeIDs from snapshot

add initial tool documentation and match the semantics in the build script and main

formalize migration doc

rename function and clarify docs

fix nil pointer

fix the record conversion test

add migration to test suite and fix govet
2014-11-14 16:46:08 -05:00
5ea1f2d96f etcd4: migration from v0.4 -> v0.5 2014-11-14 15:57:26 -05:00
c36abeabd1 etcdserver: export Member.StoreKey 2014-11-14 15:57:26 -05:00
b6887e4a83 Merge pull request #1719 from yichengq/228
etcdserver: recover snapshot before applying committed entries
2014-11-14 12:18:41 -08:00
77433ff6da etcdserver: recover cluster when receiving newer snapshot 2014-11-14 12:11:21 -08:00
dfaa7290c4 etcdserver: check and select committed entries to apply 2014-11-14 12:11:16 -08:00
f6a7f96967 etcdserver: recover from snapshot before applying requests 2014-11-14 12:08:39 -08:00
7d0ffb3f12 raft: not set applied when restored from snapshot
applied is only updated by application level through Advance.
2014-11-14 12:08:39 -08:00
1f7198855b Merge pull request #1703 from jonboulle/proc
*: fix Procfile
2014-11-14 08:39:08 -08:00
6f7fd89ba2 Merge pull request #1718 from yichengq/226
sender: support elegant stop
2014-11-13 21:29:02 -08:00
12dba7d413 sender: support elegant stop 2014-11-13 17:44:36 -08:00
e66bda957b Merge pull request #1714 from xiang90/stop
StopNotify
2014-11-13 15:16:52 -08:00
6a1fe00615 Merge pull request #1704 from xiang90/print_config
*: print out configuration when necessary
2014-11-13 14:35:50 -08:00
11f392bdc8 Merge pull request #1708 from yichengq/223
etcdserver: validate new node is not registered before in best effort
2014-11-13 14:30:40 -08:00
b5d480f17a etcdserver: add StopNotify 2014-11-13 14:16:48 -08:00
978d0f1ca1 etcdserver: fix TestDoProposalStopped test
We start etcd server in this test without the cluster. Sometimes it panics when
accessing the cluster. Most of the time it does not panic, since we can stop the
server fast enough before applying the first configuration change entry.
2014-11-13 14:08:59 -08:00
fb344bc33f etcdserver: minor cleanup 2014-11-13 14:01:56 -08:00
813ff6ba48 Merge pull request #1713 from xiang90/stop
etcdserver: fix server.Stop()
2014-11-13 13:58:07 -08:00
ac907d746b etcdserver: validate new node is not registered before in best effort 2014-11-13 13:56:11 -08:00
30dfdb0ea9 etcdserver: fix server.Stop()
Stop should be idempotent. It should simply send a stop signal to the server.
It is the server's responsibility to stop the go-routines and related components.
2014-11-13 13:47:12 -08:00
8c4494a39d Merge pull request #1711 from bdarnell/stable-restart
raft: Only call stableTo when we have ready entries or a snapshot.
2014-11-13 12:49:19 -08:00
9716def94b Merge pull request #1700 from yichengq/222
etcdserver: add sender tests
2014-11-13 12:37:29 -08:00
d89bf9f215 etcdserver: add sender tests 2014-11-13 12:29:25 -08:00
32824e053c raft: Only call stableTo when we have ready entries or a snapshot.
The first Ready after RestartNode (with no snapshot) will have no
unstable entries, so we don't have the correct prevLastUnstablei
when Advance is called. This would cause raftLog.unstable to move
backwards and previously-stable entries would be returned to
the application again.

This should have been caught by the "unexpected Ready" portion of
TestNodeRestart, but it went unnoticed because the Node's goroutine
takes some time to read from advancec and prepare the write to read to
readyc. Added a small (1ms) delay to all such tests to ensure that the
goroutine has time to enter its select wait.
2014-11-13 14:57:01 -05:00
8319d4dcbe Merge pull request #1709 from xiang90/server_id
etcdserver: add ID() function to the Server interface.
2014-11-13 11:37:39 -08:00
d6f40acc86 etcdserver: add ID() function to the Server interface. 2014-11-13 11:37:06 -08:00
92096dfdc3 *: print out configuration when necessary 2014-11-13 10:46:42 -08:00
a551b75d96 Merge pull request #1707 from xiang90/wait_pkg
pkg/wait: move wait to pkg/wait
2014-11-13 09:31:34 -08:00
0d18a0f381 pkg/wait: move wait to pkg/wait 2014-11-13 09:11:53 -08:00
23b5bc0dfe sender: use RoundTripper instead of Client in sender 2014-11-12 21:42:08 -08:00
1e1535e6f9 Merge pull request #1620 from yichengq/204
etcdserver: not record attributes when add member
2014-11-12 21:33:53 -08:00
4adbd821a3 Merge pull request #1706 from xiang90/fix_sender_hub_add
etcdserver: do not add/remove/update local member to/from sender hub
2014-11-12 21:29:33 -08:00
04994048bb Merge pull request #1702 from xiang90/node_config_propose
raft: add a test for proposeConfChange
2014-11-12 21:16:54 -08:00
ba915ad5a8 etcdserver: do not add/remove/update local member to/from sender hub 2014-11-12 20:45:21 -08:00
84ecb89774 *: fix Procfile 2014-11-12 17:54:09 -08:00
0c2b45ddc6 etcdserver: not record attributes when add member
There is no need to set attributes value when adding member because new
member will publish the information whenever it starts.
2014-11-12 17:48:15 -08:00
eb66d2b0eb Merge pull request #1699 from jonboulle/node_stop
raft: block Stop() on n.done, support idempotency
2014-11-12 16:26:54 -08:00
2a407dadc0 raft: add a test for proposeConfChange 2014-11-12 16:16:26 -08:00
634011eb8b Merge pull request #1698 from xiang90/node_propose
raft: add a test for node proposal
2014-11-12 16:02:57 -08:00
2cedf127d4 raft: block Stop() on n.done, support idempotency 2014-11-12 15:54:45 -08:00
68ab7e69e1 raft: add a test for node proposal 2014-11-12 15:44:24 -08:00
ec7793557a Merge pull request #1664 from yichengq/212
integration: add AddMember test
2014-11-12 15:04:30 -08:00
b271e88c20 Merge pull request #1696 from xiang90/testnodetick
raft: add a test for node.Tick
2014-11-12 14:38:07 -08:00
bc9de47a9a integration: add increase cluster size test 2014-11-12 14:33:18 -08:00
fc21f299b1 Merge pull request #1676 from jonboulle/doc_initial_cluster
etcdserver: validate and document initial-advertise-peer-urls
2014-11-12 14:13:03 -08:00
5cef3d888a integration: remove unnecessary t.Testing argument 2014-11-12 14:11:56 -08:00
d834324e97 raft: stop the node synchronously 2014-11-12 14:06:52 -08:00
d1ae276434 integration: fix test to propagate NewServer errors 2014-11-12 13:12:49 -08:00
1197c1f965 etcdserver: move peer URLs check to config 2014-11-12 13:12:49 -08:00
3f358b6d5d etcdserver: ensure initial-advertise-peer-urls match initial-cluster
This adds a check to setupCluster to ensure that the list of URLs
specified in `initial-advertise-peer-urls` matches those configured in
`initial-cluster` for this node. Also updates the documentation to
clarify this and address some changes in wording.
2014-11-12 12:54:35 -08:00
45c36a0808 raft: add a test for node.Tick 2014-11-12 11:51:51 -08:00
0772987128 Merge pull request #1695 from xiang90/nodestart
raft: add comment string for TestNodeStart
2014-11-12 11:41:03 -08:00
fe0325fce7 raft: add comment string for TestNodeStart 2014-11-12 11:40:40 -08:00
f1f796f2fc Merge pull request #1694 from xiang90/use_member
etcdserver: use member instead of node at etcd level
2014-11-12 10:48:45 -08:00
0aa8258d29 etcdserver: use member instead of node at etcd level 2014-11-12 10:45:35 -08:00
fb93e3fa00 Merge pull request #1689 from yichengq/219
raft: update unstable when calling stableTo with 0
2014-11-12 10:41:40 -08:00
d494014782 Merge pull request #1679 from xiang90/peerurl
update peer url
2014-11-12 10:21:13 -08:00
48644f465d Merge pull request #1692 from yichengq/221
raft: nodes return sorted ids
2014-11-12 10:08:19 -08:00
78cbb1512c raft: nodes return sorted ids
This makes raft.softState return the same result when its soft state is
not changed.
2014-11-11 22:58:15 -08:00
7dba92dd53 raft: update unstable when calling stableTo with 0
It should update unstable in this case because it may happen that raft
only writes entry 0 into stable storage.
2014-11-11 17:20:31 -08:00
3f3fc05c8f Merge pull request #1687 from xiang90/fix_listener
Fix listener
2014-11-11 13:10:51 -08:00
5967794009 *: support updating advertise-peer-url
Users might want to update the peerurl of the etcd member in several cases.
For example, if the IP address of the physical machine etcd running on is
changed, user need to update the adversite-pee-rurl accordingly.
This commit makes etcd support updating the advertise-peer-url of its members.
2014-11-11 12:07:03 -08:00
b6f0c789b8 transport: create a tls listener only if the tlsInfo is not empty and the scheme is HTTPS 2014-11-11 11:51:57 -08:00
b87243d827 Merge pull request #1688 from xiang90/cluster_test
Cluster test
2014-11-11 11:36:08 -08:00
67a0de4bbc etcdserver: use member pointer for all tests 2014-11-11 11:20:56 -08:00
e4931e0c47 etcdserver: remove unnecessary newTestMemberp 2014-11-11 11:09:33 -08:00
077e144e8a etcdserver: move newTestMember* to member_test.go 2014-11-11 11:02:50 -08:00
4b2d6fc70b Merge pull request #1686 from xiang90/proto
raftpb: fix proto
2014-11-10 17:08:24 -08:00
f64963de88 raftpb: fix proto 2014-11-10 17:05:30 -08:00
246ba4301d Merge pull request #1682 from yichengq/217
integration: rewrite the way to check cluster make progress
2014-11-10 16:58:17 -08:00
24edf57e12 integration: newMember -> mustNewMember 2014-11-10 16:53:15 -08:00
b1c3c4a202 integration: rewrite the way to check cluster make progress 2014-11-10 16:53:07 -08:00
50ffd87831 Merge pull request #1685 from xiang90/proxy
proxy: return JSON errors
2014-11-10 16:51:25 -08:00
424377f859 proxy: add a todo for logging 2014-11-10 16:37:15 -08:00
6fa8f77638 proxy: return JSON errors 2014-11-10 15:56:42 -08:00
ac77971f99 Merge pull request #1671 from xiang90/proxy_doc
doc: add doc for proxy
2014-11-10 13:39:03 -08:00
645cfb8355 Merge pull request #1681 from jonboulle/fix_exit
etcdmain: do not exit inappropriately
2014-11-10 12:34:36 -08:00
e1e454f138 etcdmain: do not exit inappropriately 2014-11-10 12:34:14 -08:00
a0002d0598 doc: add fallback to discovery section in clustering.md 2014-11-10 12:14:19 -08:00
99aa2caa3d Merge pull request #1680 from jonboulle/fix_errors
etcdmain: actually return errors
2014-11-10 12:04:05 -08:00
8799679083 etcdmain: actually return errors 2014-11-10 11:59:59 -08:00
2dcd8213e4 Merge pull request #1670 from yichengq/207
integration: add basic discovery tests
2014-11-10 10:30:20 -08:00
5396037450 integration: add basic discovery tests 2014-11-10 10:04:30 -08:00
1e299d8232 doc: add doc for proxy 2014-11-08 19:59:24 -08:00
8870b739b3 Merge pull request #1661 from jonboulle/wal_errors
wal: propagate errors
2014-11-08 17:16:48 -08:00
5a964f49a5 wal: propagate errors 2014-11-08 17:16:23 -08:00
aca58ec605 Merge pull request #1655 from jonboulle/wal_logic
etcdserver: collapse shared readWAL logic
2014-11-08 17:07:43 -08:00
41757e7f78 etcdserver: collapse shared readWAL logic 2014-11-08 17:07:05 -08:00
f333c7ff87 Merge pull request #1668 from yichengq/214
integration: wait cluster to be stable before return launch
2014-11-08 15:54:31 -08:00
071ebb9feb integration: wait cluster to be stable before return launch
The wait ensures that cluster goes into the stable stage, which means that
leader has been elected and starts to heartbeat to followers.

This makes future client requests always handled in time, and there is no
need to retry sending requests.
2014-11-08 15:39:10 -08:00
aa72cda7b2 Merge pull request #1667 from yichengq/213
etcdserver: not get cluster info from self peer urls
2014-11-08 14:05:20 -08:00
4b9c3a9102 etcdserver: not get cluster info from self peer urls
Self peer urls have not started to serve at the time that it tries to
get cluster info, so it is useless to get cluster info from self peer
urls.
2014-11-08 13:52:48 -08:00
0b493ac8d4 version: bump to alpha.2 2014-11-07 16:44:02 -08:00
c73d41d98b Merge pull request #1658 from jonboulle/doc_etcdctl_backup
Add doc for backup and force-new-cluster
2014-11-07 16:39:45 -08:00
3d2f65fc0d docs: clarify rewriting 2014-11-07 16:35:33 -08:00
6b283f6ea1 docs: reword failure descriptions 2014-11-07 16:34:19 -08:00
4367c9a1db docs: no need to stop etcd while doing backup 2014-11-07 16:25:38 -08:00
c9894687fc Merge pull request #1662 from yichengq/211
etcdserver: fix data race in cluster
2014-11-07 16:20:59 -08:00
a56fa60fb4 doc: add backup/restore guide 2014-11-07 16:14:45 -08:00
014ef0f52d etcdserver: fix data race in cluster
The data race happens when etcd updates member attributes and fetches
member info in http handler at the same time.
2014-11-07 16:13:07 -08:00
2fc47034ee Merge pull request #1660 from yichengq/209
etcdserver: not add sender when it has existed
2014-11-07 16:10:50 -08:00
46cbfbc630 etcdserver: not add sender when it has existed 2014-11-07 14:05:00 -08:00
d3fd10798b Merge pull request #1656 from jonboulle/1656_sender_garbage
sender logs garbage
2014-11-07 13:44:18 -08:00
a6ba4d357c Merge pull request #1474 from jonboulle/1474_print_peers
print out remote peers' information and config change in the cluster
2014-11-07 13:39:32 -08:00
e707af7c3a Merge pull request #1654 from yichengq/208
integration: use client to do requests
2014-11-07 13:37:14 -08:00
ca06fd0060 etcdserver: log cluster when adding/removing node 2014-11-07 13:36:41 -08:00
958ade86a5 etcdserver: log message after loading peers from snapshot 2014-11-07 13:34:43 -08:00
85a4477f71 integration: use client to do requests 2014-11-07 13:34:30 -08:00
38ec84693f etcdserver: clean up sender error message 2014-11-07 13:32:44 -08:00
78865aa7f7 Merge pull request #1657 from xiangli-cmu/backup
*: add ctl backup support
2014-11-07 13:30:54 -08:00
0d541e6338 *: add ctl backup support 2014-11-07 13:27:44 -08:00
5f6e536be8 Merge pull request #1639 from bcwaldon/etcdctl-tls
Wire up TLS flags for etcdctl
2014-11-07 13:19:36 -08:00
4f85a68c25 Merge pull request #1650 from jonboulle/build_release
scripts: clean build-release script a bit
2014-11-07 12:51:30 -08:00
c3aae88b0c Merge pull request #1653 from jonboulle/server_order
etcdserver: re-order ServerConfig fields
2014-11-07 12:28:29 -08:00
32a82bb423 Merge pull request #1651 from jonboulle/discard
etcdserver: discard log output in tests
2014-11-07 12:05:40 -08:00
285cd404e3 etcdserver: print peerURLs when adding member 2014-11-07 12:00:41 -08:00
a607e097c6 etcdserver: re-order ServerConfig fields 2014-11-07 11:45:59 -08:00
55b4ff0cdf etcdserver: discard log output in tests 2014-11-07 11:45:46 -08:00
82094f05e0 scripts: clean build-release script a bit 2014-11-07 11:45:40 -08:00
c4f273478d Merge pull request #1652 from jonboulle/fix_tests
etcdserver: sort IDs and s/getIDset/getIDs/
2014-11-07 11:43:51 -08:00
14e1442d2d etcdserver: sort IDs and s/getIDset/getIDs/ 2014-11-07 10:57:42 -08:00
810a5146dd Merge pull request #1635 from jonboulle/doc
etcdserver: add docstrings for confchanges
2014-11-07 10:20:05 -08:00
5055863e09 etcdserver: add docstrings for confchanges 2014-11-07 10:19:55 -08:00
bf47fe7cac Merge pull request #1647 from xiangli-cmu/force_cluster
etcdserver: force new cluster
2014-11-07 10:15:53 -08:00
9d19429993 Merge pull request #1609 from yichengq/202
etcdserver: refactor sender
2014-11-07 10:12:02 -08:00
142dfc7d88 etcdserver: add doc for getIDset 2014-11-07 09:00:58 -08:00
0a9c6164af etcdserver: add support for force cluster 2014-11-07 08:49:01 -08:00
376268391b Merge pull request #1646 from jonboulle/1536_disco_proxy
discovery: add command line flag for discovery-proxy
2014-11-07 08:32:23 -08:00
d6f37ec9ad Merge pull request #1648 from jonboulle/delete_member_404
etcdhttp: return 404 when removing nonexistent member
2014-11-06 17:24:17 -08:00
ca1b30db10 etcdhttp: return 404 when removing nonexistent member 2014-11-06 16:59:40 -08:00
9454d30854 etcdserver: add sendHub tests 2014-11-06 16:49:13 -08:00
f75e56932a Merge pull request #1643 from jonboulle/fix_flags
pkg: fix SetFlagsFromEnv behaviour
2014-11-06 16:43:21 -08:00
5604b4c57c flag: split out SetFlagsFromEnvBad test; declare return error 2014-11-06 16:40:13 -08:00
8f1885a398 discovery: add command line flag for discovery-proxy 2014-11-06 16:35:24 -08:00
ccded6644a Merge pull request #1505 from yichengq/193
etcdserver: refactor non-blocking check for sync tests
2014-11-06 15:48:18 -08:00
321d65c4ac pkg: fix SetFlagsFromEnv behaviour
This function was fundamentally buggy, as a panic could be trivially
triggered by setting the wrong environment variable (e.g.
ETCD_BIND_ADDR=foo). Instead, let's propagate the error and present it
to the user in a cleaner way.
2014-11-06 14:39:30 -08:00
c5e6053fcd Merge pull request #1638 from xiangli-cmu/better_logging
etcdserver: better logging for clusterFromPeerURLs
2014-11-06 14:33:53 -08:00
eb0d80767e etcdserver: better logging for clusterFromPeerURLs 2014-11-06 14:28:07 -08:00
6fa031fa69 Merge pull request #1641 from bdarnell/remove-raftlog-reset
raft: remove raftLog.resetUnstable and resetNextEnts
2014-11-06 14:26:08 -08:00
21987c8701 raft: remove raftLog.resetUnstable and resetNextEnts
These methods are no longer used outside of tests and are redundant with
the new stableTo and appliedTo methods.
2014-11-06 17:18:00 -05:00
457b30e585 etcdserver: add/remove sender in sendhub explicitly 2014-11-06 14:04:14 -08:00
2138163c61 etcdserver: code clean on sender struct 2014-11-06 14:04:14 -08:00
211c5e3e29 etcdserver: fix data race in Cluster struct 2014-11-06 14:04:14 -08:00
c3b0de943c etcdserver: discard messages if sender reaches max serving
It is the correct thing to do to ensure that the communication is full
of out-of-date messages.

It results in that integration testing is very easy to throw MsgProp away,
and makes client wait until 5 min timeout. Sync interval and heartbeat are
increased to alleviate the traffic.
2014-11-06 14:04:14 -08:00
1e05cd75c7 etcdserver: refactor sender
1. restrict the number of inflight connections to remote member
2. support stop
2014-11-06 14:04:14 -08:00
2d942e970b etcdctl: add --ca-file, --cert-file, --key-file flags 2014-11-06 12:50:38 -08:00
087e0e8b62 Merge pull request #1636 from xiangli-cmu/client
client: add error handling for addmember
2014-11-06 12:46:15 -08:00
b65dd84e1a Merge pull request #1632 from jonboulle/cs_flag
etcdmain: use StringsFlag for initialclusterstate
2014-11-06 12:36:22 -08:00
66572561bf client: add error handling for addmember 2014-11-06 12:31:24 -08:00
902f06c5c4 pkg/transport: generate TLS client config w/ only CAFile 2014-11-06 12:13:36 -08:00
b53a98eb38 Merge pull request #1631 from xiangli-cmu/validate_doc
Validate doc
2014-11-06 11:45:00 -08:00
a1f5df22ad doc: document conflict case when adding a member 2014-11-06 11:16:49 -08:00
04f6208ace etcdmain: use StringsFlag for initialclusterstate 2014-11-06 11:13:24 -08:00
3cb885c6b2 etcdhttp: return 409 instead of 412 when there is a conflict when adding a member 2014-11-06 11:07:25 -08:00
f4ea274555 etcdctl: centralize getEndpoints logic 2014-11-06 10:54:59 -08:00
4b555dba99 client: add SyncableHTTPClient.Endpoints 2014-11-06 10:54:56 -08:00
9c8f9b3560 Merge pull request #1585 from coreos/clean-up-other-apis-docs
docs: clean up other apis
2014-11-06 10:48:02 -08:00
4ed60471fe Merge pull request #1627 from xiangli-cmu/validate_peer_url
etcdserver: validate peerurl when adding members
2014-11-06 10:43:22 -08:00
7d28d80e5a Merge pull request #1626 from jonboulle/proxy_stuff
discovery: simplify interface
2014-11-06 10:09:16 -08:00
45d7ef99c4 Merge pull request #1629 from asmundg/x-fix-typo
Fix typo in docs
2014-11-06 09:58:54 -08:00
0d8345e0c1 Fix typo in docs
Suggesting that users add two nodes with the same name is probably not a
good idea.
2014-11-06 10:49:40 +01:00
2760739ceb Merge pull request #1625 from yichengq/205
docs: describe the lifetime of discovery url
2014-11-06 00:32:49 -08:00
5d755bd54a docs: describe the lifetime of discovery url 2014-11-06 00:31:19 -08:00
bd2b18b6de etcdserver: validate peerurl when adding members 2014-11-05 23:12:48 -08:00
68bca981de discovery: simplify interface
There's no real need to expose a Discoverer interface/struct when the
only use of the interface (and indeed the module) is to invoke a single
function. This isn't Java, after all. So instead, simplify to Discovery
exposing just two functions: JoinCluster (i.e. what was formerly called
"discovery"), and GetCluster (hitherto "ProxyDiscovery")
2014-11-05 22:45:01 -08:00
6fdbb086f4 Merge pull request #1623 from xiangli-cmu/valid_configuration
Valid configuration
2014-11-05 18:13:04 -08:00
99b1af40c6 etcdserver: move config validation to cluster 2014-11-05 17:55:07 -08:00
99bb479a60 Merge pull request #1618 from yichengq/203
etcdserver: improve panic message in Cluster
2014-11-05 17:14:26 -08:00
98406af448 cluster: separate out membersFromStore from newClusterFromStore 2014-11-05 15:56:43 -08:00
6c9169b4f4 etcdserver: improve panic message in Cluster 2014-11-05 15:39:28 -08:00
3fc6f9c24f Merge pull request #1586 from xiangli-cmu/fix_node
*: add Advance interface to raft.Node
2014-11-05 15:09:51 -08:00
0d7c43d885 *: add a Advance interface to raft.Node
Node set the applied to committed right after it sends out Ready to application. This is not
correct since the application has not actually applied the entries at that point. We add a
Advance interface to Node. Application needs to call Advance to tell raft Node its progress.
Also this change can avoid unnecessary copying when application is still applying entires but
there are more entries to be applied.
2014-11-05 15:04:14 -08:00
c5140d5c18 Merge pull request #1614 from yichengq/194
*: handle panic and fatal organizedly
2014-11-05 14:08:35 -08:00
fdb82718e0 Merge pull request #1612 from jonboulle/proxy
proxy: add docstrings
2014-11-05 13:56:24 -08:00
791b2fd503 *: handle panic and fatal more consistently
1. etcd fatals if there is critical error in the system and operator should
do something for it
2. etcd panics if there happens something unexpected, and it should be
reported to us to debug.
2014-11-05 13:53:24 -08:00
3c3cae57c6 Merge pull request #1616 from jonboulle/philips-add-error-to-test
test: add error package
2014-11-05 13:40:58 -08:00
bdd2a0a018 test: add error package 2014-11-05 13:31:42 -08:00
c6104c1e2a Merge pull request #1613 from jonboulle/proxy_clean
etcdmain: simplify proxy start logic
2014-11-05 11:41:15 -08:00
b85496922f etcdmain: simplify proxy start logic 2014-11-05 11:41:03 -08:00
89eac70d09 proxy: add docstrings 2014-11-05 10:30:05 -08:00
58b171b3e5 Merge pull request #1610 from jonboulle/discovery_docs
discovery: add clarifying docstrings
2014-11-04 19:38:59 -08:00
bb84aaebaf discovery: add clarifying docstrings 2014-11-04 17:02:33 -08:00
ab00d23cd3 Merge pull request #1608 from jonboulle/flags
pkg: move to more generic StringsFlag
2014-11-04 16:53:48 -08:00
5de9d38cc6 pkg: move to more generic StringsFlag 2014-11-04 16:52:56 -08:00
d36f09d643 Merge pull request #1602 from jonboulle/bump_timeout
integration: bump timeout for good path
2014-11-04 16:52:44 -08:00
f71c247d87 Merge pull request #1604 from xiangli-cmu/fallback_proxy
*: support discovery fallback
2014-11-04 16:41:28 -08:00
71acd0c3d0 discovery: consolidate proxyDiscover and Discover interface 2014-11-04 16:38:05 -08:00
288624550e Merge pull request #1581 from jonboulle/log_changes
No logs when members added/removed from cluster
2014-11-04 15:13:12 -08:00
e4d0c25365 etcdserver: log adding and removing nodes 2014-11-04 15:05:15 -08:00
c628d7f412 Merge pull request #1601 from jonboulle/client
client: return ErrNoEndpoint when none available
2014-11-04 14:58:22 -08:00
5cb13fd071 *: support discovery fallback 2014-11-04 14:30:22 -08:00
9e001dee29 Merge pull request #1603 from jonboulle/typo
etcdhttp: fix typo in test comment
2014-11-04 13:18:12 -08:00
4d40816a90 etcdserver: refactor non-blocking check for sync tests
to make it much more reliable and avoid false errors.
2014-11-04 13:07:44 -08:00
0f7add9722 etcdhttp: fix typo in test comment 2014-11-04 12:57:59 -08:00
9f29545f66 integration: bump timeout for good path
When waiting for a watch result, we expect the good path to complete
quickly here so we don't need to time out so aggressively. (Failure
noted in #1600)
2014-11-04 12:55:40 -08:00
45b7c9a4ac client: return ErrNoEndpoint when none available
In certain cases (for example, if a cluster peer is accessible but it
has no members listed), the httpClusterClient could have an empty set of
endpoints as a result of the Sync. This means that its Do function could
potentially return a nil response and nil error, with catastrophic
consequences for callers.

To be safe (particularly about this latter behaviour), this change
errors in both Sync and Do if no endpoints are available.
2014-11-04 12:51:43 -08:00
34dabe281b Merge pull request #1591 from philips/application-json-errors
error: use application/json as the content-type
2014-11-04 12:31:06 -08:00
5fbef59dbc error: use application/json as the content-type
Fixes #1584
2014-11-04 12:08:18 -08:00
915f8f4822 Merge pull request #1531 from jonboulle/410_gone
return 410 Gone for member that has been removed in /v2/members -XDELETE
2014-11-04 11:54:01 -08:00
cedcc0d8df etchttp: return 410 gone for permanently removed members 2014-11-04 11:21:24 -08:00
ac49e1d50f Merge pull request #1594 from unihorn/201
etcdhttp/etcdserver: support HEAD on /v2/keys/ namespace
2014-11-04 00:11:47 -08:00
866ec5948c etcdhttp/etcdserver: support HEAD on /v2/keys/ namespace 2014-11-04 00:06:49 -08:00
aa5711bd0f Merge pull request #1595 from jonboulle/header
*: add copyright header to remaining files
2014-11-03 23:42:14 -08:00
f7434b55e5 *: add copyright header to remaining files 2014-11-03 23:29:15 -08:00
2235b47030 Merge pull request #1545 from unihorn/197
etcdhttp: always respond json-format error to client
2014-11-03 23:25:14 -08:00
5ead800ff5 Merge pull request #1572 from xiangli-cmu/raft_test
raft: add paper tests for section 5.4.1
2014-11-03 22:37:26 -08:00
e4b12a8e28 Merge pull request #1593 from unihorn/200
etcdserver: print out initial cluster members
2014-11-03 22:23:40 -08:00
9aefb91531 etcdhttp: always respond json-format error to client 2014-11-03 22:19:17 -08:00
5ed5d44652 etcdserver: print out initial cluster members
It is moved from etcdmain pkg because the line should only be printed out
when etcd bootstraps at the first time.
2014-11-03 19:34:24 -08:00
cc0ef16346 docs: clean up other apis
The docs for the other APIs use curl for example usage, which matches
the docs for the etcd APIs.

Other cleanup include fixing usage of peer ports and using 10.0.0.x IPs
throughout.
2014-11-03 17:14:08 -08:00
a272f5d7e3 Merge pull request #1592 from jonboulle/integration_tests
integration: add keys API integration tests
2014-11-03 16:31:59 -08:00
63cf0b9d90 integration: add keys API integration tests 2014-11-03 16:30:29 -08:00
ab69c2adbd etcdhttp: use EcodePrevValueRequired when appropriate 2014-11-03 16:12:50 -08:00
075ab6415f Merge pull request #1587 from xiangli-cmu/fix_wal
wal: sync before returning from create
2014-11-03 15:58:47 -08:00
dd09042632 etcdserver: try to listen on ports before initializing etcd server 2014-11-03 15:55:58 -08:00
165ac654e8 raft: add paper tests for section 5.4.1 2014-11-03 15:50:56 -08:00
dbdeceda7b raft: do not load empty state and ents 2014-11-03 15:16:41 -08:00
ff1f5a9d57 wal: sync before returning from create 2014-11-03 14:28:59 -08:00
d1ec13210f Merge pull request #1571 from bcwaldon/client-redirects
client: follow redirects
2014-11-03 14:26:20 -08:00
2ba02c04be Merge pull request #1576 from coreos/print-initial-cluster-members
etcd: print initial cluster members during startup
2014-11-03 14:24:33 -08:00
6dd4944e62 client: follow redirects 2014-11-03 12:15:16 -08:00
5da481213e Merge pull request #1478 from unihorn/190
etcdserver: panic on storage error
2014-11-03 11:07:55 -08:00
433b4138c5 etcdserver: panic on storage error
It is a critical error to etcd, and etcd is not able to recover it now.
2014-11-03 10:46:04 -08:00
729770f32a Merge pull request #1570 from bcwaldon/client-endpoints
client: use all endpoints
2014-11-03 10:44:09 -08:00
3ec4da6ac6 etcd: print initial cluster members during startup
etcd now prints the initial clusters members during startup.

```
2014/11/03 10:32:46 etcd: initial cluster members: etcd0=http://127.0.0.1:2380,etcd1=http://127.0.0.1:2390,etcd2=http://127.0.0.1:2400
```
2014-11-03 10:38:18 -08:00
9df06bfa94 Merge pull request #1579 from coreos/cleanup-clustering-doc
docs: clean up clustering doc
2014-11-03 10:25:04 -08:00
20df86e3c3 docs: clean up clustering doc 2014-11-03 09:51:50 -08:00
6433be5738 Merge pull request #1575 from coreos/improve-admin-docs
docs: fix usage of peers urls
2014-11-02 22:56:23 -08:00
3068340a83 docs: fix usage of peers urls 2014-11-02 22:00:41 -08:00
da6827f09e client: use all endpoints 2014-10-31 20:51:47 -07:00
75104c10d4 Merge pull request #1553 from bcwaldon/client-sync
Support syncing and `--no-sync` flag in `etcdctl member` commands
2014-10-31 20:51:01 -07:00
58af26736c client: further clarify external interfaces 2014-10-31 20:45:55 -07:00
17c6f21d68 client: elevate context to caller of KeysAPI 2014-10-31 17:27:43 -07:00
f0760d6246 client: elevate context to caller of MembersAPI 2014-10-31 17:27:42 -07:00
913d102a81 client: remove unused field 2014-10-31 17:25:05 -07:00
824049897d client: export necessary interfaces/methods 2014-10-31 17:25:05 -07:00
b47631b38f etcdctl: respect --no-sync in member subcommands 2014-10-31 17:25:05 -07:00
22b86684f0 etcdctl: sync before running member subcommands 2014-10-31 17:25:05 -07:00
5ed5d018be client: add httpClusterClient.Sync 2014-10-31 17:25:05 -07:00
f6e8b677cf client: pass httpActionDo into NewMembersAPI 2014-10-31 17:25:05 -07:00
0ef270c25c client: pass httpActionDo into New[Discovery]KeysAPI 2014-10-31 17:25:05 -07:00
1130273178 client: s/newHTTPClusterClient/NewHTTPClient/ 2014-10-31 17:25:05 -07:00
3eb126af4d client: use httpActionDo in httpClusterClient 2014-10-31 17:25:05 -07:00
c282664c23 client: s/transport/CancelableTransport/
CancelableTransport is implemented by callers of the
client pkg, so we should export it so it is
documented publicly.
2014-10-31 17:25:04 -07:00
d52d836761 client: return full http.Response in httpActionDo 2014-10-31 17:25:04 -07:00
5bdf6a4110 Merge pull request #1528 from unihorn/191
raft: add tests based on section 5.3 in raft paper
2014-10-31 16:35:36 -07:00
421d5fbe72 raft: add tests based on section 5.3 in raft paper 2014-10-31 16:32:34 -07:00
f35130a0ed etcdctl: clean up formatting of member add 2014-10-31 15:37:08 -07:00
500e9e2212 version: bump to v0.5.0-alpha.1 2014-10-31 15:22:13 -07:00
7c52a86325 Merge pull request #1569 from philips/dynamic-configuration-docs
Documentation: add the runtime configuration document
2014-10-31 15:19:42 -07:00
124dd7096a Documentation: add the runtime configuration document 2014-10-31 15:16:45 -07:00
388b4aeb71 Merge pull request #1568 from philips/dynamic-configuration
etcdctl: two member fixes
2014-10-31 15:08:40 -07:00
6b4485d1ae etcdctl: take a name and print out the initial cluster
To help the user lets print out the configuration that they will need to give
to their new member:

$ etcdctl member add infra4 http://localhost:7004
added member 9bf1b35fc7761a23 to cluster
ETCD_NAME="infra4"
ETCD_INITIAL_CLUSTER="node2=http://localhost:7002,node3=http://localhost:7003,infra4=http://localhost:7004,node1=http://localhost:7001"

This is a little weird because the API doesn't take a name so the user gives us
a name and we just pass it on through.
2014-10-31 15:05:23 -07:00
74886713db Merge pull request #1567 from unihorn/199
*: name node{1,2,3} -> infra{1,2,3}
2014-10-31 14:53:34 -07:00
8f3be206ed etcdctl: add help on the members subcommands 2014-10-31 14:50:23 -07:00
1db23109ad *: name node{1,2,3} -> infra{1,2,3}
Be consistent with the naming in documentations.
2014-10-31 14:46:39 -07:00
749097429f Merge pull request #1565 from jonboulle/int
integration: clean up licenses and docs
2014-10-31 14:34:05 -07:00
34b2fecd28 integration: clean up licenses and docs 2014-10-31 14:33:56 -07:00
faede90293 Merge pull request #1556 from philips/fixup-the-admin-guide
Documentation: fixup the admin_guide
2014-10-31 14:11:39 -07:00
b6cc34b52e Documentation: fixup the admin_guide
- Provide more concrete examples and explanation
- Cleanup the formatting to one sentence per line, this makes reviewing
  easier
- Point to existing docs on wal and snap instead of trying to duplicate
  it here again.
2014-10-31 14:11:27 -07:00
308b8796e4 Merge pull request #1557 from bcwaldon/id-logging
etcdserver: fix logging of IDs
2014-10-31 12:27:37 -07:00
6e038e02a6 etcdserver: fix logging of IDs 2014-10-31 12:26:53 -07:00
38250d3fac Merge pull request #1541 from bcwaldon/client-peers
Use `-peers` in `etcdctl members` commands
2014-10-31 12:26:15 -07:00
eab4692744 client: use v2MembersURL helper 2014-10-31 12:21:15 -07:00
f0c3385cfc etcdctl: wire up --peers for member commands 2014-10-31 12:21:15 -07:00
8b8b3efdaa client: accept slice of endpoints 2014-10-31 12:21:15 -07:00
8d519ffdb8 client: introduce httpClusterClient 2014-10-31 12:21:15 -07:00
323fb1ec85 client: introduce httpActionDo interface 2014-10-31 12:21:15 -07:00
9d07db4432 client: move timeout into caller of httpClient 2014-10-31 12:21:15 -07:00
7c1f4a9baf client: explicitly carry API prefix around 2014-10-31 12:21:15 -07:00
dee912f2fd etcdctl: break out mustNewMembersAPI 2014-10-31 12:21:08 -07:00
bc62b05c7f etcdctl: break out getPeersFlagValue 2014-10-31 12:21:00 -07:00
48ec876af9 godep: bump github.com/codegangsta/cli 2014-10-31 12:21:00 -07:00
a576dbca43 Merge pull request #1554 from xiangli-cmu/removed_logging
etcdserver: better logging when member is removed
2014-10-31 12:07:57 -07:00
eb472b7745 etcdserver: better logging when member is removed 2014-10-31 12:00:50 -07:00
a535161a84 Merge pull request #1552 from philips/fixup-wal-doc
wal: update the docs to show the optional metadata field
2014-10-31 11:32:26 -07:00
513c72ec8b wal: update the docs to show the optional metadata field 2014-10-31 11:32:17 -07:00
e02ef6b141 Merge pull request #1546 from unihorn/198
etcdserver: better logging for assign ids from upstream
2014-10-31 11:13:43 -07:00
2c5f062b7f etcdserver: better logging for assign ids from upstream 2014-10-31 11:06:31 -07:00
1bb07115f2 Merge pull request #1550 from jonboulle/bump_timeout
etcdhttp: bump default Server timeout to 5 mins
2014-10-31 11:05:44 -07:00
9726d3909c etcdhttp: bump default Server timeout to 5 mins 2014-10-31 10:52:46 -07:00
c53e58e97c Merge pull request #1309 from jonboulle/1309_standard_id
standardize ID serialization
2014-10-31 10:50:32 -07:00
55c92ad456 *: create ID type
This creates a simple ID type (wrapped around uint64) to provide for
standard serialization/deserialization to a string (i.e. base 16
encoded). This replaces strutil so now that package is removed.
2014-10-31 10:34:07 -07:00
781abc1db0 Merge pull request #1539 from unihorn/195
*: clean log.Print
2014-10-30 18:18:35 -07:00
aa50af1c69 *: clean log.Print
1. only log things by default that the operator of etcd may need to react to
2. put package name at the head of log lines
2014-10-30 18:15:53 -07:00
7f29045c0f Merge pull request #1543 from xiangli-cmu/fix_logging
etcdserver: fix sender logging
2014-10-30 18:13:16 -07:00
0f8b035253 etcdserver: fix sender logging 2014-10-30 18:00:00 -07:00
42a7c928d4 Merge pull request #1542 from xiangli-cmu/fix_logging
etcdhttp: fix logging in raft handler
2014-10-30 17:41:47 -07:00
02ff59514f etcdhttp: fix logging in raft handler 2014-10-30 17:39:01 -07:00
9a56001d63 Merge pull request #1537 from xiangli-cmu/cluster-token
Cluster token
2014-10-30 17:09:25 -07:00
8e633db5cb doc: add doc for initial-cluster-token 2014-10-30 17:08:15 -07:00
64a12e9341 Merge pull request #1511 from coreos/set-watch-consistency-to-strong
etcdctl: Set watch consistency to STRONG
2014-10-30 16:52:50 -07:00
ac71ad92af Merge pull request #1452 from unihorn/187
etcdserver: exit program when node is removed
2014-10-30 15:32:26 -07:00
ed30b6deca etcdserver: exit program when node is removed
Originally added in 400dd2d7bc,
and removed by mistake when refactor cluster.
2014-10-30 15:31:58 -07:00
76298ebcd8 Merge pull request #1534 from bcwaldon/client-tests
client: test assertStatusCode
2014-10-30 13:50:21 -07:00
d36a3e18d2 etcdctl: remove SetConsistency call
This call to SetConsistency is explicitly setting the default
value, so it's really unnecessary.
2014-10-30 13:45:59 -07:00
3dfb6723b2 *: rename initial-cluster-name to initial-cluster-token 2014-10-30 13:43:38 -07:00
6087e2b2f6 Merge pull request #1509 from bcwaldon/etcdctl-102
etcdctl: add --sort flag to ls command
2014-10-30 13:33:37 -07:00
6e8de1f426 Merge pull request #1508 from bcwaldon/etcdctl-96
etcdctl: add -p to ls command
2014-10-30 13:33:30 -07:00
052521eaf1 client: test assertStatusCode 2014-10-30 11:46:44 -07:00
549c643bfe Merge pull request #1530 from xiangli-cmu/discovery_doc
doc: add a Custom etcd discovery service section
2014-10-30 11:40:23 -07:00
af7d73717c doc: add a Custom etcd discovery service section 2014-10-30 11:34:46 -07:00
816c173edf Merge pull request #1526 from xiangli-cmu/leader_log
raft: better logging for leader transition
2014-10-30 10:13:58 -07:00
9359a57211 Merge pull request #1523 from jonboulle/raft_stuff
raft: minor cleanup in comments
2014-10-30 10:01:59 -07:00
b99633207c raft: minor cleanup in comments 2014-10-30 10:01:44 -07:00
4f6206bf65 Merge pull request #1522 from jonboulle/raft_tests
raft: add tests for progress.maybeDecr
2014-10-30 10:00:01 -07:00
bf44219766 Merge pull request #1492 from jonboulle/1492_nonexistent_member
Attempting to remove nonexistent member mishandled
2014-10-30 09:58:02 -07:00
19881b2f15 etcdhttp: return 404 when removing nonexistent member 2014-10-30 09:57:54 -07:00
46ebf69c02 raft: better logging for leader transition 2014-10-30 09:33:38 -07:00
0cf0cb3d02 raft: add tests for progress.maybeDecr 2014-10-29 22:57:25 -07:00
83ca16188c Merge pull request #1417 from jonboulle/master
RFC: move main logic to etcd subpackage
2014-10-29 18:47:42 -07:00
cf9dd31daa etcd: move main logic to etcdmain subpackage 2014-10-29 18:43:22 -07:00
38617f5c9b Merge pull request #1510 from xiangli-cmu/fix_discovery
discovery: fix discovery for not working on customized discovery service
2014-10-29 18:33:06 -07:00
027e944985 discovery: fix discovery for not working on customized discovery service 2014-10-29 18:30:59 -07:00
2be3f870cc etcdctl: Set watch consistency to STRONG 2014-10-29 18:11:05 -07:00
ba38847bdd Merge pull request #1507 from bcwaldon/doc-fix
doc: link directly to members API
2014-10-29 18:01:27 -07:00
97597eca03 etcdctl: add --sort flag to ls command
This is a port of coreos/etcdctl#102
2014-10-29 17:45:11 -07:00
243886edc8 etcdctl: add -p to ls command
This is a port of coreos/etcdctl#96
2014-10-29 17:42:21 -07:00
f61824ce01 Merge pull request #1500 from bcwaldon/client-tests
Add tests for client pkg
2014-10-29 17:26:26 -07:00
ac810b86bc doc: link directly to members API 2014-10-29 17:21:10 -07:00
e85ba2f384 Merge pull request #1504 from xiangli-cmu/admin_guide
doc: add a doc for data directory
2014-10-29 17:16:27 -07:00
f5c1da6967 doc: add a doc for data directory 2014-10-29 17:07:21 -07:00
0f51cbde6c Merge pull request #1502 from unihorn/192
store: copy Nodes correctly in NodeExtern.Clone
2014-10-29 16:57:04 -07:00
a910d8ba9f store: copy Nodes correctly in NodeExtern.Clone 2014-10-29 16:54:09 -07:00
d756dd2079 client: test membersAPIActionRemove 2014-10-29 16:42:16 -07:00
5264c05ddb client: clean up style of TestMembersAPIActionList 2014-10-29 16:42:16 -07:00
4e759b46ce client: use httptypes.MemberCreateRequest in member add 2014-10-29 16:42:14 -07:00
011a67c878 httptypes: add MemberCreateRequest.MarshalJSON 2014-10-29 16:37:07 -07:00
e457d52f5c client: log incorrect HTTP resp body as string 2014-10-29 16:37:07 -07:00
ccca32b138 Merge pull request #1497 from unihorn/189
raft: add tests based on section 5.1 in raft paper
2014-10-29 16:35:25 -07:00
dabb5c150d raft: add tests based on section 5.1 in raft paper 2014-10-29 16:22:17 -07:00
b7b3bf40e0 Merge pull request #1501 from bcwaldon/rename-members-tests
etcdhttp: s/TestServeAdminMembers*/TestServeMembers*/
2014-10-29 16:21:16 -07:00
2c0f6e4bf9 etcdhttp: s/TestServeAdminMembers*/TestServeMembers*/ 2014-10-29 16:18:03 -07:00
3f6e584702 Merge pull request #1499 from bcwaldon/client_clean
client: pass around statuscode instead of Response
2014-10-29 15:51:58 -07:00
97c23c4333 client: pass around statuscode instead of Response
There's no real need for do and doWithTimeout to return Responses when
the only field of interest is the status code.

This also removes the superfluous httpMembersAPIResponse struct.
2014-10-29 15:47:55 -07:00
95231c1278 Merge pull request #1493 from bcwaldon/etcdctl-members
etcdctl members [list|add|remove]
2014-10-29 15:44:30 -07:00
f810dda9b2 Merge pull request #1481 from bcwaldon/no-more-admin-api
Remove "admin" from /v2/admin/members
2014-10-29 15:43:37 -07:00
f6e242aa01 etcdctl: impl members commands 2014-10-29 15:09:23 -07:00
8b12e1aa37 client: fill in MembersAPI 2014-10-29 15:03:22 -07:00
b59961228b Merge pull request #1495 from xiangli-cmu/fix_raft_test
raft: fix a incorrect in testMaybeAppend
2014-10-29 15:03:13 -07:00
738da2b3fa raft: fix a incorrect in testMaybeAppend 2014-10-29 14:57:39 -07:00
de0cf2fb8e Merge pull request #1459 from bcwaldon/member-list
etcdhttp: encode MembersCollection properly
2014-10-29 14:49:22 -07:00
4b1431109e Merge pull request #1490 from jonboulle/1490
Logging member ID as int
2014-10-29 14:42:02 -07:00
6375bd7960 Merge pull request #1469 from xiangli-cmu/raft_log_test
raft: add tests for maybeappend
2014-10-29 14:36:07 -07:00
e99da41539 etcdserver: log member ID as hex string 2014-10-29 14:36:05 -07:00
14f4163e41 raft: add several test cases for testMaybeAppend 2014-10-29 14:35:13 -07:00
5bba81f5fc Merge pull request #1472 from jonboulle/clone_event
waitIndex is not working
2014-10-29 14:23:32 -07:00
a59e8cf1a6 Merge pull request #1487 from jonboulle/peerurls
etcdserver: make peer URLs log message more readable
2014-10-29 14:23:25 -07:00
9546df9a6c etcdserver: make peer URLs log message more readable 2014-10-29 14:18:51 -07:00
f7631be453 Merge pull request #1485 from jonboulle/raft_clean
raft: move test helper function into tests
2014-10-29 14:06:39 -07:00
81304b2b7e raft: move test helper function into tests 2014-10-29 14:04:45 -07:00
8298e06627 doc: remove trailing slashes 2014-10-29 12:16:02 -07:00
ab67fa4cc6 api: remove admin prefix from members API 2014-10-29 12:12:51 -07:00
bab19e3b0b doc: move admin_api.md to other_apis.md 2014-10-29 12:12:51 -07:00
d3bafd6aa4 etcdhttp: encode MembersCollection properly 2014-10-29 12:06:22 -07:00
84be7c1e9e etcdserver/store: clone Events before modifying 2014-10-29 11:54:35 -07:00
ad1718a3e5 raft: add a test case for testLogMaybeAppend 2014-10-29 11:44:18 -07:00
35bba87d2a Merge pull request #1471 from unihorn/189
raft: add tests based on section 5.2 in raft paper
2014-10-29 10:49:23 -07:00
bffe611fe6 raft: add tests based on section 5.2 in raft paper 2014-10-29 10:17:09 -07:00
6bcfa2b05d Merge pull request #1475 from bketelsen/patch-1
Add Skydns, crypt, viper
2014-10-29 08:55:09 -07:00
3857e92cad Add Skydns, crypt, viper
Added a plethora of tools we've written or contributed to that use etcd.
2014-10-29 09:09:31 -04:00
04e56a454e Merge pull request #1465 from bcwaldon/member-post
Clean up POST /v2/admin/members
2014-10-28 17:08:30 -07:00
658a84312b Merge pull request #1464 from bcwaldon/error-ctype
Set Content-Type before calling WriteHeader
2014-10-28 15:48:23 -07:00
ae7280dcf3 Merge pull request #1466 from jonboulle/errors
main: catch a few unhandled errors
2014-10-28 15:43:57 -07:00
c6873c1eab raft: add tests for maybeappend 2014-10-28 15:07:49 -07:00
a96f5ab146 main: catch a few unhandled errors
If any of this initialization fails, something very bad has happened,
and we should not continue as-is (this has previously manifested in
strange bugs)
2014-10-28 11:18:22 -07:00
6f851ac885 httptypes: set headers before call to WriteHeader 2014-10-28 11:08:10 -07:00
2b4201c53d httptypes: test HTTPError 2014-10-28 10:59:53 -07:00
57d447fef6 Merge pull request #1463 from bcwaldon/writeError-logging
etcdhttp: only log when error deserves it
2014-10-28 10:45:55 -07:00
c07b9ae32e etcdhttp: 415 when content-type not JSON 2014-10-28 10:38:09 -07:00
8fbf887e52 etcdhttp: only log when error deserves it 2014-10-28 10:30:05 -07:00
d1fb732e63 etcdhttp: properly serialize Member on POST 2014-10-28 10:21:11 -07:00
8b0eaa9e15 etcdhttp: separate member create deserialization 2014-10-28 10:09:05 -07:00
ad0664da9c doc: fix documentation of POST /v2/admin/members 2014-10-28 09:44:59 -07:00
b6b5081254 Merge pull request #1450 from bcwaldon/raft-logging
raft: stop logging IDs with 0x prefix
2014-10-28 08:17:11 -07:00
6796669484 raft: stop logging IDs with 0x prefix 2014-10-27 18:56:13 -07:00
87327a245d Merge pull request #1448 from bcwaldon/member-URLs
fix usage of copy in newMemberCollection
2014-10-27 18:43:33 -07:00
e08c2bbe3e Merge pull request #1449 from bcwaldon/id-to-hex
Move ID*Hex functions to pkg/strutil
2014-10-27 18:43:00 -07:00
8d052dd374 etcdhttp: copy Member URLs properly 2014-10-27 18:39:33 -07:00
480e92d340 strutil: move IDAsHex/IDFromHex to new pkg 2014-10-27 18:39:09 -07:00
dad7500d13 Merge pull request #1443 from bcwaldon/member-ID
Serialize member ID in API
2014-10-27 18:38:53 -07:00
d55546d62e Merge pull request #1446 from philips/add-quay-badge
README: add the quay badge to the official git builds
2014-10-27 17:40:50 -07:00
acd8eecd4e README: add the quay badge to the official git builds 2014-10-27 17:39:16 -07:00
2d31e5ab56 Merge pull request #1441 from philips/add-root-dockerfile
Dockerfile: initial commit
2014-10-27 17:27:57 -07:00
2472953939 etcdhttp: hex-encode member ID 2014-10-27 17:25:22 -07:00
80172c3d4a etcdserver: s/parseMemberID/mustParseMemberIDFromKey/ 2014-10-27 17:25:00 -07:00
b316c6b002 Merge pull request #1435 from xiangli-cmu/json
etcdhttp: make admin HTTP endpoint return json format error
2014-10-27 17:07:58 -07:00
6cb45236ac etcdhttp: make admin HTTP endpoint return json format error 2014-10-27 17:03:58 -07:00
04b5853261 Merge pull request #1439 from kelseyhightower/check-data-dir-permissions
etcd: ensure data dir is writable
2014-10-27 16:57:19 -07:00
b1731f0843 etcd: ensure data dir is writable
etcd checks that the data dir is writable by writing and removing an
empty file to the data dir during startup and exits non-zero if that
fails.

fixes #876
2014-10-27 16:52:05 -07:00
36cacb8bd8 Merge pull request #1429 from coreos/1392
Fix #1392
2014-10-27 16:50:46 -07:00
e849d8e157 etcdhttp: DELETE on members = MethodNotAllowed 2014-10-27 16:49:04 -07:00
387639e802 etcdserver/etcdhttp: treat /v2/admin/members and /v2/admin/members/ equally 2014-10-27 16:49:03 -07:00
3e234918ee Dockerfile: initial commit 2014-10-27 16:43:27 -07:00
0ce78d7a9c Merge pull request #1431 from philips/build-release-script
scripts: import script from 0.5 release
2014-10-27 16:42:30 -07:00
52350d1d2f Merge pull request #1433 from philips/add-build-docker-script
scripts: build-docker
2014-10-27 16:42:11 -07:00
7384ee39a6 Merge pull request #1436 from xiangli-cmu/writeTo
error: write->writeTo
2014-10-27 15:34:17 -07:00
d0604c7d5c error: write->writeTo 2014-10-27 15:32:36 -07:00
74c257f63d Merge pull request #1419 from xiangli-cmu/raft_log_test
raft: add test for findConflict
2014-10-27 14:30:36 -07:00
460d6490ba raft: address issues in comments 2014-10-27 14:20:42 -07:00
60cb18b6c2 Merge pull request #1432 from unihorn/187
raft: use raft-specific rand.Rand instead of global one
2014-10-27 14:15:58 -07:00
e8302c8413 scripts: build-docker
Build docker images from the release tarballs. The default CMD is
etcd.
2014-10-27 12:34:51 -07:00
b986a52579 raft: use raft-specific rand.Rand instead of global one 2014-10-27 12:32:11 -07:00
538ce935f0 scripts: import script from 0.5 release
It isn't pretty but this was the tool used to build the zip and tar
files for 0.5
2014-10-27 12:18:24 -07:00
94e4595af5 Merge pull request #1427 from bcwaldon/members-serialization
Centralize Members serialization
2014-10-27 11:33:39 -07:00
753bc5e166 httptypes: add doc.go 2014-10-27 11:22:47 -07:00
80ca168cbe client: simplify MembersAPI response parsing 2014-10-27 11:22:47 -07:00
14795d8ed9 httptypes: use MemberCollection for JSON (de)serialization 2014-10-27 11:22:47 -07:00
7545152318 httptypes: use []string for Member URLs 2014-10-27 11:22:47 -07:00
54a2d8ffc9 client: move Member models to new types pkg 2014-10-27 11:22:46 -07:00
ee27846d5b Merge pull request #1422 from unihorn/187
etcdserver: parse context error for better message
2014-10-27 11:00:01 -07:00
e77f8e311c etcdserver: parse context error for better message 2014-10-27 10:59:15 -07:00
585881a870 Merge pull request #1428 from jonboulle/subpackages
pkg: move everything into subpackages
2014-10-27 10:32:11 -07:00
9964bfa6b9 Merge pull request #1426 from xiangli-cmu/clusterid
etcdhttp: attach clusterID to key and adminMember endpoint
2014-10-27 10:16:16 -07:00
6e6d1897d8 pkg: move everything into subpackages 2014-10-27 09:57:28 -07:00
328d8f2d26 Merge pull request #1424 from bcwaldon/pkg-readme
pkg: add README.md
2014-10-27 09:13:26 -07:00
6f792354ca etcdhttp: attach clusterID to key and adminMember endpoint 2014-10-27 07:52:39 -07:00
40048d7300 Merge pull request #1420 from xiangli-cmu/clean_log
raft: remove unused code
2014-10-26 20:14:05 -07:00
000962d689 Merge pull request #1421 from xiangli-cmu/logging
*: better logging
2014-10-26 20:13:55 -07:00
444e6e952b pkg: add README.md 2014-10-26 16:28:48 -07:00
f9af07eb5b Merge pull request #1423 from bcwaldon/http-refactor-hdlrs
eradicate serverHandler
2014-10-26 14:44:30 -07:00
b06499d0c2 etcdserver/etcdhttp: break apart HTTP handlers 2014-10-26 13:20:53 -07:00
4b77082b6e Merge pull request #1415 from bcwaldon/http-refactor
etcdserver/etcdhttp: break apart http.go
2014-10-26 12:39:22 -07:00
009b737cef *: better logging 2014-10-26 08:13:03 -07:00
94f701cf95 raft: refactor isUpToDate and add a test 2014-10-25 20:34:14 -07:00
8cd95e916d raft: comments for isUpToDate 2014-10-25 20:12:54 -07:00
86c66cd802 raft: remove unused code 2014-10-25 19:56:13 -07:00
90f26e4a56 raft: add test for findConflict 2014-10-25 18:58:11 -07:00
73215447c1 Merge pull request #1414 from bcwaldon/client-members-API
Add MembersAPI w/ List method
2014-10-25 11:33:42 -07:00
cba19e348f client: MembersAPI.List 2014-10-25 11:30:15 -07:00
435611cf0d etcdserver/etcdhttp: break apart http.go 2014-10-25 11:28:52 -07:00
00dcbf8bf7 client: unexport HTTPKeysAPI 2014-10-25 08:58:25 -07:00
73e48068c2 client: add prefix to KeysAPI 2014-10-25 08:58:25 -07:00
150 changed files with 14322 additions and 4106 deletions

2
Dockerfile Normal file
View File

@ -0,0 +1,2 @@
FROM golang:onbuild
EXPOSE 4001 7001 2379 2380

View File

@ -0,0 +1,47 @@
## etcd 0.4.x -> 0.5.0 Data Migration Tool
### Upgrading from 0.4.x
Between 0.4.x and 0.5, the on-disk data formats have changed. In order to allow users to convert to 0.5, a migration tool is provided.
In the early 0.5.0-alpha series, we're providing this tool early to encourage adoption. However, before 0.5.0-release, etcd will autodetect the 0.4.x data dir upon upgrade and automatically update the data too (while leaving a backup, in case of emergency).
### Data Migration Tips
* Keep the environment variables and etcd instance flags the same (much as [the upgrade document](../upgrade.md) suggests), particularly `--name`/`ETCD_NAME`.
* Don't change the cluster configuration. If there's a plan to add or remove machines, it's probably best to arrange for that after the migration, rather than before or at the same time.
### Running the tool
The tool can be run via:
```sh
./bin/etcd-migrate --data-dir=<PATH TO YOUR DATA>
```
It should autodetect everything and convert the data-dir to be 0.5 compatible. It does not remove the 0.4.x data, and is safe to convert multiple times; the 0.5 data will be overwritten. Recovering the disk space once everything is settled is covered later in the document.
If, however, it complains about autodetecting the name (which can happen, depending on how the cluster was configured), you need to supply the name of this particular node. This is equivalent to the `--name` flag (or `ETCD_NAME` variable) that etcd was run with, which can also be found by accessing the self api, eg:
```sh
curl -L http://127.0.0.1:4001/v2/stats/self
```
Where the `"name"` field is the name of the local machine.
Then, run the migration tool with
```sh
./bin/etcd-migrate --data-dir=<PATH TO YOUR DATA> --name=<NAME>
```
And the tool should migrate successfully. If it still has an error at this time, it's a failure or bug in the tool and it's worth reporting a bug.
### Recovering Disk Space
If the conversion has completed, the entire cluster is running on something 0.5-based, and the disk space is important, the following command will clear 0.4.x data from the data-dir:
```sh
rm -ri snapshot conf log
```
It will ask before every deletion, but these are the 0.4.x files and will not affect the working 0.5 data.

View File

@ -1,72 +0,0 @@
## Admin API
### GET /v2/admin/members/
Return an HTTP 200 OK response code and a representation of all members in the etcd cluster:
```
Example Request: GET
http://localhost:2379/v2/admin/members/
Response formats: JSON
Example Response:
```
```json
{
"members": [
{
"id":"272e204152",
"name":"node1",
"peerURLs":[
"http://10.0.0.10:2379"
],
"clientURLs":[
"http://10.0.0.10:2380"
]
},
{
"id":"2225373f43",
"name":"node2",
"peerURLs":[
"http://127.0.0.11:2379"
],
"clientURLs":[
"http://127.0.0.11:2380"
]
},
]
}
```
### POST /v2/admin/members/
Add a member to the cluster.
Returns an HTTP 201 response code and the representation of added member with a newly generated a memberID when successful. Returns a string describing the failure condition when unsuccessful.
If the POST body is malformed an HTTP 400 will be returned. If the member exists in the cluster or existed in the cluster at some point in the past an HTTP 500(TODO: fix this) will be returned. If the cluster fails to process the request within timeout an HTTP 500 will be returned, though the request may be processed later.
```
Example Request: POST
http://localhost:2379/v2/admin/members/
Body:
[{"PeerURLs":["http://10.0.0.10:2379"]}]
Respose formats: JSON
Example Response:
```
```json
[
{
"id":"3777296169",
"peerURLs":[
"http://10.0.0.10:2379"
],
},
]
```
### DELETE /v2/admin/members/:id
Remove a member from the cluster.
Returns empty when successful. Returns a string describing the failure condition when unsuccessful.
If the member does not exist in the cluster an HTTP 500(TODO: fix this) will be returned. If the cluster fails to process the request within timeout an HTTP 500 will be returned, though the request may be processed later.
```
Response formats: JSON
Example Request: DELETE
http://localhost:2379/v2/admin/members/272e204152
Example Response: Empty
```

View File

@ -0,0 +1,79 @@
## Administration
### Data Directory
#### Lifecycle
When first started, etcd stores its configuration into a data directory specified by the data-dir configuration parameter.
Configuration is stored in the write ahead log and includes: the local member ID, cluster ID, and initial cluster configuration.
The write ahead log and snapshot files are used during member operation and to recover after a restart.
If a members data directory is ever lost or corrupted then the user should remove the etcd member from the cluster via the [members API][members-api].
A user should avoid restarting an etcd member with a data directory from an out-of-date backup.
Using an out-of-date data directory can lead to inconsistency as the member had agreed to store information via raft then re-joins saying it needs that information again.
For maximum safety, if an etcd member suffers any sort of data corruption or loss, it must be removed from the cluster.
Once removed the member can be re-added with an empty data directory.
[members-api]: https://github.com/coreos/etcd/blob/master/Documentation/0.5/other_apis.md#members-api
#### Contents
The data directory has two sub-directories in it:
1. wal: write ahead log files are stored here. For details see the [wal package documentation][wal-pkg]
2. snap: log snapshots are stored here. For details see the [snap package documentation][snap-pkg]
[wal-pkg]: http://godoc.org/github.com/coreos/etcd/wal
[snap-pkg]: http://godoc.org/github.com/coreos/etcd/snap
### Cluster Lifecycle
If you are spinning up multiple clusters for testing it is recommended that you specify a unique initial-cluster-token for the different clusters.
This can protect you from cluster corruption in case of mis-configuration because two members started with different cluster tokens will refuse members from each other.
### Disaster Recovery
etcd is designed to be resilient to machine failures. An etcd cluster can automatically recover from any number of temporary failures (for example, machine reboots), and a cluster of N members can tolerate up to _(N/2)-1_ permanent failures (where a member can no longer access the cluster, due to hardware failure or disk corruption). However, in extreme circumstances, a cluster might permanently lose enough members such that quorum is irrevocably lost. For example, if a three-node cluster suffered two simultaneous and unrecoverable machine failures, it would be normally impossible for the cluster to restore quorum and continue functioning.
To recover from such scenarios, etcd provides functionality to backup and restore the datastore and recreate the cluster without data loss.
#### Backing up the datastore
The first step of the recovery is to backup the data directory on a functioning etcd node. To do this, use the `etcdctl backup` command, passing in the original data directory used by etcd. For example:
```sh
etcdctl backup \
--data-dir /var/lib/etcd \
--backup-dir /tmp/etcd_backup
```
This command will rewrite some of the metadata contained in the backup (specifically, the node ID and cluster ID), which means that the node will lose its former identity. In order to recreate a cluster from the backup, you will need to start a new, single-node cluster. The metadata is rewritten to prevent the new node from inadvertently being joined onto an existing cluster.
#### Restoring a backup
To restore a backup using the procedure created above, start etcd with the `-force-new-cluster` option and pointing to the backup directory. This will initialize a new, single-member cluster with the default advertised peer URLs, but preserve the entire contents of the etcd data store. Continuing from the previous example:
```sh
etcd \
-data-dir=/tmp/etcd_backup \
-force-new-cluster \
...
```
Now etcd should be available on this node and serving the original datastore.
Once you have verified that etcd has started successfully, shut it down and move the data back to the previous location (you may wish to make another copy as well to be safe):
```sh
pkill etcd
rm -fr /var/lib/etcd
mv /tmp/etcd_backup /var/lib/etcd
etcd \
-data-dir=/var/lib/etcd \
...
```
#### Restoring the cluster
Now that the node is running successfully, you can add more nodes to the cluster and restore resiliency. See the [runtime configuration](runtime-configuration.md) guide for more details.

View File

@ -401,19 +401,19 @@ curl 'http://127.0.0.1:2379/v2/keys/dir/asdf?consistent=true&wait=true'
```json
{
"action": "expire",
"node": {
"createdIndex": 8,
"key": "/dir",
"modifiedIndex": 15
},
"prevNode": {
"createdIndex": 8,
"key": "/dir",
"dir":true,
"modifiedIndex": 17,
"expiration": "2013-12-11T10:39:35.689275857-08:00"
}
"action": "expire",
"node": {
"createdIndex": 8,
"key": "/dir",
"modifiedIndex": 15
},
"prevNode": {
"createdIndex": 8,
"key": "/dir",
"dir":true,
"modifiedIndex": 17,
"expiration": "2013-12-11T10:39:35.689275857-08:00"
}
}
```
@ -639,22 +639,22 @@ We should see the response as an array of items:
```json
{
"action": "get",
    "node": {
     "key": "/",
       "dir": true,
       "nodes": [
         {
             "key": "/foo_dir",
             "dir": true,
             "modifiedIndex": 2,
             "createdIndex": 2
          },
          {
             "key": "/foo",
             "value": "two",
             "modifiedIndex": 1,
             "createdIndex": 1
          }
"node": {
"key": "/",
"dir": true,
"nodes": [
{
"key": "/foo_dir",
"dir": true,
"modifiedIndex": 2,
"createdIndex": 2
},
{
"key": "/foo",
"value": "two",
"modifiedIndex": 1,
"createdIndex": 1
}
]
}
}
@ -669,33 +669,33 @@ curl http://127.0.0.1:2379/v2/keys/?recursive=true
```json
{
    "action": "get",
    "node": {
        "key": "/",
        "dir": true,
        "nodes": [
            {
                "key": "/foo_dir",
                "dir": true,
                "nodes": [
                    {
                        "key": "/foo_dir/foo",
                        "value": "bar",
                        "modifiedIndex": 2,
                        "createdIndex": 2
                    }
                ],
                "modifiedIndex": 2,
                "createdIndex": 2
            },
            {
                "key": "/foo",
                "value": "two",
                "modifiedIndex": 1,
                "createdIndex": 1
            }
        ]
    }
"action": "get",
"node": {
"key": "/",
"dir": true,
"nodes": [
{
"key": "/foo_dir",
"dir": true,
"nodes": [
{
"key": "/foo_dir/foo",
"value": "bar",
"modifiedIndex": 2,
"createdIndex": 2
}
],
"modifiedIndex": 2,
"createdIndex": 2
},
{
"key": "/foo",
"value": "two",
"modifiedIndex": 1,
"createdIndex": 1
}
]
}
}
```
@ -896,7 +896,7 @@ curl http://127.0.0.1:2379/v2/stats/leader
"startTime": "2014-10-24T13:15:51.184719899-07:00",
"uptime": "7m17.859616962s"
},
"name": "node1",
"name": "infra1",
"recvAppendRequestCnt": 3949,
"recvBandwidthRate": 561.5729321100841,
"recvPkgRate": 9.008227977383449,
@ -1005,6 +1005,6 @@ curl http://127.0.0.1:2379/v2/stats/store
## Cluster Config
See the [admin API guide][admin-api] for details on the cluster management APIs.
See the [other etcd APIs][other-apis] for details on the cluster management.
[admin-api]: https://github.com/coreos/etcd/blob/master/Documentation/0.5/admin_api.md
[other-apis]: https://github.com/coreos/etcd/blob/master/Documentation/0.5/other_apis.md

View File

@ -1,118 +1,144 @@
# Clustering Guide
This guide will walk you through configuring a three machine etcd cluster with
the following details:
This guide will walk you through configuring a three machine etcd cluster with the following details:
|Name |Address |
|-------|---------------|
|-------|-----------|
|infra0 |10.0.1.10 |
|infra1 |10.0.1.11 |
|infra2 |10.0.1.12 |
## Static
As we know the cluster members, their addresses and the size of the cluster
before starting we can use an offline bootstrap configuration. Each machine
will get either the following command line or environment variables:
As we know the cluster members, their addresses and the size of the cluster before starting, we can use an offline bootstrap configuration by setting the `initial-cluster` flag. Each machine will get either the following command line or environment variables:
```
ETCD_INITIAL_CLUSTER=infra0=http://10.0.1.10:2379,infra1=http://10.0.1.11:2379,infra2=http://10.0.1.12:2379”
ETCD_INITIAL_CLUSTER="infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380"
ETCD_INITIAL_CLUSTER_STATE=new
```
```
-initial-cluster infra0=http://10.0.1.10:2379,http://10.0.1.11:2379,infra2=http://10.0.1.12:2379 \
-initial-cluster-state new
-initial-cluster infra0=http://10.0.1.10:2380,http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
-initial-cluster-state new
```
Note that the URLs specified in `initial-cluster` are the _advertised peer URLs_, i.e. they should match the value of `initial-advertise-peer-urls` on the respective nodes.
If you are spinning up multiple clusters (or creating and destroying a single cluster) with same configuration for testing purpose, it is highly recommended that you specify a unique `initial-cluster-token` for the different clusters. By doing this, etcd can generate unique cluster IDs and member IDs for the clusters even if they otherwise have the exact same configuration. This can protect you from cross-cluster-interaction, which might corrupt your clusters.
On each machine you would start etcd with these flags:
```
$ etcd -name infra0 -initial-advertise-peer-urls https://10.0.1.10:2379 \
-initial-cluster infra0=http://10.0.1.10:2379,infra1=http://10.0.1.11:2379,infra2=http://10.0.1.12:2379 \
-initial-cluster-state new
$ etcd -name infra1 -initial-advertise-peer-urls https://10.0.1.11:2379 \
-initial-cluster infra0=http://10.0.1.10:2379,infra1=http://10.0.1.11:2379,infra2=http://10.0.1.12:2379 \
-initial-cluster-state new
$ etcd -name infra2 -initial-advertise-peer-urls https://10.0.1.12:2379 \
-initial-cluster infra0=http://10.0.1.10:2379,infra1=http://10.0.1.11:2379,infra2=http://10.0.1.12:2379 \
-initial-cluster-state new
$ etcd -name infra0 -initial-advertise-peer-urls https://10.0.1.10:2380 \
-initial-cluster-token etcd-cluster-1 \
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
-initial-cluster-state new
```
```
$ etcd -name infra1 -initial-advertise-peer-urls https://10.0.1.11:2380 \
-initial-cluster-token etcd-cluster-1 \
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
-initial-cluster-state new
```
```
$ etcd -name infra2 -initial-advertise-peer-urls https://10.0.1.12:2380 \
-initial-cluster-token etcd-cluster-1 \
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
-initial-cluster-state new
```
The command line parameters starting with `-initial-cluster` will be ignored on
subsequent runs of etcd. You are free to remove the environment variables or
command line flags after the initial bootstrap process. If you need to make
changes to the configuration later see our guide on runtime configuration.
The command line parameters starting with `-initial-cluster` will be ignored on subsequent runs of etcd. You are free to remove the environment variables or command line flags after the initial bootstrap process. If you need to make changes to the configuration later (for example, adding or removing members to/from the cluster), see the [runtime configuration](runtime-configuration.md) guide.
### Error Cases
In the following case we have not included our new host in the list of
enumerated nodes. If this is a new cluster, the node must be added to the list
of initial cluster members.
In the following example, we have not included our new host in the list of enumerated nodes. If this is a new cluster, the node _must_ be added to the list of initial cluster members.
```
$ etcd -name infra1 -initial-advertise-peer-urls http://10.0.1.11:2379 \
-initial-cluster infra0=http://10.0.1.10:2379 \
-initial-cluster-state new
$ etcd -name infra1 -initial-advertise-peer-urls http://10.0.1.11:2380 \
-initial-cluster infra0=http://10.0.1.10:2380 \
-initial-cluster-state new
etcd: infra1 not listed in the initial cluster config
exit 1
```
In this case we are attempting to map a node (infra0) on a different address
(127.0.0.1:2379) than its enumerated address in the cluster list
(10.0.1.10:2379). If this node is to listen on multiple addresses, all
addresses must be reflected in the “initial-cluster” configuration directive.
In this example, we are attempting to map a node (infra0) on a different address (127.0.0.1:2380) than its enumerated address in the cluster list (10.0.1.10:2380). If this node is to listen on multiple addresses, all addresses _must_ be reflected in the "initial-cluster" configuration directive.
```
$ etcd -name infra0 -initial-advertise-peer-urls http://127.0.0.1:2379 \
-initial-cluster infra0=http://10.0.1.10:2379,infra1=http://10.0.1.11:2379,infra2=http://10.0.1.12:2379 \
-initial-cluster-state=new
etcd: infra0 has different advertised URLs in the cluster and advertised peer URLs list
$ etcd -name infra0 -initial-advertise-peer-urls http://127.0.0.1:2380 \
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
-initial-cluster-state=new
etcd: error setting up initial cluster: infra0 has different advertised URLs in the cluster and advertised peer URLs list
exit 1
```
If you configure a peer with a different set of configuration and attempt to
join this cluster you will get a cluster ID mismatch and etcd will exit.
If you configure a peer with a different set of configuration and attempt to join this cluster you will get a cluster ID mismatch and etcd will exit.
```
$ etcd -name infra3 -initial-advertise-peer-urls http://10.0.1.13:2379 \
-initial-cluster infra0=http://10.0.1.10:2379,infra1=http://10.0.1.11:2379,infra3=http://10.0.1.13:2379 \
-initial-cluster-state=new
$ etcd -name infra3 -initial-advertise-peer-urls http://10.0.1.13:2380 \
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra3=http://10.0.1.13:2380 \
-initial-cluster-state=new
etcd: conflicting cluster ID to the target cluster (c6ab534d07e8fcc4 != bc25ea2a74fb18b0). Exiting.
exit 1
```
## Discovery
In a number of cases you might not know the IPs of your cluster peers ahead of
time. This is common when utilizing cloud providers or when your network uses
DHCP. In these cases you can use an existing etcd cluster to bootstrap a new
one. We call this process “discovery”.
In a number of cases, you might not know the IPs of your cluster peers ahead of time. This is common when utilizing cloud providers or when your network uses DHCP. In these cases, rather than specifying a static configuration, you can use an existing etcd cluster to bootstrap a new one. We call this process "discovery".
Discovery uses an existing cluster to bootstrap itself. If you are using your
own etcd cluster you can create a URL like so:
### Lifetime of a Discovery URL
A discovery URL identifies a unique etcd cluster. Instead of reusing a discovery URL, you should always create discovery URLs for new clusters.
Moreover, discovery URLs should ONLY be used for the initial bootstrapping of a cluster. To change cluster membership after the cluster is already running, see the [runtime reconfiguration][runtime] guide.
[runtime]: https://github.com/coreos/etcd/blob/master/Documentation/0.5/runtime-configuration.md
### Custom etcd discovery service
Discovery uses an existing cluster to bootstrap itself. If you are using your own etcd cluster you can create a URL like so:
```
$ curl -X PUT https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83/_config/size -d value=5
$ curl -X PUT https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83/_config/size -d value=3
```
The URL you will use in this case will be
`https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83`
and the machines will use the
`https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83`
directory for registration as they start.
By setting the size key to the URL, you create a discovery URL with an expected cluster size of 3.
If you do not have access to an existing cluster you can use the hosted
discovery.etcd.io service. You can create a private discovery URL using the
"new" endpoint like so:
If you bootstrap an etcd cluster using discovery service with more than the expected number of etcd members, the extra etcd processes will [fall back][fall-back] to being [proxies][proxy] by default.
The URL you will use in this case will be `https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83` and the etcd members will use the `https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83` directory for registration as they start.
Now we start etcd with those relevant flags for each member:
```
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \
-discovery https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
```
```
$ etcd -name infra1 -initial-advertise-peer-urls http://10.0.1.11:2380 \
-discovery https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
```
```
$ etcd -name infra2 -initial-advertise-peer-urls http://10.0.1.12:2380 \
-discovery https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
```
This will cause each member to register itself with the custom etcd discovery service and begin the cluster once all machines have been registered.
### Public discovery service
If you do not have access to an existing cluster, you can use the public discovery service hosted at `discovery.etcd.io`. You can create a private discovery URL using the "new" endpoint like so:
```
$ curl https://discovery.etcd.io/new?size=3
https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
```
This will create the cluster with an initial expected size of 3 members. If you
do not specify a size a default of 3 will be used.
This will create the cluster with an initial expected size of 3 members. If you do not specify a size, a default of 3 will be used.
If you bootstrap an etcd cluster using discovery service with more than the expected number of etcd members, the extra etcd processes will [fall back][fall-back] to being [proxies][proxy] by default.
[fall-back]: proxy.md#fallback-to-proxy-mode-with-discovery-service
[proxy]: proxy.md
```
ETCD_DISCOVERY=https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
@ -122,16 +148,22 @@ ETCD_DISCOVERY=https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573d
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
```
Now we start etcd with those relevant flags on each machine:
Now we start etcd with those relevant flags for each member:
```
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2379 -discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
$ etcd -name infra1 -initial-advertise-peer-urls http://10.0.1.11:2379 -discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
$ etcd -name infra2 -initial-advertise-peer-urls http://10.0.1.12:2379 -discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
```
```
$ etcd -name infra1 -initial-advertise-peer-urls http://10.0.1.11:2380 \
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
```
```
$ etcd -name infra2 -initial-advertise-peer-urls http://10.0.1.12:2380 \
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
```
This will cause each machine to register itself with the etcd service and begin
the cluster once all machines have been registered.
This will cause each member to register itself with the discovery service and begin the cluster once all members have been registered.
You can use the environment variable `ETCD_DISCOVERY_PROXY` to cause etcd to use an HTTP proxy to connect to the discovery service.
@ -139,17 +171,23 @@ You can use the environment variable `ETCD_DISCOVERY_PROXY` to cause etcd to use
#### Discovery Server Errors
```
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2379 -discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
etcd: error: the cluster doesnt have a size configuration value in https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de/_config
exit 1
```
#### User Errors
This error will occur if the discovery cluster already has the configured number of members, and `discovery-fallback` is explicitly disabled
```
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2379 -discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
etcd: error: the cluster using discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de has already started with all 5 members
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de \
-discovery-fallback exit
etcd: discovery: cluster is full
exit 1
```
@ -159,20 +197,16 @@ This is a harmless warning notifying you that the discovery URL will be
ignored on this machine.
```
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2379 -discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
etcd: warn: ignoring discovery URL: etcd has already been initialized and has a valid log in /var/lib/etcd
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
etcdserver: warn: ignoring discovery: etcd has already been initialized and has a valid log in /var/lib/etcd
```
# 0.4 to 0.5+ Migration Guide
In etcd 0.5 we introduced the ability to listen on more than one address and to
advertise multiple addresses. This makes using etcd easier when you have
complex networking, such as private and public networks on various cloud
providers.
In etcd 0.5 we introduced the ability to listen on more than one address and to advertise multiple addresses. This makes using etcd easier when you have complex networking, such as private and public networks on various cloud providers.
To make understanding this feature easier, we changed the naming of some flags,
but we support the old flags to make the migration from the old to new version
easier.
To make understanding this feature easier, we changed the naming of some flags, but we support the old flags to make the migration from the old to new version easier.
|Old Flag |New Flag |Migration Behavior |
|-----------------------|-----------------------|---------------------------------------------------------------------------------------|

View File

@ -0,0 +1,96 @@
## Members API
* [List members](#list-members)
* [Add a member](#add-a-member)
* [Delete a member](#delete-a-member)
## List members
Return an HTTP 200 OK response code and a representation of all members in the etcd cluster.
### Request
```
GET /v2/members HTTP/1.1
```
### Example
```
curl http://10.0.0.10:2379/v2/members
```
```json
{
"members": [
{
"id": "272e204152",
"name": "infra1",
"peerURLs": [
"http://10.0.0.10:2380"
],
"clientURLs": [
"http://10.0.0.10:2379"
]
},
{
"id": "2225373f43",
"name": "infra2",
"peerURLs": [
"http://10.0.0.11:2380"
],
"clientURLs": [
"http://10.0.0.11:2379"
]
},
]
}
```
## Add a member
Returns an HTTP 201 response code and the representation of added member with a newly generated a memberID when successful. Returns a string describing the failure condition when unsuccessful.
If the POST body is malformed an HTTP 400 will be returned. If the member exists in the cluster or existed in the cluster at some point in the past an HTTP 409 will be returned. If any of the given peerURLs exists in the cluster an HTTP 409 will be returned. If the cluster fails to process the request within timeout an HTTP 500 will be returned, though the request may be processed later.
### Request
```
POST /v2/members HTTP/1.1
{"peerURLs": ["http://10.0.0.10:2379"]}
```
### Example
```
curl http://10.0.0.10:2379/v2/members -XPOST -H "Content-Type: application/json" -d '{"peerURLs":["http://10.0.0.10:2379"]}'
```
```json
{
"id": "3777296169",
"peerURLs": [
"http://10.0.0.10:2379"
]
}
```
## Delete a member
Remove a member from the cluster. The member ID must be a hex-encoded uint64.
Returns empty when successful. Returns a string describing the failure condition when unsuccessful.
If the member does not exist in the cluster an HTTP 500(TODO: fix this) will be returned. If the cluster fails to process the request within timeout an HTTP 500 will be returned, though the request may be processed later.
### Request
```
DELETE /v2/members/<id> HTTP/1.1
```
### Example
```
curl http://10.0.0.10:2379/v2/members/272e204152 -XDELETE
```

View File

@ -0,0 +1,32 @@
## Proxy
etcd can now run as a transparent proxy. Running etcd as a proxy allows for easily discovery of etcd within your infrastructure, since it can run on each machine as a local service. In this mode, etcd acts as a reverse proxy and forwards client requests to an active etcd cluster. The etcd proxy does not participant in the consensus replication of the etcd cluster, thus it neither increases the resilience nor decreases the write performance of the etcd cluster.
etcd currently supports two proxy modes: `readwrite` and `readonly`. The default mode is `readwrite`, which forwards both read and write requests to the etcd cluster. A `readonly` etcd proxy only forwards read requests to the etcd cluster, and returns `HTTP 501` to all write requests.
### Using an etcd proxy
To start etcd in proxy mode, you need to provide three flags: `proxy`, `listen-client-urls`, and `initial-cluster` (or `discovery-url`).
To start a readwrite proxy, set `-proxy on`; To start a readonly proxy, set `-proxy readonly`.
The proxy will be listening on `listen-client-urls` and forward requests to the etcd cluster discovered from in `initial-cluster` or `discovery url`.
#### Start an etcd proxy with a static configuration
To start a proxy that will connect to a statically defined etcd cluster, specify the `initial-cluster` flag:
```
etcd -proxy on -client-listen-urls 127.0.0.1:8080 -initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380
```
#### Start an etcd proxy with the discovery service
If you bootstrap an etcd cluster using the [discovery service][discovery-service], you can also start the proxy with the same `discovery-url`.
To start a proxy using the discovery service, specify the `discovery-url` flag. The proxy will wait until the etcd cluster defined at the `discovery-url` finishes bootstrapping, and then start to forward the requests.
```
etcd -proxy on -client-listen-urls 127.0.0.1:8080 -discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
```
#### Fallback to proxy mode with discovery service
If you bootstrap a etcd cluster using [discovery service][discovery-service] with more than the expected number of etcd members, the extra etcd processes will fall back to being `readwrite` proxies by default. They will forward the requests to the cluster as described above. For example, if you create a discovery url with `size=5`, and start ten etcd processes using that same discovery URL, the result will be a cluster with five etcd members and five proxies. Note that this behaviour can be disabled with the `proxy-fallback` flag.
[discovery-service]: https://github.com/coreos/etcd/blob/master/Documentation/0.5/clustering.md#discovery

View File

@ -0,0 +1,143 @@
## Runtime Reconfiguration
etcd comes with support for incremental runtime reconfiguration, which allows users to update the membership of the cluster at run time.
## Reconfiguration Use Cases
Let us walk through the four use cases for re-configuring a cluster: replacing a member, increasing or decreasing cluster size, and restarting a cluster from a majority failure.
### Replace a Member
The most common use case of cluster reconfiguration is to replace a member because of a permanent failure of the existing member: for example, hardware failure, loss of network address, or data directory corruption.
It is important to replace failed members as soon as the failure is detected.
If etcd falls below a simple majority of members it can no longer accept writes: e.g. in a 3 member cluster the loss of two members will cause writes to fail and the cluster to stop operating.
### Increase Cluster Size
To make your cluster more resilient to machine failure you can increase the size of the cluster.
For example, if the cluster consists of three machines, it can tolerate one failure.
If we increase the cluster size to five, it can tolerate two machine failures.
Increasing the cluster size can also provide better read performance.
When a client accesses etcd, the normal read gets the data from the local copy of each member (members always shares the same view of the cluster at the same index, which is guaranteed by the sequential consistency of etcd).
Since clients can read from any member, increasing the number of members thus increases overall read throughput.
### Decrease Cluster Size
To improve the write performance of a cluster, you might want to trade off resilience by removing members.
etcd replicates the data to the majority of members of the cluster before committing the write.
Decreasing the cluster size means the etcd cluster has to do less work for each write, thus increasing the write performance.
### Restart Cluster from Majority Failure
If the majority of your cluster is lost, then you need to take manual action in order to recover safely.
The basic steps in the recovery process include creating a new cluster using the old data, forcing a single member to act as the leader, and finally using runtime configuration to add members to this new cluster.
TODO: https://github.com/coreos/etcd/issues/1242
## Cluster Reconfiguration Operations
Now that we have the use cases in mind, let us lay out the operations involved in each.
Before making any change, the simple majority (quorum) of etcd members must be available.
This is essentially the same requirement as for any other write to etcd.
All changes to the cluster are done one at a time:
To replace a single member you will make an add then a remove operation
To increase from 3 to 5 members you will make two add operations
To decrease from 5 to 3 you will make two remove operations
All of these examples will use the `etcdctl` command line tool that ships with etcd.
If you want to use the member API directly you can find the documentation [here](https://github.com/coreos/etcd/blob/master/Documentation/0.5/other_apis.md).
### Remove a Member
First, we need to find the target member:
```
$ etcdctl member list
6e3bd23ae5f1eae0: name=node2 peerURLs=http://localhost:7002 clientURLs=http://127.0.0.1:4002
924e2e83e93f2560: name=node3 peerURLs=http://localhost:7003 clientURLs=http://127.0.0.1:4003
a8266ecf031671f3: name=node1 peerURLs=http://localhost:7001 clientURLs=http://127.0.0.1:4001
```
Let us say the member ID we want to remove is a8266ecf031671f3.
We then use the `remove` command to perform the removal:
```
$ etcdctl member remove a8266ecf031671f3
Removed member a8266ecf031671f3 from cluster
```
The target member will stop itself at this point and print out the removal in the log:
```
etcd: this member has been permanently removed from the cluster. Exiting.
```
Removal of the leader is safe, but the cluster will be out of progress for a period of election timeout because it needs to elect the new leader.
### Add a Member
Adding a member is a two step process:
* Add the new member to the cluster via the [members API](https://github.com/coreos/etcd/blob/master/Documentation/0.5/other_apis.md#post-v2members) or the `etcdctl member add` command.
* Start the member with the correct configuration.
Using `etcdctl` let's add the new member to the cluster:
```
$ etcdctl member add infra3 http://10.0.1.13:2380
added member 9bf1b35fc7761a23 to cluster
ETCD_NAME="infra3"
ETCD_INITIAL_CLUSTER="infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380,infra3=http://10.0.1.13:2380"
ETCD_INITIAL_CLUSTER_STATE=existing
```
> Notice that infra3 was added to the cluster using its advertised peer URL.
Now start the new etcd process with the relevant flags for the new member:
```
$ export ETCD_NAME="infra3"
$ export ETCD_INITIAL_CLUSTER="infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380,infra3=http://10.0.1.13:2380"
$ export ETCD_INITIAL_CLUSTER_STATE=existing
$ etcd -listen-client-urls http://10.0.1.13:2379 -advertise-client-urls http://10.0.1.13:2379 -listen-peer-urls http://10.0.1.13:2380 -initial-advertise-peer-urls http://10.0.1.13:2380
```
The new member will run as a part of the cluster and immediately begin catching up with the rest of the cluster.
If you are adding multiple members the best practice is to configure the new member, then start the process, then configure the next, and so on.
A common case is increasing a cluster from 1 to 3: if you add one member to a 1-node cluster, the cluster cannot make progress before the new member starts because it needs two members as majority to agree on the consensus.
#### Error Cases
In the following case we have not included our new host in the list of enumerated nodes.
If this is a new cluster, the node must be added to the list of initial cluster members.
```
$ etcd -name infra3 \
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
-initial-cluster-state existing
etcdserver: assign ids error: the member count is unequal
exit 1
```
In this case we give a different address (10.0.1.14:2380) to the one that we used to join the cluster (10.0.1.13:2380).
```
$ etcd -name infra4 \
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380,infra4=http://10.0.1.14:2380 \
-initial-cluster-state existing
etcdserver: assign ids error: unmatched member while checking PeerURLs
exit 1
```
When we start etcd using the data directory of a removed member, etcd will exit automatically if it connects to any alive member in the cluster:
```
$ etcd
etcd: this member has been permanently removed from the cluster. Exiting.
exit 1
```

View File

@ -101,3 +101,6 @@ A detailed recap of client functionalities can be found in the [clients compatib
- [GoogleCloudPlatform/kubernetes](https://github.com/GoogleCloudPlatform/kubernetes) - Container cluster manager.
- [mailgun/vulcand](https://github.com/mailgun/vulcand) - HTTP proxy that uses etcd as a configuration backend.
- [duedil-ltd/discodns](https://github.com/duedil-ltd/discodns) - Simple DNS nameserver using etcd as a database for names and records.
- [skynetservices/skydns](https://github.com/skynetservices/skydns) - RFC compliant DNS server
- [xordataexchange/crypt](https://github.com/xordataexchange/crypt) - Securely store values in etcd using GPG encryption
- [spf13/viper](https://github.com/spf13/viper) - Go configuration library, reads values from ENV, pflags, files, and etcd with optional encryption

View File

@ -124,7 +124,7 @@ DISCOVERY_URL=... # from https://discovery.etcd.io/new
etcd -name node1 -data-dir node1 -ca-file=/path/to/ca.crt -cert-file=/path/to/node1.crt -key-file=/path/to/node1.key -peer-addr ${node1_public_ip}:7001 -discovery ${DISCOVERY_URL}
# Node2
etcd -name node1 -data-dir node2 -ca-file=/path/to/ca.crt -cert-file=/path/to/node2.crt -key-file=/path/to/node2.key -peer-addr ${node2_public_ip}:7001 -discovery ${DISCOVERY_URL}
etcd -name node2 -data-dir node2 -ca-file=/path/to/ca.crt -cert-file=/path/to/node2.crt -key-file=/path/to/node2.key -peer-addr ${node2_public_ip}:7001 -discovery ${DISCOVERY_URL}
```
The etcd nodes will form a cluster and all communication between nodes in the cluster will be encrypted and authenticated using the client certificates. You will see in the output of etcd that the addresses it connects to use HTTPS.

4
Godeps/Godeps.json generated
View File

@ -16,8 +16,8 @@
},
{
"ImportPath": "github.com/codegangsta/cli",
"Comment": "1.0.0-72-gbb91895",
"Rev": "bb9189510af1f49580c073c9e59e8bf288f0df27"
"Comment": "1.2.0-26-gf7ebb76",
"Rev": "f7ebb761e83e21225d1d8954fde853bf8edd46c4"
},
{
"ImportPath": "github.com/coreos/go-etcd/etcd",

View File

@ -1,2 +1,6 @@
language: go
go: 1.1
script:
- go vet ./...
- go test -v ./...

View File

@ -9,17 +9,17 @@ http://godoc.org/github.com/codegangsta/cli
## Overview
Command line apps are usually so tiny that there is absolutely no reason why your code should *not* be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app.
This is where cli.go comes into play. cli.go makes command line programming fun, organized, and expressive!
**This is where cli.go comes into play.** cli.go makes command line programming fun, organized, and expressive!
## Installation
Make sure you have a working Go environment (go 1.1 is *required*). [See the install instructions](http://golang.org/doc/install.html).
To install cli.go, simply run:
To install `cli.go`, simply run:
```
$ go get github.com/codegangsta/cli
```
Make sure your PATH includes to the `$GOPATH/bin` directory so your commands can be easily used:
Make sure your `PATH` includes to the `$GOPATH/bin` directory so your commands can be easily used:
```
export PATH=$PATH:$GOPATH/bin
```
@ -122,7 +122,7 @@ GLOBAL OPTIONS
```
### Arguments
You can lookup arguments by calling the `Args` function on cli.Context.
You can lookup arguments by calling the `Args` function on `cli.Context`.
``` go
...
@ -137,7 +137,11 @@ Setting and querying flags is simple.
``` go
...
app.Flags = []cli.Flag {
cli.StringFlag{"lang", "english", "language for the greeting"},
cli.StringFlag{
Name: "lang",
Value: "english",
Usage: "language for the greeting",
},
}
app.Action = func(c *cli.Context) {
name := "someone"
@ -155,11 +159,30 @@ app.Action = func(c *cli.Context) {
#### Alternate Names
You can set alternate (or short) names for flags by providing a comma-delimited list for the Name. e.g.
You can set alternate (or short) names for flags by providing a comma-delimited list for the `Name`. e.g.
``` go
app.Flags = []cli.Flag {
cli.StringFlag{"lang, l", "english", "language for the greeting"},
cli.StringFlag{
Name: "lang, l",
Value: "english",
Usage: "language for the greeting",
},
}
```
#### Values from the Environment
You can also have the default value set from the environment via `EnvVar`. e.g.
``` go
app.Flags = []cli.Flag {
cli.StringFlag{
Name: "lang, l",
Value: "english",
Usage: "language for the greeting",
EnvVar: "APP_LANG",
},
}
```
@ -214,8 +237,8 @@ app.Commands = []cli.Command{
### Bash Completion
You can enable completion commands by setting the EnableBashCompletion
flag on the App object. By default, this setting will only auto-complete to
You can enable completion commands by setting the `EnableBashCompletion`
flag on the `App` object. By default, this setting will only auto-complete to
show an app's subcommands, but you can write your own completion methods for
the App or its subcommands.
```go
@ -237,7 +260,7 @@ app.Commands = []cli.Command{
return
}
for _, t := range tasks {
println(t)
fmt.Println(t)
}
},
}
@ -247,11 +270,18 @@ app.Commands = []cli.Command{
#### To Enable
Source the autocomplete/bash_autocomplete file in your .bashrc file while
setting the PROG variable to the name of your program:
Source the `autocomplete/bash_autocomplete` file in your `.bashrc` file while
setting the `PROG` variable to the name of your program:
`PROG=myprogram source /.../cli/autocomplete/bash_autocomplete`
## Contribution Guidelines
Feel free to put up a pull request to fix a bug or maybe add a feature. I will give it a code review and make sure that it does not break backwards compatibility. If I or any other collaborators agree that it is in line with the vision of the project, we will work with you to get the code into a mergeable state and merge it into the master branch.
If you are have contributed something significant to the project, I will most likely add you as a collaborator. As a collaborator you are given the ability to merge others pull requests. It is very important that new code does not break existing code, so be careful about what code you do choose to merge. If you have any questions feel free to link @codegangsta to the issue in question and we can review it together.
If you feel like you have contributed to the project but have not yet been added as a collaborator, I probably forgot to add you. Hit @codegangsta up over email and we will get it figured out.
## About
cli.go is written by none other than the [Code Gangsta](http://codegangsta.io)

View File

@ -22,6 +22,8 @@ type App struct {
Flags []Flag
// Boolean to enable bash completion commands
EnableBashCompletion bool
// Boolean to hide built-in help command
HideHelp bool
// An action to execute when the bash-completion flag is set
BashComplete func(context *Context)
// An action to execute before any subcommands are run, but after the context is ready
@ -58,16 +60,15 @@ func NewApp() *App {
BashComplete: DefaultAppComplete,
Action: helpCommand.Action,
Compiled: compileTime(),
Author: "Author",
Email: "unknown@email",
}
}
// Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination
func (a *App) Run(arguments []string) error {
// append help to commands
if a.Command(helpCommand.Name) == nil {
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
a.Commands = append(a.Commands, helpCommand)
a.appendFlag(HelpFlag)
}
//append version/help flags
@ -75,7 +76,6 @@ func (a *App) Run(arguments []string) error {
a.appendFlag(BashCompletionFlag)
}
a.appendFlag(VersionFlag)
a.appendFlag(HelpFlag)
// parse flags
set := flagSet(a.Name, a.Flags)
@ -131,12 +131,21 @@ func (a *App) Run(arguments []string) error {
return nil
}
// Another entry point to the cli app, takes care of passing arguments and error handling
func (a *App) RunAndExitOnError() {
if err := a.Run(os.Args); err != nil {
os.Stderr.WriteString(fmt.Sprintln(err))
os.Exit(1)
}
}
// Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags
func (a *App) RunAsSubcommand(ctx *Context) error {
// append help to commands
if len(a.Commands) > 0 {
if a.Command(helpCommand.Name) == nil {
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
a.Commands = append(a.Commands, helpCommand)
a.appendFlag(HelpFlag)
}
}
@ -144,14 +153,13 @@ func (a *App) RunAsSubcommand(ctx *Context) error {
if a.EnableBashCompletion {
a.appendFlag(BashCompletionFlag)
}
a.appendFlag(HelpFlag)
// parse flags
set := flagSet(a.Name, a.Flags)
set.SetOutput(ioutil.Discard)
err := set.Parse(ctx.Args().Tail())
nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, set)
context := NewContext(a, set, ctx.globalSet)
if nerr != nil {
fmt.Println(nerr)

View File

@ -2,9 +2,10 @@ package cli_test
import (
"fmt"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
"os"
"testing"
"github.com/codegangsta/cli"
)
func ExampleApp() {
@ -42,7 +43,11 @@ func ExampleAppSubcommand() {
Usage: "sends a greeting in english",
Description: "greets someone in english",
Flags: []cli.Flag{
cli.StringFlag{"name", "Bob", "Name of the person to greet"},
cli.StringFlag{
Name: "name",
Value: "Bob",
Usage: "Name of the person to greet",
},
},
Action: func(c *cli.Context) {
fmt.Println("Hello,", c.String("name"))
@ -83,12 +88,10 @@ func ExampleAppHelp() {
// describeit - use it to see a description
//
// USAGE:
// command describeit [command options] [arguments...]
// command describeit [arguments...]
//
// DESCRIPTION:
// This is how we describe describeit the function
//
// OPTIONS:
}
func ExampleAppBashComplete() {
@ -254,11 +257,11 @@ func TestApp_ParseSliceFlags(t *testing.T) {
var expectedStringSlice = []string{"8.8.8.8", "8.8.4.4"}
if !IntsEquals(parsedIntSlice, expectedIntSlice) {
t.Errorf("%s does not match %s", parsedIntSlice, expectedIntSlice)
t.Errorf("%v does not match %v", parsedIntSlice, expectedIntSlice)
}
if !StrsEquals(parsedStringSlice, expectedStringSlice) {
t.Errorf("%s does not match %s", parsedStringSlice, expectedStringSlice)
t.Errorf("%v does not match %v", parsedStringSlice, expectedStringSlice)
}
}
@ -347,6 +350,26 @@ func TestAppHelpPrinter(t *testing.T) {
}
}
func TestAppVersionPrinter(t *testing.T) {
oldPrinter := cli.VersionPrinter
defer func() {
cli.VersionPrinter = oldPrinter
}()
var wasCalled = false
cli.VersionPrinter = func(c *cli.Context) {
wasCalled = true
}
app := cli.NewApp()
ctx := cli.NewContext(app, nil, nil)
cli.ShowVersion(ctx)
if wasCalled == false {
t.Errorf("Version printer expected to be called, but was not")
}
}
func TestAppCommandNotFound(t *testing.T) {
beforeRun, subcommandRun := false, false
app := cli.NewApp()
@ -369,3 +392,32 @@ func TestAppCommandNotFound(t *testing.T) {
expect(t, beforeRun, true)
expect(t, subcommandRun, false)
}
func TestGlobalFlagsInSubcommands(t *testing.T) {
subcommandRun := false
app := cli.NewApp()
app.Flags = []cli.Flag{
cli.BoolFlag{Name: "debug, d", Usage: "Enable debugging"},
}
app.Commands = []cli.Command{
cli.Command{
Name: "foo",
Subcommands: []cli.Command{
{
Name: "bar",
Action: func(c *cli.Context) {
if c.GlobalBool("debug") {
subcommandRun = true
}
},
},
},
},
}
app.Run([]string{"command", "-d", "foo", "bar"})
expect(t, subcommandRun, true)
}

View File

@ -5,7 +5,7 @@ _cli_bash_autocomplete() {
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts=$( ${COMP_WORDS[@]:0:COMP_CWORD} --generate-bash-completion )
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
}

View File

@ -0,0 +1,5 @@
autoload -U compinit && compinit
autoload -U bashcompinit && bashcompinit
script_dir=$(dirname $0)
source ${script_dir}/bash_autocomplete

View File

@ -1,8 +1,9 @@
package cli_test
import (
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
"os"
"github.com/codegangsta/cli"
)
func Example() {
@ -47,7 +48,11 @@ func ExampleSubcommand() {
Usage: "sends a greeting in english",
Description: "greets someone in english",
Flags: []cli.Flag{
cli.StringFlag{"name", "Bob", "Name of the person to greet"},
cli.StringFlag{
Name: "name",
Value: "Bob",
Usage: "Name of the person to greet",
},
},
Action: func(c *cli.Context) {
println("Hello, ", c.String("name"))
@ -57,7 +62,11 @@ func ExampleSubcommand() {
ShortName: "sp",
Usage: "sends a greeting in spanish",
Flags: []cli.Flag{
cli.StringFlag{"surname", "Jones", "Surname of the person to greet"},
cli.StringFlag{
Name: "surname",
Value: "Jones",
Usage: "Surname of the person to greet",
},
},
Action: func(c *cli.Context) {
println("Hola, ", c.String("surname"))
@ -67,7 +76,11 @@ func ExampleSubcommand() {
ShortName: "fr",
Usage: "sends a greeting in french",
Flags: []cli.Flag{
cli.StringFlag{"nickname", "Stevie", "Nickname of the person to greet"},
cli.StringFlag{
Name: "nickname",
Value: "Stevie",
Usage: "Nickname of the person to greet",
},
},
Action: func(c *cli.Context) {
println("Bonjour, ", c.String("nickname"))

View File

@ -29,6 +29,8 @@ type Command struct {
Flags []Flag
// Treat all flags as normal arguments if true
SkipFlagParsing bool
// Boolean to hide built-in help command
HideHelp bool
}
// Invokes the command given the context, parses ctx.Args() to generate command-specific flags
@ -38,11 +40,13 @@ func (c Command) Run(ctx *Context) error {
return c.startApp(ctx)
}
// append help to flags
c.Flags = append(
c.Flags,
HelpFlag,
)
if !c.HideHelp {
// append help to flags
c.Flags = append(
c.Flags,
HelpFlag,
)
}
if ctx.App.EnableBashCompletion {
c.Flags = append(c.Flags, BashCompletionFlag)
@ -114,9 +118,13 @@ func (c Command) startApp(ctx *Context) error {
app.Usage = c.Usage
}
// set CommandNotFound
app.CommandNotFound = ctx.App.CommandNotFound
// set the flags and commands
app.Commands = c.Subcommands
app.Flags = c.Flags
app.HideHelp = c.HideHelp
// bash completion
app.EnableBashCompletion = ctx.App.EnableBashCompletion

View File

@ -2,8 +2,9 @@ package cli_test
import (
"flag"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
"testing"
"github.com/codegangsta/cli"
)
func TestCommandDoNotIgnoreFlags(t *testing.T) {

View File

@ -5,6 +5,7 @@ import (
"flag"
"strconv"
"strings"
"time"
)
// Context is a type that is passed through to
@ -29,6 +30,11 @@ func (c *Context) Int(name string) int {
return lookupInt(name, c.flagSet)
}
// Looks up the value of a local time.Duration flag, returns 0 if no time.Duration flag exists
func (c *Context) Duration(name string) time.Duration {
return lookupDuration(name, c.flagSet)
}
// Looks up the value of a local float64 flag, returns 0 if no float64 flag exists
func (c *Context) Float64(name string) float64 {
return lookupFloat64(name, c.flagSet)
@ -69,6 +75,11 @@ func (c *Context) GlobalInt(name string) int {
return lookupInt(name, c.globalSet)
}
// Looks up the value of a global time.Duration flag, returns 0 if no time.Duration flag exists
func (c *Context) GlobalDuration(name string) time.Duration {
return lookupDuration(name, c.globalSet)
}
// Looks up the value of a global bool flag, returns false if no bool flag exists
func (c *Context) GlobalBool(name string) bool {
return lookupBool(name, c.globalSet)
@ -105,6 +116,18 @@ func (c *Context) IsSet(name string) bool {
return c.setFlags[name] == true
}
// Returns a slice of flag names used in this context.
func (c *Context) FlagNames() (names []string) {
for _, flag := range c.Command.Flags {
name := strings.Split(flag.getName(), ",")[0]
if name == "help" {
continue
}
names = append(names, name)
}
return
}
type Args []string
// Returns the command line arguments associated with the context.
@ -140,6 +163,15 @@ func (a Args) Present() bool {
return len(a) != 0
}
// Swaps arguments at the given indexes
func (a Args) Swap(from, to int) error {
if from >= len(a) || to >= len(a) {
return errors.New("index out of range")
}
a[from], a[to] = a[to], a[from]
return nil
}
func lookupInt(name string, set *flag.FlagSet) int {
f := set.Lookup(name)
if f != nil {
@ -153,6 +185,18 @@ func lookupInt(name string, set *flag.FlagSet) int {
return 0
}
func lookupDuration(name string, set *flag.FlagSet) time.Duration {
f := set.Lookup(name)
if f != nil {
val, err := time.ParseDuration(f.Value.String())
if err == nil {
return val
}
}
return 0
}
func lookupFloat64(name string, set *flag.FlagSet) float64 {
f := set.Lookup(name)
if f != nil {

View File

@ -2,8 +2,10 @@ package cli_test
import (
"flag"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
"testing"
"time"
"github.com/codegangsta/cli"
)
func TestNewContext(t *testing.T) {
@ -26,6 +28,13 @@ func TestContext_Int(t *testing.T) {
expect(t, c.Int("myflag"), 12)
}
func TestContext_Duration(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Duration("myflag", time.Duration(12*time.Second), "doc")
c := cli.NewContext(nil, set, set)
expect(t, c.Duration("myflag"), time.Duration(12*time.Second))
}
func TestContext_String(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.String("myflag", "hello world", "doc")

View File

@ -3,18 +3,28 @@ package cli
import (
"flag"
"fmt"
"os"
"strconv"
"strings"
"time"
)
// This flag enables bash-completion for all commands and subcommands
var BashCompletionFlag = BoolFlag{"generate-bash-completion", ""}
var BashCompletionFlag = BoolFlag{
Name: "generate-bash-completion",
}
// This flag prints the version for the application
var VersionFlag = BoolFlag{"version, v", "print the version"}
var VersionFlag = BoolFlag{
Name: "version, v",
Usage: "print the version",
}
// This flag prints the help for all commands and subcommands
var HelpFlag = BoolFlag{"help, h", "show help"}
var HelpFlag = BoolFlag{
Name: "help, h",
Usage: "show help",
}
// Flag is a common interface related to parsing flags in cli.
// For more advanced flag parsing techniques, it is recomended that
@ -51,16 +61,24 @@ type Generic interface {
// GenericFlag is the flag type for types implementing Generic
type GenericFlag struct {
Name string
Value Generic
Usage string
Name string
Value Generic
Usage string
EnvVar string
}
func (f GenericFlag) String() string {
return fmt.Sprintf("%s%s %v\t`%v` %s", prefixFor(f.Name), f.Name, f.Value, "-"+f.Name+" option -"+f.Name+" option", f.Usage)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s%s %v\t`%v` %s", prefixFor(f.Name), f.Name, f.Value, "-"+f.Name+" option -"+f.Name+" option", f.Usage))
}
func (f GenericFlag) Apply(set *flag.FlagSet) {
val := f.Value
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
val.Set(envVal)
}
}
eachName(f.Name, func(name string) {
set.Var(f.Value, name, f.Usage)
})
@ -86,18 +104,29 @@ func (f *StringSlice) Value() []string {
}
type StringSliceFlag struct {
Name string
Value *StringSlice
Usage string
Name string
Value *StringSlice
Usage string
EnvVar string
}
func (f StringSliceFlag) String() string {
firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ")
pref := prefixFor(firstName)
return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage))
}
func (f StringSliceFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
newVal := &StringSlice{}
for _, s := range strings.Split(envVal, ",") {
newVal.Set(s)
}
f.Value = newVal
}
}
eachName(f.Name, func(name string) {
set.Var(f.Value, name, f.Usage)
})
@ -129,18 +158,32 @@ func (f *IntSlice) Value() []int {
}
type IntSliceFlag struct {
Name string
Value *IntSlice
Usage string
Name string
Value *IntSlice
Usage string
EnvVar string
}
func (f IntSliceFlag) String() string {
firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ")
pref := prefixFor(firstName)
return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage))
}
func (f IntSliceFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
newVal := &IntSlice{}
for _, s := range strings.Split(envVal, ",") {
err := newVal.Set(s)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
}
}
f.Value = newVal
}
}
eachName(f.Name, func(name string) {
set.Var(f.Value, name, f.Usage)
})
@ -151,17 +194,28 @@ func (f IntSliceFlag) getName() string {
}
type BoolFlag struct {
Name string
Usage string
Name string
Usage string
EnvVar string
}
func (f BoolFlag) String() string {
return fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage))
}
func (f BoolFlag) Apply(set *flag.FlagSet) {
val := false
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
envValBool, err := strconv.ParseBool(envVal)
if err == nil {
val = envValBool
}
}
}
eachName(f.Name, func(name string) {
set.Bool(name, false, f.Usage)
set.Bool(name, val, f.Usage)
})
}
@ -170,17 +224,28 @@ func (f BoolFlag) getName() string {
}
type BoolTFlag struct {
Name string
Usage string
Name string
Usage string
EnvVar string
}
func (f BoolTFlag) String() string {
return fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage))
}
func (f BoolTFlag) Apply(set *flag.FlagSet) {
val := true
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
envValBool, err := strconv.ParseBool(envVal)
if err == nil {
val = envValBool
}
}
}
eachName(f.Name, func(name string) {
set.Bool(name, true, f.Usage)
set.Bool(name, val, f.Usage)
})
}
@ -189,9 +254,10 @@ func (f BoolTFlag) getName() string {
}
type StringFlag struct {
Name string
Value string
Usage string
Name string
Value string
Usage string
EnvVar string
}
func (f StringFlag) String() string {
@ -204,10 +270,16 @@ func (f StringFlag) String() string {
fmtString = "%s %v\t%v"
}
return fmt.Sprintf(fmtString, prefixedNames(f.Name), f.Value, f.Usage)
return withEnvHint(f.EnvVar, fmt.Sprintf(fmtString, prefixedNames(f.Name), f.Value, f.Usage))
}
func (f StringFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
f.Value = envVal
}
}
eachName(f.Name, func(name string) {
set.String(name, f.Value, f.Usage)
})
@ -218,16 +290,26 @@ func (f StringFlag) getName() string {
}
type IntFlag struct {
Name string
Value int
Usage string
Name string
Value int
Usage string
EnvVar string
}
func (f IntFlag) String() string {
return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage))
}
func (f IntFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
envValInt, err := strconv.ParseUint(envVal, 10, 64)
if err == nil {
f.Value = int(envValInt)
}
}
}
eachName(f.Name, func(name string) {
set.Int(name, f.Value, f.Usage)
})
@ -237,17 +319,57 @@ func (f IntFlag) getName() string {
return f.Name
}
type DurationFlag struct {
Name string
Value time.Duration
Usage string
EnvVar string
}
func (f DurationFlag) String() string {
return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage))
}
func (f DurationFlag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
envValDuration, err := time.ParseDuration(envVal)
if err == nil {
f.Value = envValDuration
}
}
}
eachName(f.Name, func(name string) {
set.Duration(name, f.Value, f.Usage)
})
}
func (f DurationFlag) getName() string {
return f.Name
}
type Float64Flag struct {
Name string
Value float64
Usage string
Name string
Value float64
Usage string
EnvVar string
}
func (f Float64Flag) String() string {
return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage)
return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage))
}
func (f Float64Flag) Apply(set *flag.FlagSet) {
if f.EnvVar != "" {
if envVal := os.Getenv(f.EnvVar); envVal != "" {
envValFloat, err := strconv.ParseFloat(envVal, 10)
if err == nil {
f.Value = float64(envValFloat)
}
}
}
eachName(f.Name, func(name string) {
set.Float64(name, f.Value, f.Usage)
})
@ -278,3 +400,11 @@ func prefixedNames(fullName string) (prefixed string) {
}
return
}
func withEnvHint(envVar, str string) string {
envText := ""
if envVar != "" {
envText = fmt.Sprintf(" [$%s]", envVar)
}
return str + envText
}

View File

@ -1,12 +1,13 @@
package cli_test
import (
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
"fmt"
"os"
"reflect"
"strings"
"testing"
"github.com/codegangsta/cli"
)
var boolFlagTests = []struct {
@ -52,6 +53,71 @@ func TestStringFlagHelpOutput(t *testing.T) {
}
}
func TestStringFlagWithEnvVarHelpOutput(t *testing.T) {
os.Setenv("APP_FOO", "derp")
for _, test := range stringFlagTests {
flag := cli.StringFlag{Name: test.name, Value: test.value, EnvVar: "APP_FOO"}
output := flag.String()
if !strings.HasSuffix(output, " [$APP_FOO]") {
t.Errorf("%s does not end with [$APP_FOO]", output)
}
}
}
var stringSliceFlagTests = []struct {
name string
value *cli.StringSlice
expected string
}{
{"help", func() *cli.StringSlice {
s := &cli.StringSlice{}
s.Set("")
return s
}(), "--help '--help option --help option'\t"},
{"h", func() *cli.StringSlice {
s := &cli.StringSlice{}
s.Set("")
return s
}(), "-h '-h option -h option'\t"},
{"h", func() *cli.StringSlice {
s := &cli.StringSlice{}
s.Set("")
return s
}(), "-h '-h option -h option'\t"},
{"test", func() *cli.StringSlice {
s := &cli.StringSlice{}
s.Set("Something")
return s
}(), "--test '--test option --test option'\t"},
}
func TestStringSliceFlagHelpOutput(t *testing.T) {
for _, test := range stringSliceFlagTests {
flag := cli.StringSliceFlag{Name: test.name, Value: test.value}
output := flag.String()
if output != test.expected {
t.Errorf("%q does not match %q", output, test.expected)
}
}
}
func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) {
os.Setenv("APP_QWWX", "11,4")
for _, test := range stringSliceFlagTests {
flag := cli.StringSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_QWWX"}
output := flag.String()
if !strings.HasSuffix(output, " [$APP_QWWX]") {
t.Errorf("%q does not end with [$APP_QWWX]", output)
}
}
}
var intFlagTests = []struct {
name string
expected string
@ -72,6 +138,92 @@ func TestIntFlagHelpOutput(t *testing.T) {
}
}
func TestIntFlagWithEnvVarHelpOutput(t *testing.T) {
os.Setenv("APP_BAR", "2")
for _, test := range intFlagTests {
flag := cli.IntFlag{Name: test.name, EnvVar: "APP_BAR"}
output := flag.String()
if !strings.HasSuffix(output, " [$APP_BAR]") {
t.Errorf("%s does not end with [$APP_BAR]", output)
}
}
}
var durationFlagTests = []struct {
name string
expected string
}{
{"help", "--help '0'\t"},
{"h", "-h '0'\t"},
}
func TestDurationFlagHelpOutput(t *testing.T) {
for _, test := range durationFlagTests {
flag := cli.DurationFlag{Name: test.name}
output := flag.String()
if output != test.expected {
t.Errorf("%s does not match %s", output, test.expected)
}
}
}
func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) {
os.Setenv("APP_BAR", "2h3m6s")
for _, test := range durationFlagTests {
flag := cli.DurationFlag{Name: test.name, EnvVar: "APP_BAR"}
output := flag.String()
if !strings.HasSuffix(output, " [$APP_BAR]") {
t.Errorf("%s does not end with [$APP_BAR]", output)
}
}
}
var intSliceFlagTests = []struct {
name string
value *cli.IntSlice
expected string
}{
{"help", &cli.IntSlice{}, "--help '--help option --help option'\t"},
{"h", &cli.IntSlice{}, "-h '-h option -h option'\t"},
{"h", &cli.IntSlice{}, "-h '-h option -h option'\t"},
{"test", func() *cli.IntSlice {
i := &cli.IntSlice{}
i.Set("9")
return i
}(), "--test '--test option --test option'\t"},
}
func TestIntSliceFlagHelpOutput(t *testing.T) {
for _, test := range intSliceFlagTests {
flag := cli.IntSliceFlag{Name: test.name, Value: test.value}
output := flag.String()
if output != test.expected {
t.Errorf("%q does not match %q", output, test.expected)
}
}
}
func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) {
os.Setenv("APP_SMURF", "42,3")
for _, test := range intSliceFlagTests {
flag := cli.IntSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_SMURF"}
output := flag.String()
if !strings.HasSuffix(output, " [$APP_SMURF]") {
t.Errorf("%q does not end with [$APP_SMURF]", output)
}
}
}
var float64FlagTests = []struct {
name string
expected string
@ -92,6 +244,54 @@ func TestFloat64FlagHelpOutput(t *testing.T) {
}
}
func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) {
os.Setenv("APP_BAZ", "99.4")
for _, test := range float64FlagTests {
flag := cli.Float64Flag{Name: test.name, EnvVar: "APP_BAZ"}
output := flag.String()
if !strings.HasSuffix(output, " [$APP_BAZ]") {
t.Errorf("%s does not end with [$APP_BAZ]", output)
}
}
}
var genericFlagTests = []struct {
name string
value cli.Generic
expected string
}{
{"help", &Parser{}, "--help <nil>\t`-help option -help option` "},
{"h", &Parser{}, "-h <nil>\t`-h option -h option` "},
{"test", &Parser{}, "--test <nil>\t`-test option -test option` "},
}
func TestGenericFlagHelpOutput(t *testing.T) {
for _, test := range genericFlagTests {
flag := cli.GenericFlag{Name: test.name}
output := flag.String()
if output != test.expected {
t.Errorf("%q does not match %q", output, test.expected)
}
}
}
func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) {
os.Setenv("APP_ZAP", "3")
for _, test := range genericFlagTests {
flag := cli.GenericFlag{Name: test.name, EnvVar: "APP_ZAP"}
output := flag.String()
if !strings.HasSuffix(output, " [$APP_ZAP]") {
t.Errorf("%s does not end with [$APP_ZAP]", output)
}
}
}
func TestParseMultiString(t *testing.T) {
(&cli.App{
Flags: []cli.Flag{
@ -108,6 +308,23 @@ func TestParseMultiString(t *testing.T) {
}).Run([]string{"run", "-s", "10"})
}
func TestParseMultiStringFromEnv(t *testing.T) {
os.Setenv("APP_COUNT", "20")
(&cli.App{
Flags: []cli.Flag{
cli.StringFlag{Name: "count, c", EnvVar: "APP_COUNT"},
},
Action: func(ctx *cli.Context) {
if ctx.String("count") != "20" {
t.Errorf("main name not set")
}
if ctx.String("c") != "20" {
t.Errorf("short name not set")
}
},
}).Run([]string{"run"})
}
func TestParseMultiStringSlice(t *testing.T) {
(&cli.App{
Flags: []cli.Flag{
@ -124,6 +341,24 @@ func TestParseMultiStringSlice(t *testing.T) {
}).Run([]string{"run", "-s", "10", "-s", "20"})
}
func TestParseMultiStringSliceFromEnv(t *testing.T) {
os.Setenv("APP_INTERVALS", "20,30,40")
(&cli.App{
Flags: []cli.Flag{
cli.StringSliceFlag{Name: "intervals, i", Value: &cli.StringSlice{}, EnvVar: "APP_INTERVALS"},
},
Action: func(ctx *cli.Context) {
if !reflect.DeepEqual(ctx.StringSlice("intervals"), []string{"20", "30", "40"}) {
t.Errorf("main name not set from env")
}
if !reflect.DeepEqual(ctx.StringSlice("i"), []string{"20", "30", "40"}) {
t.Errorf("short name not set from env")
}
},
}).Run([]string{"run"})
}
func TestParseMultiInt(t *testing.T) {
a := cli.App{
Flags: []cli.Flag{
@ -141,6 +376,93 @@ func TestParseMultiInt(t *testing.T) {
a.Run([]string{"run", "-s", "10"})
}
func TestParseMultiIntFromEnv(t *testing.T) {
os.Setenv("APP_TIMEOUT_SECONDS", "10")
a := cli.App{
Flags: []cli.Flag{
cli.IntFlag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"},
},
Action: func(ctx *cli.Context) {
if ctx.Int("timeout") != 10 {
t.Errorf("main name not set")
}
if ctx.Int("t") != 10 {
t.Errorf("short name not set")
}
},
}
a.Run([]string{"run"})
}
func TestParseMultiIntSlice(t *testing.T) {
(&cli.App{
Flags: []cli.Flag{
cli.IntSliceFlag{Name: "serve, s", Value: &cli.IntSlice{}},
},
Action: func(ctx *cli.Context) {
if !reflect.DeepEqual(ctx.IntSlice("serve"), []int{10, 20}) {
t.Errorf("main name not set")
}
if !reflect.DeepEqual(ctx.IntSlice("s"), []int{10, 20}) {
t.Errorf("short name not set")
}
},
}).Run([]string{"run", "-s", "10", "-s", "20"})
}
func TestParseMultiIntSliceFromEnv(t *testing.T) {
os.Setenv("APP_INTERVALS", "20,30,40")
(&cli.App{
Flags: []cli.Flag{
cli.IntSliceFlag{Name: "intervals, i", Value: &cli.IntSlice{}, EnvVar: "APP_INTERVALS"},
},
Action: func(ctx *cli.Context) {
if !reflect.DeepEqual(ctx.IntSlice("intervals"), []int{20, 30, 40}) {
t.Errorf("main name not set from env")
}
if !reflect.DeepEqual(ctx.IntSlice("i"), []int{20, 30, 40}) {
t.Errorf("short name not set from env")
}
},
}).Run([]string{"run"})
}
func TestParseMultiFloat64(t *testing.T) {
a := cli.App{
Flags: []cli.Flag{
cli.Float64Flag{Name: "serve, s"},
},
Action: func(ctx *cli.Context) {
if ctx.Float64("serve") != 10.2 {
t.Errorf("main name not set")
}
if ctx.Float64("s") != 10.2 {
t.Errorf("short name not set")
}
},
}
a.Run([]string{"run", "-s", "10.2"})
}
func TestParseMultiFloat64FromEnv(t *testing.T) {
os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
a := cli.App{
Flags: []cli.Flag{
cli.Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"},
},
Action: func(ctx *cli.Context) {
if ctx.Float64("timeout") != 15.5 {
t.Errorf("main name not set")
}
if ctx.Float64("t") != 15.5 {
t.Errorf("short name not set")
}
},
}
a.Run([]string{"run"})
}
func TestParseMultiBool(t *testing.T) {
a := cli.App{
Flags: []cli.Flag{
@ -158,6 +480,59 @@ func TestParseMultiBool(t *testing.T) {
a.Run([]string{"run", "--serve"})
}
func TestParseMultiBoolFromEnv(t *testing.T) {
os.Setenv("APP_DEBUG", "1")
a := cli.App{
Flags: []cli.Flag{
cli.BoolFlag{Name: "debug, d", EnvVar: "APP_DEBUG"},
},
Action: func(ctx *cli.Context) {
if ctx.Bool("debug") != true {
t.Errorf("main name not set from env")
}
if ctx.Bool("d") != true {
t.Errorf("short name not set from env")
}
},
}
a.Run([]string{"run"})
}
func TestParseMultiBoolT(t *testing.T) {
a := cli.App{
Flags: []cli.Flag{
cli.BoolTFlag{Name: "serve, s"},
},
Action: func(ctx *cli.Context) {
if ctx.BoolT("serve") != true {
t.Errorf("main name not set")
}
if ctx.BoolT("s") != true {
t.Errorf("short name not set")
}
},
}
a.Run([]string{"run", "--serve"})
}
func TestParseMultiBoolTFromEnv(t *testing.T) {
os.Setenv("APP_DEBUG", "0")
a := cli.App{
Flags: []cli.Flag{
cli.BoolTFlag{Name: "debug, d", EnvVar: "APP_DEBUG"},
},
Action: func(ctx *cli.Context) {
if ctx.BoolT("debug") != false {
t.Errorf("main name not set from env")
}
if ctx.BoolT("d") != false {
t.Errorf("short name not set from env")
}
},
}
a.Run([]string{"run"})
}
type Parser [2]string
func (p *Parser) Set(value string) error {
@ -192,3 +567,21 @@ func TestParseGeneric(t *testing.T) {
}
a.Run([]string{"run", "-s", "10,20"})
}
func TestParseGenericFromEnv(t *testing.T) {
os.Setenv("APP_SERVE", "20,30")
a := cli.App{
Flags: []cli.Flag{
cli.GenericFlag{Name: "serve, s", Value: &Parser{}, EnvVar: "APP_SERVE"},
},
Action: func(ctx *cli.Context) {
if !reflect.DeepEqual(ctx.Generic("serve"), &Parser{"20", "30"}) {
t.Errorf("main name not set from env")
}
if !reflect.DeepEqual(ctx.Generic("s"), &Parser{"20", "30"}) {
t.Errorf("short name not set from env")
}
},
}
a.Run([]string{"run"})
}

View File

@ -14,17 +14,21 @@ var AppHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{.Name}} [global options] command [command options] [arguments...]
{{.Name}} {{if .Flags}}[global options] {{end}}command{{if .Flags}} [command options]{{end}} [arguments...]
VERSION:
{{.Version}}
{{.Version}}{{if or .Author .Email}}
AUTHOR:{{if .Author}}
{{.Author}}{{if .Email}} - <{{.Email}}>{{end}}{{else}}
{{.Email}}{{end}}{{end}}
COMMANDS:
{{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}
{{end}}
{{end}}{{if .Flags}}
GLOBAL OPTIONS:
{{range .Flags}}{{.}}
{{end}}
{{end}}{{end}}
`
// The text template for the command help topic.
@ -34,14 +38,14 @@ var CommandHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
USAGE:
command {{.Name}} [command options] [arguments...]
command {{.Name}}{{if .Flags}} [command options]{{end}} [arguments...]{{if .Description}}
DESCRIPTION:
{{.Description}}
{{.Description}}{{end}}{{if .Flags}}
OPTIONS:
{{range .Flags}}{{.}}
{{end}}
{{end}}{{ end }}
`
// The text template for the subcommand help topic.
@ -51,14 +55,14 @@ var SubcommandHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{.Name}} [global options] command [command options] [arguments...]
{{.Name}} command{{if .Flags}} [command options]{{end}} [arguments...]
COMMANDS:
{{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}
{{end}}
{{end}}{{if .Flags}}
OPTIONS:
{{range .Flags}}{{.}}
{{end}}
{{end}}{{end}}
`
var helpCommand = Command{
@ -92,6 +96,9 @@ var helpSubcommand = Command{
// Prints help for the App
var HelpPrinter = printHelp
// Prints version for the App
var VersionPrinter = printVersion
func ShowAppHelp(c *Context) {
HelpPrinter(AppHelpTemplate, c.App)
}
@ -129,6 +136,10 @@ func ShowSubcommandHelp(c *Context) {
// Prints the version number of the App
func ShowVersion(c *Context) {
VersionPrinter(c)
}
func printVersion(c *Context) {
fmt.Printf("%v version %v\n", c.App.Name, c.App.Version)
}

View File

@ -1,5 +1,5 @@
# Use goreman to run `go get github.com/mattn/goreman`
etcd1: bin/etcd -name node1 -listen-client-urls http://127.0.0.1:4001 -advertise-client-urls http://127.0.0.1:4001 -listen-peer-urls http://127.0.0.1:7001 -initial-advertise-peer-urls http://127.0.0.1:7001 -initial-cluster 'node1=http://localhost:7001,node2=http://localhost:7002,node3=http://localhost:7003' -initial-cluster-state new
etcd2: bin/etcd -name node2 -listen-client-urls http://127.0.0.1:4002 -advertise-client-urls http://127.0.0.1:4002 -listen-peer-urls http://127.0.0.1:7002 -initial-advertise-peer-urls http://127.0.0.1:7002 -initial-cluster 'node1=http://localhost:7001,node2=http://localhost:7002,node3=http://localhost:7003' -initial-cluster-state new
etcd3: bin/etcd -name node3 -listen-client-urls http://127.0.0.1:4003 -advertise-client-urls http://127.0.0.1:4003 -listen-peer-urls http://127.0.0.1:7003 -initial-advertise-peer-urls http://127.0.0.1:7003 -initial-cluster 'node1=http://localhost:7001,node2=http://localhost:7002,node3=http://localhost:7003' -initial-cluster-state new
proxy: bin/etcd -proxy=on -bind-addr 127.0.0.1:8080 -initial-cluster 'node1=http://localhost:7001,node2=http://localhost:7002,node3=http://localhost:7003'
etcd1: bin/etcd -name infra1 -listen-client-urls http://localhost:4001 -advertise-client-urls http://localhost:4001 -listen-peer-urls http://localhost:7001 -initial-advertise-peer-urls http://localhost:7001 -initial-cluster-token etcd-cluster-1 -initial-cluster 'infra1=http://localhost:7001,infra2=http://localhost:7002,infra3=http://localhost:7003' -initial-cluster-state new
etcd2: bin/etcd -name infra2 -listen-client-urls http://localhost:4002 -advertise-client-urls http://localhost:4002 -listen-peer-urls http://localhost:7002 -initial-advertise-peer-urls http://localhost:7002 -initial-cluster-token etcd-cluster-1 -initial-cluster 'infra1=http://localhost:7001,infra2=http://localhost:7002,infra3=http://localhost:7003' -initial-cluster-state new
etcd3: bin/etcd -name infra3 -listen-client-urls http://localhost:4003 -advertise-client-urls http://localhost:4003 -listen-peer-urls http://localhost:7003 -initial-advertise-peer-urls http://localhost:7003 -initial-cluster-token etcd-cluster-1 -initial-cluster 'infra1=http://localhost:7001,infra2=http://localhost:7002,infra3=http://localhost:7003' -initial-cluster-state new
proxy: bin/etcd -proxy=on -bind-addr 127.0.0.1:8080 -initial-cluster 'infra1=http://localhost:7001,infra2=http://localhost:7002,infra3=http://localhost:7003'

View File

@ -1,6 +1,7 @@
# etcd
[![Build Status](https://travis-ci.org/coreos/etcd.png?branch=master)](https://travis-ci.org/coreos/etcd)
[![Docker Repository on Quay.io](https://quay.io/repository/coreos/etcd-git/status "Docker Repository on Quay.io")](https://quay.io/repository/coreos/etcd-git)
### WARNING ###

1
build
View File

@ -13,3 +13,4 @@ eval $(go env)
go build -o bin/etcd ${REPO_PATH}
go build -o bin/etcdctl ${REPO_PATH}/etcdctl
go build -o bin/etcd-migrate ${REPO_PATH}/migrate/cmd/etcd-migrate

View File

@ -17,6 +17,8 @@
package client
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
@ -26,20 +28,117 @@ import (
)
var (
ErrTimeout = context.DeadlineExceeded
ErrTimeout = context.DeadlineExceeded
ErrCanceled = context.Canceled
ErrNoEndpoints = errors.New("no endpoints available")
ErrTooManyRedirects = errors.New("too many redirects")
DefaultRequestTimeout = 5 * time.Second
DefaultMaxRedirects = 10
)
// transport mimics http.Transport to provide an interface which can be
type SyncableHTTPClient interface {
HTTPClient
Sync(context.Context) error
Endpoints() []string
}
type HTTPClient interface {
Do(context.Context, HTTPAction) (*http.Response, []byte, error)
}
type HTTPAction interface {
HTTPRequest(url.URL) *http.Request
}
// CancelableTransport mimics http.Transport to provide an interface which can be
// substituted for testing (since the RoundTripper interface alone does not
// require the CancelRequest method)
type transport interface {
type CancelableTransport interface {
http.RoundTripper
CancelRequest(req *http.Request)
}
type httpAction interface {
httpRequest(url.URL) *http.Request
func NewHTTPClient(tr CancelableTransport, eps []string) (SyncableHTTPClient, error) {
return newHTTPClusterClient(tr, eps)
}
func newHTTPClusterClient(tr CancelableTransport, eps []string) (*httpClusterClient, error) {
c := httpClusterClient{
transport: tr,
endpoints: eps,
clients: make([]HTTPClient, len(eps)),
}
for i, ep := range eps {
u, err := url.Parse(ep)
if err != nil {
return nil, err
}
c.clients[i] = &redirectFollowingHTTPClient{
max: DefaultMaxRedirects,
client: &httpClient{
transport: tr,
endpoint: *u,
},
}
}
return &c, nil
}
type httpClusterClient struct {
transport CancelableTransport
endpoints []string
clients []HTTPClient
}
func (c *httpClusterClient) Do(ctx context.Context, act HTTPAction) (resp *http.Response, body []byte, err error) {
if len(c.clients) == 0 {
return nil, nil, ErrNoEndpoints
}
for _, hc := range c.clients {
resp, body, err = hc.Do(ctx, act)
if err != nil {
if err == ErrTimeout || err == ErrCanceled {
return nil, nil, err
}
continue
}
if resp.StatusCode/100 == 5 {
continue
}
break
}
return
}
func (c *httpClusterClient) Endpoints() []string {
return c.endpoints
}
func (c *httpClusterClient) Sync(ctx context.Context) error {
mAPI := NewMembersAPI(c)
ms, err := mAPI.List(ctx)
if err != nil {
return err
}
eps := make([]string, 0)
for _, m := range ms {
eps = append(eps, m.ClientURLs...)
}
if len(eps) == 0 {
return ErrNoEndpoints
}
nc, err := newHTTPClusterClient(c.transport, eps)
if err != nil {
return err
}
*c = *nc
return nil
}
type roundTripResponse struct {
@ -48,34 +147,12 @@ type roundTripResponse struct {
}
type httpClient struct {
transport transport
transport CancelableTransport
endpoint url.URL
timeout time.Duration
}
func newHTTPClient(tr *http.Transport, ep string, to time.Duration) (*httpClient, error) {
u, err := url.Parse(ep)
if err != nil {
return nil, err
}
c := &httpClient{
transport: tr,
endpoint: *u,
timeout: to,
}
return c, nil
}
func (c *httpClient) doWithTimeout(act httpAction) (*http.Response, []byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
return c.do(ctx, act)
}
func (c *httpClient) do(ctx context.Context, act httpAction) (*http.Response, []byte, error) {
req := act.httpRequest(c.endpoint)
func (c *httpClient) Do(ctx context.Context, act HTTPAction) (*http.Response, []byte, error) {
req := act.HTTPRequest(c.endpoint)
rtchan := make(chan roundTripResponse, 1)
go func() {
@ -112,3 +189,45 @@ func (c *httpClient) do(ctx context.Context, act httpAction) (*http.Response, []
body, err := ioutil.ReadAll(resp.Body)
return resp, body, err
}
type redirectFollowingHTTPClient struct {
client HTTPClient
max int
}
func (r *redirectFollowingHTTPClient) Do(ctx context.Context, act HTTPAction) (*http.Response, []byte, error) {
for i := 0; i <= r.max; i++ {
resp, body, err := r.client.Do(ctx, act)
if err != nil {
return nil, nil, err
}
if resp.StatusCode/100 == 3 {
hdr := resp.Header.Get("Location")
if hdr == "" {
return nil, nil, fmt.Errorf("Location header not set")
}
loc, err := url.Parse(hdr)
if err != nil {
return nil, nil, fmt.Errorf("Location header not valid URL: %s", hdr)
}
act = &redirectedHTTPAction{
action: act,
location: *loc,
}
continue
}
return resp, body, nil
}
return nil, nil, ErrTooManyRedirects
}
type redirectedHTTPAction struct {
action HTTPAction
location url.URL
}
func (r *redirectedHTTPAction) HTTPRequest(ep url.URL) *http.Request {
orig := r.action.HTTPRequest(ep)
orig.URL = &r.location
return orig
}

View File

@ -29,6 +29,39 @@ import (
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
)
type staticHTTPClient struct {
resp http.Response
err error
}
func (s *staticHTTPClient) Do(context.Context, HTTPAction) (*http.Response, []byte, error) {
return &s.resp, nil, s.err
}
type staticHTTPAction struct {
request http.Request
}
type staticHTTPResponse struct {
resp http.Response
err error
}
func (s *staticHTTPAction) HTTPRequest(url.URL) *http.Request {
return &s.request
}
type multiStaticHTTPClient struct {
responses []staticHTTPResponse
cur int
}
func (s *multiStaticHTTPClient) Do(context.Context, HTTPAction) (*http.Response, []byte, error) {
r := s.responses[s.cur]
s.cur++
return &r.resp, nil, r.err
}
type fakeTransport struct {
respchan chan *http.Response
errchan chan error
@ -65,7 +98,7 @@ func (t *fakeTransport) CancelRequest(*http.Request) {
type fakeAction struct{}
func (a *fakeAction) httpRequest(url.URL) *http.Request {
func (a *fakeAction) HTTPRequest(url.URL) *http.Request {
return &http.Request{}
}
@ -78,7 +111,7 @@ func TestHTTPClientDoSuccess(t *testing.T) {
Body: ioutil.NopCloser(strings.NewReader("foo")),
}
resp, body, err := c.do(context.Background(), &fakeAction{})
resp, body, err := c.Do(context.Background(), &fakeAction{})
if err != nil {
t.Fatalf("incorrect error value: want=nil got=%v", err)
}
@ -100,7 +133,7 @@ func TestHTTPClientDoError(t *testing.T) {
tr.errchan <- errors.New("fixture")
_, _, err := c.do(context.Background(), &fakeAction{})
_, _, err := c.Do(context.Background(), &fakeAction{})
if err == nil {
t.Fatalf("expected non-nil error, got nil")
}
@ -113,7 +146,7 @@ func TestHTTPClientDoCancelContext(t *testing.T) {
tr.startCancel <- struct{}{}
tr.finishCancel <- struct{}{}
_, _, err := c.do(context.Background(), &fakeAction{})
_, _, err := c.Do(context.Background(), &fakeAction{})
if err == nil {
t.Fatalf("expected non-nil error, got nil")
}
@ -126,7 +159,7 @@ func TestHTTPClientDoCancelContextWaitForRoundTrip(t *testing.T) {
donechan := make(chan struct{})
ctx, cancel := context.WithCancel(context.Background())
go func() {
c.do(ctx, &fakeAction{})
c.Do(ctx, &fakeAction{})
close(donechan)
}()
@ -149,3 +182,304 @@ func TestHTTPClientDoCancelContextWaitForRoundTrip(t *testing.T) {
t.Fatalf("httpClient.do did not exit within 1s")
}
}
func TestHTTPClusterClientDo(t *testing.T) {
fakeErr := errors.New("fake!")
tests := []struct {
client *httpClusterClient
wantCode int
wantErr error
}{
// first good response short-circuits Do
{
client: &httpClusterClient{
clients: []HTTPClient{
&staticHTTPClient{resp: http.Response{StatusCode: http.StatusTeapot}},
&staticHTTPClient{err: fakeErr},
},
},
wantCode: http.StatusTeapot,
},
// fall through to good endpoint if err is arbitrary
{
client: &httpClusterClient{
clients: []HTTPClient{
&staticHTTPClient{err: fakeErr},
&staticHTTPClient{resp: http.Response{StatusCode: http.StatusTeapot}},
},
},
wantCode: http.StatusTeapot,
},
// ErrTimeout short-circuits Do
{
client: &httpClusterClient{
clients: []HTTPClient{
&staticHTTPClient{err: ErrTimeout},
&staticHTTPClient{resp: http.Response{StatusCode: http.StatusTeapot}},
},
},
wantErr: ErrTimeout,
},
// ErrCanceled short-circuits Do
{
client: &httpClusterClient{
clients: []HTTPClient{
&staticHTTPClient{err: ErrCanceled},
&staticHTTPClient{resp: http.Response{StatusCode: http.StatusTeapot}},
},
},
wantErr: ErrCanceled,
},
// return err if there are no endpoints
{
client: &httpClusterClient{
clients: []HTTPClient{},
},
wantErr: ErrNoEndpoints,
},
// return err if all endpoints return arbitrary errors
{
client: &httpClusterClient{
clients: []HTTPClient{
&staticHTTPClient{err: fakeErr},
&staticHTTPClient{err: fakeErr},
},
},
wantErr: fakeErr,
},
// 500-level errors cause Do to fallthrough to next endpoint
{
client: &httpClusterClient{
clients: []HTTPClient{
&staticHTTPClient{resp: http.Response{StatusCode: http.StatusBadGateway}},
&staticHTTPClient{resp: http.Response{StatusCode: http.StatusTeapot}},
},
},
wantCode: http.StatusTeapot,
},
}
for i, tt := range tests {
resp, _, err := tt.client.Do(context.Background(), nil)
if !reflect.DeepEqual(tt.wantErr, err) {
t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr)
continue
}
if resp == nil {
if tt.wantCode != 0 {
t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode)
}
continue
}
if resp.StatusCode != tt.wantCode {
t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode)
continue
}
}
}
func TestRedirectedHTTPAction(t *testing.T) {
act := &redirectedHTTPAction{
action: &staticHTTPAction{
request: http.Request{
Method: "DELETE",
URL: &url.URL{
Scheme: "https",
Host: "foo.example.com",
Path: "/ping",
},
},
},
location: url.URL{
Scheme: "https",
Host: "bar.example.com",
Path: "/pong",
},
}
want := &http.Request{
Method: "DELETE",
URL: &url.URL{
Scheme: "https",
Host: "bar.example.com",
Path: "/pong",
},
}
got := act.HTTPRequest(url.URL{Scheme: "http", Host: "baz.example.com", Path: "/pang"})
if !reflect.DeepEqual(want, got) {
t.Fatalf("HTTPRequest is %#v, want %#v", want, got)
}
}
func TestRedirectFollowingHTTPClient(t *testing.T) {
tests := []struct {
max int
client HTTPClient
wantCode int
wantErr error
}{
// errors bubbled up
{
max: 2,
client: &multiStaticHTTPClient{
responses: []staticHTTPResponse{
staticHTTPResponse{
err: errors.New("fail!"),
},
},
},
wantErr: errors.New("fail!"),
},
// no need to follow redirect if none given
{
max: 2,
client: &multiStaticHTTPClient{
responses: []staticHTTPResponse{
staticHTTPResponse{
resp: http.Response{
StatusCode: http.StatusTeapot,
},
},
},
},
wantCode: http.StatusTeapot,
},
// redirects if less than max
{
max: 2,
client: &multiStaticHTTPClient{
responses: []staticHTTPResponse{
staticHTTPResponse{
resp: http.Response{
StatusCode: http.StatusTemporaryRedirect,
Header: http.Header{"Location": []string{"http://example.com"}},
},
},
staticHTTPResponse{
resp: http.Response{
StatusCode: http.StatusTeapot,
},
},
},
},
wantCode: http.StatusTeapot,
},
// succeed after reaching max redirects
{
max: 2,
client: &multiStaticHTTPClient{
responses: []staticHTTPResponse{
staticHTTPResponse{
resp: http.Response{
StatusCode: http.StatusTemporaryRedirect,
Header: http.Header{"Location": []string{"http://example.com"}},
},
},
staticHTTPResponse{
resp: http.Response{
StatusCode: http.StatusTemporaryRedirect,
Header: http.Header{"Location": []string{"http://example.com"}},
},
},
staticHTTPResponse{
resp: http.Response{
StatusCode: http.StatusTeapot,
},
},
},
},
wantCode: http.StatusTeapot,
},
// fail at max+1 redirects
{
max: 1,
client: &multiStaticHTTPClient{
responses: []staticHTTPResponse{
staticHTTPResponse{
resp: http.Response{
StatusCode: http.StatusTemporaryRedirect,
Header: http.Header{"Location": []string{"http://example.com"}},
},
},
staticHTTPResponse{
resp: http.Response{
StatusCode: http.StatusTemporaryRedirect,
Header: http.Header{"Location": []string{"http://example.com"}},
},
},
staticHTTPResponse{
resp: http.Response{
StatusCode: http.StatusTeapot,
},
},
},
},
wantErr: ErrTooManyRedirects,
},
// fail if Location header not set
{
max: 1,
client: &multiStaticHTTPClient{
responses: []staticHTTPResponse{
staticHTTPResponse{
resp: http.Response{
StatusCode: http.StatusTemporaryRedirect,
},
},
},
},
wantErr: errors.New("Location header not set"),
},
// fail if Location header is invalid
{
max: 1,
client: &multiStaticHTTPClient{
responses: []staticHTTPResponse{
staticHTTPResponse{
resp: http.Response{
StatusCode: http.StatusTemporaryRedirect,
Header: http.Header{"Location": []string{":"}},
},
},
},
},
wantErr: errors.New("Location header not valid URL: :"),
},
}
for i, tt := range tests {
client := &redirectFollowingHTTPClient{client: tt.client, max: tt.max}
resp, _, err := client.Do(context.Background(), nil)
if !reflect.DeepEqual(tt.wantErr, err) {
t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr)
continue
}
if resp == nil {
if tt.wantCode != 0 {
t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode)
}
continue
}
if resp.StatusCode != tt.wantCode {
t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode)
continue
}
}
}

View File

@ -41,36 +41,30 @@ var (
ErrKeyExists = errors.New("client: key already exists")
)
func NewKeysAPI(tr *http.Transport, ep string, to time.Duration) (KeysAPI, error) {
return newHTTPKeysAPIWithPrefix(tr, ep, to, DefaultV2KeysPrefix)
}
func NewDiscoveryKeysAPI(tr *http.Transport, ep string, to time.Duration) (KeysAPI, error) {
return newHTTPKeysAPIWithPrefix(tr, ep, to, "")
}
func newHTTPKeysAPIWithPrefix(tr *http.Transport, ep string, to time.Duration, prefix string) (*HTTPKeysAPI, error) {
c, err := newHTTPClient(tr, ep, to)
if err != nil {
return nil, err
}
kAPI := HTTPKeysAPI{
func NewKeysAPI(c HTTPClient) KeysAPI {
return &httpKeysAPI{
client: c,
prefix: DefaultV2KeysPrefix,
}
}
return &kAPI, nil
func NewDiscoveryKeysAPI(c HTTPClient) KeysAPI {
return &httpKeysAPI{
client: c,
prefix: "",
}
}
type KeysAPI interface {
Create(key, value string, ttl time.Duration) (*Response, error)
Get(key string) (*Response, error)
Create(ctx context.Context, key, value string, ttl time.Duration) (*Response, error)
Get(ctx context.Context, key string) (*Response, error)
Watch(key string, idx uint64) Watcher
RecursiveWatch(key string, idx uint64) Watcher
}
type Watcher interface {
Next() (*Response, error)
Next(context.Context) (*Response, error)
}
type Response struct {
@ -92,46 +86,50 @@ func (n *Node) String() string {
return fmt.Sprintf("{Key: %s, CreatedIndex: %d, ModifiedIndex: %d}", n.Key, n.CreatedIndex, n.ModifiedIndex)
}
type HTTPKeysAPI struct {
client *httpClient
type httpKeysAPI struct {
client HTTPClient
prefix string
}
func (k *HTTPKeysAPI) Create(key, val string, ttl time.Duration) (*Response, error) {
func (k *httpKeysAPI) Create(ctx context.Context, key, val string, ttl time.Duration) (*Response, error) {
create := &createAction{
Key: key,
Value: val,
Prefix: k.prefix,
Key: key,
Value: val,
}
if ttl >= 0 {
uttl := uint64(ttl.Seconds())
create.TTL = &uttl
}
httpresp, body, err := k.client.doWithTimeout(create)
resp, body, err := k.client.Do(ctx, create)
if err != nil {
return nil, err
}
return unmarshalHTTPResponse(httpresp.StatusCode, body)
return unmarshalHTTPResponse(resp.StatusCode, body)
}
func (k *HTTPKeysAPI) Get(key string) (*Response, error) {
func (k *httpKeysAPI) Get(ctx context.Context, key string) (*Response, error) {
get := &getAction{
Prefix: k.prefix,
Key: key,
Recursive: false,
}
httpresp, body, err := k.client.doWithTimeout(get)
resp, body, err := k.client.Do(ctx, get)
if err != nil {
return nil, err
}
return unmarshalHTTPResponse(httpresp.StatusCode, body)
return unmarshalHTTPResponse(resp.StatusCode, body)
}
func (k *HTTPKeysAPI) Watch(key string, idx uint64) Watcher {
func (k *httpKeysAPI) Watch(key string, idx uint64) Watcher {
return &httpWatcher{
client: k.client,
nextWait: waitAction{
Prefix: k.prefix,
Key: key,
WaitIndex: idx,
Recursive: false,
@ -139,10 +137,11 @@ func (k *HTTPKeysAPI) Watch(key string, idx uint64) Watcher {
}
}
func (k *HTTPKeysAPI) RecursiveWatch(key string, idx uint64) Watcher {
func (k *httpKeysAPI) RecursiveWatch(key string, idx uint64) Watcher {
return &httpWatcher{
client: k.client,
nextWait: waitAction{
Prefix: k.prefix,
Key: key,
WaitIndex: idx,
Recursive: true,
@ -151,13 +150,12 @@ func (k *HTTPKeysAPI) RecursiveWatch(key string, idx uint64) Watcher {
}
type httpWatcher struct {
client *httpClient
client HTTPClient
nextWait waitAction
}
func (hw *httpWatcher) Next() (*Response, error) {
//TODO(bcwaldon): This needs to be cancellable by the calling user
httpresp, body, err := hw.client.do(context.Background(), &hw.nextWait)
func (hw *httpWatcher) Next(ctx context.Context) (*Response, error) {
httpresp, body, err := hw.client.Do(ctx, &hw.nextWait)
if err != nil {
return nil, err
}
@ -171,21 +169,24 @@ func (hw *httpWatcher) Next() (*Response, error) {
return resp, nil
}
// v2KeysURL forms a URL representing the location of a key. The provided
// endpoint must be the root of the etcd keys API. For example, a valid
// endpoint probably has the path "/v2/keys".
func v2KeysURL(ep url.URL, key string) *url.URL {
ep.Path = path.Join(ep.Path, key)
// v2KeysURL forms a URL representing the location of a key.
// The endpoint argument represents the base URL of an etcd
// server. The prefix is the path needed to route from the
// provided endpoint's path to the root of the keys API
// (typically "/v2/keys").
func v2KeysURL(ep url.URL, prefix, key string) *url.URL {
ep.Path = path.Join(ep.Path, prefix, key)
return &ep
}
type getAction struct {
Prefix string
Key string
Recursive bool
}
func (g *getAction) httpRequest(ep url.URL) *http.Request {
u := v2KeysURL(ep, g.Key)
func (g *getAction) HTTPRequest(ep url.URL) *http.Request {
u := v2KeysURL(ep, g.Prefix, g.Key)
params := u.Query()
params.Set("recursive", strconv.FormatBool(g.Recursive))
@ -196,13 +197,14 @@ func (g *getAction) httpRequest(ep url.URL) *http.Request {
}
type waitAction struct {
Prefix string
Key string
WaitIndex uint64
Recursive bool
}
func (w *waitAction) httpRequest(ep url.URL) *http.Request {
u := v2KeysURL(ep, w.Key)
func (w *waitAction) HTTPRequest(ep url.URL) *http.Request {
u := v2KeysURL(ep, w.Prefix, w.Key)
params := u.Query()
params.Set("wait", "true")
@ -215,13 +217,14 @@ func (w *waitAction) httpRequest(ep url.URL) *http.Request {
}
type createAction struct {
Key string
Value string
TTL *uint64
Prefix string
Key string
Value string
TTL *uint64
}
func (c *createAction) httpRequest(ep url.URL) *http.Request {
u := v2KeysURL(ep, c.Key)
func (c *createAction) HTTPRequest(ep url.URL) *http.Request {
u := v2KeysURL(ep, c.Prefix, c.Key)
params := u.Query()
params.Set("prevExist", "false")

View File

@ -29,12 +29,14 @@ import (
func TestV2KeysURLHelper(t *testing.T) {
tests := []struct {
endpoint url.URL
prefix string
key string
want url.URL
}{
// key is empty, no problem
{
endpoint: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"},
prefix: "",
key: "",
want: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"},
},
@ -42,6 +44,7 @@ func TestV2KeysURLHelper(t *testing.T) {
// key is joined to path
{
endpoint: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"},
prefix: "",
key: "/foo/bar",
want: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys/foo/bar"},
},
@ -49,6 +52,7 @@ func TestV2KeysURLHelper(t *testing.T) {
// key is joined to path when path is empty
{
endpoint: url.URL{Scheme: "http", Host: "example.com", Path: ""},
prefix: "",
key: "/foo/bar",
want: url.URL{Scheme: "http", Host: "example.com", Path: "/foo/bar"},
},
@ -56,6 +60,7 @@ func TestV2KeysURLHelper(t *testing.T) {
// Host field carries through with port
{
endpoint: url.URL{Scheme: "http", Host: "example.com:8080", Path: "/v2/keys"},
prefix: "",
key: "",
want: url.URL{Scheme: "http", Host: "example.com:8080", Path: "/v2/keys"},
},
@ -63,13 +68,21 @@ func TestV2KeysURLHelper(t *testing.T) {
// Scheme carries through
{
endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/v2/keys"},
prefix: "",
key: "",
want: url.URL{Scheme: "https", Host: "example.com", Path: "/v2/keys"},
},
// Prefix is applied
{
endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/foo"},
prefix: "/bar",
key: "/baz",
want: url.URL{Scheme: "https", Host: "example.com", Path: "/foo/bar/baz"},
},
}
for i, tt := range tests {
got := v2KeysURL(tt.endpoint, tt.key)
got := v2KeysURL(tt.endpoint, tt.prefix, tt.key)
if tt.want != *got {
t.Errorf("#%d: want=%#v, got=%#v", i, tt.want, *got)
}
@ -104,7 +117,7 @@ func TestGetAction(t *testing.T) {
Key: "/foo/bar",
Recursive: tt.recursive,
}
got := *f.httpRequest(ep)
got := *f.HTTPRequest(ep)
wantURL := wantURL
wantURL.RawQuery = tt.wantQuery
@ -153,7 +166,7 @@ func TestWaitAction(t *testing.T) {
WaitIndex: tt.waitIndex,
Recursive: tt.recursive,
}
got := *f.httpRequest(ep)
got := *f.HTTPRequest(ep)
wantURL := wantURL
wantURL.RawQuery = tt.wantQuery
@ -200,7 +213,7 @@ func TestCreateAction(t *testing.T) {
Value: tt.value,
TTL: tt.ttl,
}
got := *f.httpRequest(ep)
got := *f.HTTPRequest(ep)
err := assertResponse(got, wantURL, wantHeader, []byte(tt.wantBody))
if err != nil {
@ -224,7 +237,7 @@ func assertResponse(got http.Request, wantURL *url.URL, wantHeader http.Header,
}
} else {
if wantBody == nil {
return fmt.Errorf("want.Body=%v got.Body=%v", wantBody, got.Body)
return fmt.Errorf("want.Body=%v got.Body=%s", wantBody, got.Body)
} else {
gotBytes, err := ioutil.ReadAll(got.Body)
if err != nil {
@ -232,7 +245,7 @@ func assertResponse(got http.Request, wantURL *url.URL, wantHeader http.Header,
}
if !reflect.DeepEqual(wantBody, gotBytes) {
return fmt.Errorf("want.Body=%v got.Body=%v", wantBody, gotBytes)
return fmt.Errorf("want.Body=%s got.Body=%s", wantBody, gotBytes)
}
}
}

159
client/members.go Normal file
View File

@ -0,0 +1,159 @@
/*
Copyright 2014 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 client
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
"github.com/coreos/etcd/etcdserver/etcdhttp/httptypes"
"github.com/coreos/etcd/pkg/types"
)
var (
DefaultV2MembersPrefix = "/v2/members"
)
func NewMembersAPI(c HTTPClient) MembersAPI {
return &httpMembersAPI{
client: c,
}
}
type MembersAPI interface {
List(ctx context.Context) ([]httptypes.Member, error)
Add(ctx context.Context, peerURL string) (*httptypes.Member, error)
Remove(ctx context.Context, mID string) error
}
type httpMembersAPI struct {
client HTTPClient
}
func (m *httpMembersAPI) List(ctx context.Context) ([]httptypes.Member, error) {
req := &membersAPIActionList{}
resp, body, err := m.client.Do(ctx, req)
if err != nil {
return nil, err
}
if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
return nil, err
}
var mCollection httptypes.MemberCollection
if err := json.Unmarshal(body, &mCollection); err != nil {
return nil, err
}
return []httptypes.Member(mCollection), nil
}
func (m *httpMembersAPI) Add(ctx context.Context, peerURL string) (*httptypes.Member, error) {
urls, err := types.NewURLs([]string{peerURL})
if err != nil {
return nil, err
}
req := &membersAPIActionAdd{peerURLs: urls}
resp, body, err := m.client.Do(ctx, req)
if err != nil {
return nil, err
}
if err := assertStatusCode(resp.StatusCode, http.StatusCreated, http.StatusConflict); err != nil {
return nil, err
}
if resp.StatusCode != http.StatusCreated {
var httperr httptypes.HTTPError
if err := json.Unmarshal(body, &httperr); err != nil {
return nil, err
}
return nil, httperr
}
var memb httptypes.Member
if err := json.Unmarshal(body, &memb); err != nil {
return nil, err
}
return &memb, nil
}
func (m *httpMembersAPI) Remove(ctx context.Context, memberID string) error {
req := &membersAPIActionRemove{memberID: memberID}
resp, _, err := m.client.Do(ctx, req)
if err != nil {
return err
}
return assertStatusCode(resp.StatusCode, http.StatusNoContent)
}
type membersAPIActionList struct{}
func (l *membersAPIActionList) HTTPRequest(ep url.URL) *http.Request {
u := v2MembersURL(ep)
req, _ := http.NewRequest("GET", u.String(), nil)
return req
}
type membersAPIActionRemove struct {
memberID string
}
func (d *membersAPIActionRemove) HTTPRequest(ep url.URL) *http.Request {
u := v2MembersURL(ep)
u.Path = path.Join(u.Path, d.memberID)
req, _ := http.NewRequest("DELETE", u.String(), nil)
return req
}
type membersAPIActionAdd struct {
peerURLs types.URLs
}
func (a *membersAPIActionAdd) HTTPRequest(ep url.URL) *http.Request {
u := v2MembersURL(ep)
m := httptypes.MemberCreateRequest{PeerURLs: a.peerURLs}
b, _ := json.Marshal(&m)
req, _ := http.NewRequest("POST", u.String(), bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json")
return req
}
func assertStatusCode(got int, want ...int) (err error) {
for _, w := range want {
if w == got {
return nil
}
}
return fmt.Errorf("unexpected status code %d", got)
}
// v2MembersURL add the necessary path to the provided endpoint
// to route requests to the default v2 members API.
func v2MembersURL(ep url.URL) *url.URL {
ep.Path = path.Join(ep.Path, DefaultV2MembersPrefix)
return &ep
}

113
client/members_test.go Normal file
View File

@ -0,0 +1,113 @@
/*
Copyright 2014 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 client
import (
"net/http"
"net/url"
"reflect"
"testing"
"github.com/coreos/etcd/pkg/types"
)
func TestMembersAPIActionList(t *testing.T) {
ep := url.URL{Scheme: "http", Host: "example.com"}
act := &membersAPIActionList{}
wantURL := &url.URL{
Scheme: "http",
Host: "example.com",
Path: "/v2/members",
}
got := *act.HTTPRequest(ep)
err := assertResponse(got, wantURL, http.Header{}, nil)
if err != nil {
t.Error(err.Error())
}
}
func TestMembersAPIActionAdd(t *testing.T) {
ep := url.URL{Scheme: "http", Host: "example.com"}
act := &membersAPIActionAdd{
peerURLs: types.URLs([]url.URL{
url.URL{Scheme: "https", Host: "127.0.0.1:8081"},
url.URL{Scheme: "http", Host: "127.0.0.1:8080"},
}),
}
wantURL := &url.URL{
Scheme: "http",
Host: "example.com",
Path: "/v2/members",
}
wantHeader := http.Header{
"Content-Type": []string{"application/json"},
}
wantBody := []byte(`{"peerURLs":["https://127.0.0.1:8081","http://127.0.0.1:8080"]}`)
got := *act.HTTPRequest(ep)
err := assertResponse(got, wantURL, wantHeader, wantBody)
if err != nil {
t.Error(err.Error())
}
}
func TestMembersAPIActionRemove(t *testing.T) {
ep := url.URL{Scheme: "http", Host: "example.com"}
act := &membersAPIActionRemove{memberID: "XXX"}
wantURL := &url.URL{
Scheme: "http",
Host: "example.com",
Path: "/v2/members/XXX",
}
got := *act.HTTPRequest(ep)
err := assertResponse(got, wantURL, http.Header{}, nil)
if err != nil {
t.Error(err.Error())
}
}
func TestAssertStatusCode(t *testing.T) {
if err := assertStatusCode(404, 400); err == nil {
t.Errorf("assertStatusCode failed to detect conflict in 400 vs 404")
}
if err := assertStatusCode(404, 400, 404); err != nil {
t.Errorf("assertStatusCode found conflict in (404,400) vs 400: %v", err)
}
}
func TestV2MembersURL(t *testing.T) {
got := v2MembersURL(url.URL{
Scheme: "http",
Host: "foo.example.com:4002",
Path: "/pants",
})
want := &url.URL{
Scheme: "http",
Host: "foo.example.com:4002",
Path: "/pants/v2/members",
}
if !reflect.DeepEqual(want, got) {
t.Fatalf("v2MembersURL got %#v, want %#v", got, want)
}
}

View File

@ -22,15 +22,16 @@ import (
"log"
"net/http"
"net/url"
"os"
"path"
"sort"
"strconv"
"strings"
"time"
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/jonboulle/clockwork"
"github.com/coreos/etcd/client"
"github.com/coreos/etcd/pkg/types"
)
var (
@ -44,20 +45,33 @@ var (
)
const (
// Environment variable used to configure an HTTP proxy for discovery
DiscoveryProxyEnv = "ETCD_DISCOVERY_PROXY"
// Number of retries discovery will attempt before giving up and erroring out.
nRetries = uint(3)
)
type Discoverer interface {
Discover() (string, error)
// JoinCluster will connect to the discovery service at the given url, and
// register the server represented by the given id and config to the cluster
func JoinCluster(durl, dproxyurl string, id types.ID, config string) (string, error) {
d, err := newDiscovery(durl, dproxyurl, id)
if err != nil {
return "", err
}
return d.joinCluster(config)
}
// GetCluster will connect to the discovery service at the given url and
// retrieve a string describing the cluster
func GetCluster(durl, dproxyurl string) (string, error) {
d, err := newDiscovery(durl, dproxyurl, 0)
if err != nil {
return "", err
}
return d.getCluster()
}
type discovery struct {
cluster string
id uint64
config string
id types.ID
c client.KeysAPI
retries uint
url *url.URL
@ -65,11 +79,10 @@ type discovery struct {
clock clockwork.Clock
}
// proxyFuncFromEnv builds a proxy function if the appropriate environment
// variable is set. It performs basic sanitization of the environment variable
// and returns any error encountered.
func proxyFuncFromEnv() (func(*http.Request) (*url.URL, error), error) {
proxy := os.Getenv(DiscoveryProxyEnv)
// newProxyFunc builds a proxy function from the given string, which should
// represent a URL that can be used as a proxy. It performs basic
// sanitization of the URL and returns any error encountered.
func newProxyFunc(proxy string) (func(*http.Request) (*url.URL, error), error) {
if proxy == "" {
return nil, nil
}
@ -94,40 +107,39 @@ func proxyFuncFromEnv() (func(*http.Request) (*url.URL, error), error) {
return http.ProxyURL(proxyURL), nil
}
func New(durl string, id uint64, config string) (Discoverer, error) {
func newDiscovery(durl, dproxyurl string, id types.ID) (*discovery, error) {
u, err := url.Parse(durl)
if err != nil {
return nil, err
}
token := u.Path
u.Path = ""
pf, err := proxyFuncFromEnv()
pf, err := newProxyFunc(dproxyurl)
if err != nil {
return nil, err
}
c, err := client.NewDiscoveryKeysAPI(&http.Transport{Proxy: pf}, u.String(), client.DefaultRequestTimeout)
c, err := client.NewHTTPClient(&http.Transport{Proxy: pf}, []string{u.String()})
if err != nil {
return nil, err
}
dc := client.NewDiscoveryKeysAPI(c)
return &discovery{
cluster: token,
c: dc,
id: id,
config: config,
c: c,
url: u,
clock: clockwork.NewRealClock(),
}, nil
}
func (d *discovery) Discover() (string, error) {
// fast path: if the cluster is full, returns the error
// do not need to register itself to the cluster in this
// case.
func (d *discovery) joinCluster(config string) (string, error) {
// fast path: if the cluster is full, return the error
// do not need to register to the cluster in this case.
if _, _, err := d.checkCluster(); err != nil {
return "", err
}
if err := d.createSelf(); err != nil {
if err := d.createSelf(config); err != nil {
// Fails, even on a timeout, if createSelf times out.
// TODO(barakmich): Retrying the same node might want to succeed here
// (ie, createSelf should be idempotent for discovery).
@ -147,22 +159,42 @@ func (d *discovery) Discover() (string, error) {
return nodesToCluster(all), nil
}
func (d *discovery) createSelf() error {
resp, err := d.c.Create(d.selfKey(), d.config, -1)
func (d *discovery) getCluster() (string, error) {
nodes, size, err := d.checkCluster()
if err != nil {
if err == ErrFullCluster {
return nodesToCluster(nodes), nil
}
return "", err
}
all, err := d.waitNodes(nodes, size)
if err != nil {
return "", err
}
return nodesToCluster(all), nil
}
func (d *discovery) createSelf(contents string) error {
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
resp, err := d.c.Create(ctx, d.selfKey(), contents, -1)
cancel()
if err != nil {
return err
}
// ensure self appears on the server we connected to
w := d.c.Watch(d.selfKey(), resp.Node.CreatedIndex)
_, err = w.Next()
_, err = w.Next(context.Background())
return err
}
func (d *discovery) checkCluster() (client.Nodes, int, error) {
configKey := path.Join("/", d.cluster, "_config")
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
// find cluster size
resp, err := d.c.Get(path.Join(configKey, "size"))
resp, err := d.c.Get(ctx, path.Join(configKey, "size"))
cancel()
if err != nil {
if err == client.ErrKeyNoExist {
return nil, 0, ErrSizeNotFound
@ -177,7 +209,9 @@ func (d *discovery) checkCluster() (client.Nodes, int, error) {
return nil, 0, ErrBadSizeKey
}
resp, err = d.c.Get(d.cluster)
ctx, cancel = context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
resp, err = d.c.Get(ctx, d.cluster)
cancel()
if err != nil {
if err == client.ErrTimeout {
return d.checkClusterRetry()
@ -187,7 +221,7 @@ func (d *discovery) checkCluster() (client.Nodes, int, error) {
nodes := make(client.Nodes, 0)
// append non-config keys to nodes
for _, n := range resp.Node.Nodes {
if !strings.Contains(n.Key, configKey) {
if !(path.Base(n.Key) == path.Base(configKey)) {
nodes = append(nodes, n)
}
}
@ -197,11 +231,11 @@ func (d *discovery) checkCluster() (client.Nodes, int, error) {
// find self position
for i := range nodes {
if strings.Contains(nodes[i].Key, d.selfKey()) {
if path.Base(nodes[i].Key) == path.Base(d.selfKey()) {
break
}
if i >= size-1 {
return nil, size, ErrFullCluster
return nodes[:size], size, ErrFullCluster
}
}
return nodes, size, nil
@ -241,22 +275,33 @@ func (d *discovery) waitNodes(nodes client.Nodes, size int) (client.Nodes, error
w := d.c.RecursiveWatch(d.cluster, nodes[len(nodes)-1].ModifiedIndex+1)
all := make(client.Nodes, len(nodes))
copy(all, nodes)
for _, n := range all {
if path.Base(n.Key) == path.Base(d.selfKey()) {
log.Printf("discovery: found self %s in the cluster", path.Base(d.selfKey()))
} else {
log.Printf("discovery: found peer %s in the cluster", path.Base(n.Key))
}
}
// wait for others
for len(all) < size {
resp, err := w.Next()
log.Printf("discovery: found %d peer(s), waiting for %d more", len(all), size-len(all))
resp, err := w.Next(context.Background())
if err != nil {
if err == client.ErrTimeout {
return d.waitNodesRetry()
}
return nil, err
}
log.Printf("discovery: found peer %s in the cluster", path.Base(resp.Node.Key))
all = append(all, resp.Node)
}
log.Printf("discovery: found %d needed peer(s)", len(all))
return all, nil
}
func (d *discovery) selfKey() string {
return path.Join("/", d.cluster, fmt.Sprintf("%d", d.id))
return path.Join("/", d.cluster, d.id.String())
}
func nodesToCluster(ns client.Nodes) string {

View File

@ -20,21 +20,19 @@ import (
"errors"
"math/rand"
"net/http"
"os"
"reflect"
"sort"
"strconv"
"reflect"
"testing"
"time"
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/jonboulle/clockwork"
"github.com/coreos/etcd/client"
)
func TestProxyFuncFromEnvUnset(t *testing.T) {
os.Setenv(DiscoveryProxyEnv, "")
pf, err := proxyFuncFromEnv()
func TestNewProxyFuncUnset(t *testing.T) {
pf, err := newProxyFunc("")
if pf != nil {
t.Fatal("unexpected non-nil proxyFunc")
}
@ -43,14 +41,13 @@ func TestProxyFuncFromEnvUnset(t *testing.T) {
}
}
func TestProxyFuncFromEnvBad(t *testing.T) {
func TestNewProxyFuncBad(t *testing.T) {
tests := []string{
"%%",
"http://foo.com/%1",
}
for i, in := range tests {
os.Setenv(DiscoveryProxyEnv, in)
pf, err := proxyFuncFromEnv()
pf, err := newProxyFunc(in)
if pf != nil {
t.Errorf("#%d: unexpected non-nil proxyFunc", i)
}
@ -60,14 +57,13 @@ func TestProxyFuncFromEnvBad(t *testing.T) {
}
}
func TestProxyFuncFromEnv(t *testing.T) {
func TestNewProxyFunc(t *testing.T) {
tests := map[string]string{
"bar.com": "http://bar.com",
"http://disco.foo.bar": "http://disco.foo.bar",
}
for in, w := range tests {
os.Setenv(DiscoveryProxyEnv, in)
pf, err := proxyFuncFromEnv()
pf, err := newProxyFunc(in)
if pf == nil {
t.Errorf("%s: unexpected nil proxyFunc", in)
continue
@ -88,7 +84,7 @@ func TestProxyFuncFromEnv(t *testing.T) {
}
func TestCheckCluster(t *testing.T) {
cluster := "1000"
cluster := "/prefix/1000"
self := "/1000/1"
tests := []struct {
@ -100,6 +96,7 @@ func TestCheckCluster(t *testing.T) {
// self is in the size range
[]*client.Node{
{Key: "/1000/_config/size", Value: "3", CreatedIndex: 1},
{Key: "/1000/_config/"},
{Key: self, CreatedIndex: 2},
{Key: "/1000/2", CreatedIndex: 3},
{Key: "/1000/3", CreatedIndex: 4},
@ -112,6 +109,7 @@ func TestCheckCluster(t *testing.T) {
// self is in the size range
[]*client.Node{
{Key: "/1000/_config/size", Value: "3", CreatedIndex: 1},
{Key: "/1000/_config/"},
{Key: "/1000/2", CreatedIndex: 2},
{Key: "/1000/3", CreatedIndex: 3},
{Key: self, CreatedIndex: 4},
@ -124,6 +122,7 @@ func TestCheckCluster(t *testing.T) {
// self is out of the size range
[]*client.Node{
{Key: "/1000/_config/size", Value: "3", CreatedIndex: 1},
{Key: "/1000/_config/"},
{Key: "/1000/2", CreatedIndex: 2},
{Key: "/1000/3", CreatedIndex: 3},
{Key: "/1000/4", CreatedIndex: 4},
@ -136,6 +135,7 @@ func TestCheckCluster(t *testing.T) {
// self is not in the cluster
[]*client.Node{
{Key: "/1000/_config/size", Value: "3", CreatedIndex: 1},
{Key: "/1000/_config/"},
{Key: "/1000/2", CreatedIndex: 2},
{Key: "/1000/3", CreatedIndex: 3},
},
@ -145,6 +145,7 @@ func TestCheckCluster(t *testing.T) {
{
[]*client.Node{
{Key: "/1000/_config/size", Value: "3", CreatedIndex: 1},
{Key: "/1000/_config/"},
{Key: "/1000/2", CreatedIndex: 2},
{Key: "/1000/3", CreatedIndex: 3},
{Key: "/1000/4", CreatedIndex: 4},
@ -175,7 +176,7 @@ func TestCheckCluster(t *testing.T) {
rs = append(rs, &client.Response{
Node: &client.Node{
Key: cluster,
Nodes: tt.nodes,
Nodes: tt.nodes[1:],
},
})
}
@ -317,7 +318,7 @@ func TestCreateSelf(t *testing.T) {
for i, tt := range tests {
d := discovery{cluster: "1000", c: tt.c}
if err := d.createSelf(); err != tt.werr {
if err := d.createSelf(""); err != tt.werr {
t.Errorf("#%d: err = %v, want %v", i, err, nil)
}
}
@ -392,7 +393,7 @@ type clientWithResp struct {
w client.Watcher
}
func (c *clientWithResp) Create(key string, value string, ttl time.Duration) (*client.Response, error) {
func (c *clientWithResp) Create(ctx context.Context, key string, value string, ttl time.Duration) (*client.Response, error) {
if len(c.rs) == 0 {
return &client.Response{}, nil
}
@ -401,7 +402,7 @@ func (c *clientWithResp) Create(key string, value string, ttl time.Duration) (*c
return r, nil
}
func (c *clientWithResp) Get(key string) (*client.Response, error) {
func (c *clientWithResp) Get(ctx context.Context, key string) (*client.Response, error) {
if len(c.rs) == 0 {
return &client.Response{}, client.ErrKeyNoExist
}
@ -423,11 +424,11 @@ type clientWithErr struct {
w client.Watcher
}
func (c *clientWithErr) Create(key string, value string, ttl time.Duration) (*client.Response, error) {
func (c *clientWithErr) Create(ctx context.Context, key string, value string, ttl time.Duration) (*client.Response, error) {
return &client.Response{}, c.err
}
func (c *clientWithErr) Get(key string) (*client.Response, error) {
func (c *clientWithErr) Get(ctx context.Context, key string) (*client.Response, error) {
return &client.Response{}, c.err
}
@ -443,7 +444,7 @@ type watcherWithResp struct {
rs []*client.Response
}
func (w *watcherWithResp) Next() (*client.Response, error) {
func (w *watcherWithResp) Next(context.Context) (*client.Response, error) {
if len(w.rs) == 0 {
return &client.Response{}, nil
}
@ -456,7 +457,7 @@ type watcherWithErr struct {
err error
}
func (w *watcherWithErr) Next() (*client.Response, error) {
func (w *watcherWithErr) Next(context.Context) (*client.Response, error) {
return &client.Response{}, w.err
}
@ -467,20 +468,20 @@ type clientWithRetry struct {
failTimes int
}
func (c *clientWithRetry) Create(key string, value string, ttl time.Duration) (*client.Response, error) {
func (c *clientWithRetry) Create(ctx context.Context, key string, value string, ttl time.Duration) (*client.Response, error) {
if c.failCount < c.failTimes {
c.failCount++
return nil, client.ErrTimeout
}
return c.clientWithResp.Create(key, value, ttl)
return c.clientWithResp.Create(ctx, key, value, ttl)
}
func (c *clientWithRetry) Get(key string) (*client.Response, error) {
func (c *clientWithRetry) Get(ctx context.Context, key string) (*client.Response, error) {
if c.failCount < c.failTimes {
c.failCount++
return nil, client.ErrTimeout
}
return c.clientWithResp.Get(key)
return c.clientWithResp.Get(ctx, key)
}
// watcherWithRetry will timeout all requests up to failTimes
@ -490,7 +491,7 @@ type watcherWithRetry struct {
failTimes int
}
func (w *watcherWithRetry) Next() (*client.Response, error) {
func (w *watcherWithRetry) Next(context.Context) (*client.Response, error) {
if w.failCount < w.failTimes {
w.failCount++
return nil, client.ErrTimeout

View File

@ -63,6 +63,16 @@ var errors = map[int]string{
EcodeClientInternal: "Client Internal Error",
}
var errorStatus = map[int]int{
EcodeKeyNotFound: http.StatusNotFound,
EcodeNotFile: http.StatusForbidden,
EcodeDirNotEmpty: http.StatusForbidden,
EcodeTestFailed: http.StatusPreconditionFailed,
EcodeNodeExist: http.StatusPreconditionFailed,
EcodeRaftInternal: http.StatusInternalServerError,
EcodeLeaderElect: http.StatusInternalServerError,
}
const (
EcodeKeyNotFound = 100
EcodeTestFailed = 101
@ -133,22 +143,17 @@ func (e Error) toJsonString() string {
return string(b)
}
func (e Error) Write(w http.ResponseWriter) {
w.Header().Add("X-Etcd-Index", fmt.Sprint(e.Index))
// 3xx is raft internal error
status := http.StatusBadRequest
switch e.ErrorCode {
case EcodeKeyNotFound:
status = http.StatusNotFound
case EcodeNotFile, EcodeDirNotEmpty:
status = http.StatusForbidden
case EcodeTestFailed, EcodeNodeExist:
status = http.StatusPreconditionFailed
default:
if e.ErrorCode/100 == 3 {
status = http.StatusInternalServerError
}
func (e Error) statusCode() int {
status, ok := errorStatus[e.ErrorCode]
if !ok {
status = http.StatusBadRequest
}
w.WriteHeader(status)
return status
}
func (e Error) WriteTo(w http.ResponseWriter) {
w.Header().Add("X-Etcd-Index", fmt.Sprint(e.Index))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(e.statusCode())
fmt.Fprintln(w, e.toJsonString())
}

52
error/error_test.go Normal file
View File

@ -0,0 +1,52 @@
/*
Copyright 2014 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 error
import (
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
)
func TestErrorWriteTo(t *testing.T) {
for k, _ := range errors {
err := NewError(k, "", 1)
rr := httptest.NewRecorder()
err.WriteTo(rr)
if err.statusCode() != rr.Code {
t.Errorf("HTTP status code %d, want %d", rr.Code, err.statusCode())
}
gbody := strings.TrimSuffix(rr.Body.String(), "\n")
if err.toJsonString() != gbody {
t.Errorf("HTTP body %q, want %q", gbody, err.toJsonString())
}
wheader := http.Header(map[string][]string{
"Content-Type": []string{"application/json"},
"X-Etcd-Index": []string{"1"},
})
if !reflect.DeepEqual(wheader, rr.HeaderMap) {
t.Errorf("HTTP headers %v, want %v", rr.HeaderMap, wheader)
}
}
}

View File

@ -127,6 +127,13 @@ $ etcdctl ls --recursive
/adir/key2
```
Directories can also have a trailing `/` added to output using `-p`.
```
$ etcdctl ls -p
/akey
/adir/
```
### Deleting a key

View File

@ -0,0 +1,87 @@
/*
Copyright 2014 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 (
"log"
"math/rand"
"path"
"time"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
"github.com/coreos/etcd/etcdserver"
"github.com/coreos/etcd/etcdserver/etcdserverpb"
"github.com/coreos/etcd/pkg/pbutil"
"github.com/coreos/etcd/snap"
"github.com/coreos/etcd/wal"
)
func NewBackupCommand() cli.Command {
return cli.Command{
Name: "backup",
Usage: "backup an etcd directory",
Flags: []cli.Flag{
cli.StringFlag{Name: "data-dir", Value: "", Usage: "Path to the etcd data dir"},
cli.StringFlag{Name: "backup-dir", Value: "", Usage: "Path to the backup dir"},
},
Action: handleBackup,
}
}
// handleBackup handles a request that intends to do a backup.
func handleBackup(c *cli.Context) {
srcSnap := path.Join(c.String("data-dir"), "snap")
destSnap := path.Join(c.String("backup-dir"), "snap")
srcWAL := path.Join(c.String("data-dir"), "wal")
destWAL := path.Join(c.String("backup-dir"), "wal")
ss := snap.New(srcSnap)
snapshot, err := ss.Load()
if err != nil && err != snap.ErrNoSnapshot {
log.Fatal(err)
}
var index uint64
if snapshot != nil {
index = snapshot.Index
newss := snap.New(destSnap)
newss.SaveSnap(*snapshot)
}
w, err := wal.OpenAtIndex(srcWAL, index)
if err != nil {
log.Fatal(err)
}
defer w.Close()
wmetadata, state, ents, err := w.ReadAll()
if err != nil {
log.Fatal(err)
}
var metadata etcdserverpb.Metadata
pbutil.MustUnmarshal(&metadata, wmetadata)
rand.Seed(time.Now().UnixNano())
metadata.NodeID = etcdserver.GenID()
metadata.ClusterID = etcdserver.GenID()
neww, err := wal.Create(destWAL, pbutil.MustMarshal(&metadata))
if err != nil {
log.Fatal(err)
}
defer neww.Close()
if err := neww.Save(state, ents); err != nil {
log.Fatal(err)
}
}

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 (

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 (
@ -65,7 +81,6 @@ func execWatchCommandFunc(c *cli.Context, client *etcd.Client) (*etcd.Response,
}()
receiver := make(chan *etcd.Response)
client.SetConsistency(etcd.WEAK_CONSISTENCY)
go client.Watch(key, uint64(index), recursive, receiver, stop)
for {

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 (

View File

@ -1,10 +1,25 @@
/*
Copyright 2014 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"
"net/url"
"os"
"strings"
@ -14,6 +29,7 @@ import (
type handlerFunc func(*cli.Context, *etcd.Client) (*etcd.Response, error)
type printFunc func(*etcd.Response, string)
type contextualPrintFunc func(*cli.Context, *etcd.Response, string)
// dumpCURL blindly dumps all curl output to os.Stderr
func dumpCURL(client *etcd.Client) {
@ -23,68 +39,35 @@ func dumpCURL(client *etcd.Client) {
}
}
// createHttpPath attaches http scheme to the given address if needed
func createHttpPath(addr string) (string, error) {
u, err := url.Parse(addr)
if err != nil {
return "", err
}
if u.Scheme == "" {
u.Scheme = "http"
}
return u.String(), nil
}
// rawhandle wraps the command function handlers and sets up the
// environment but performs no output formatting.
func rawhandle(c *cli.Context, fn handlerFunc) (*etcd.Response, error) {
sync := !c.GlobalBool("no-sync")
peerstr := c.GlobalString("peers")
// Use an environment variable if nothing was supplied on the
// command line
if peerstr == "" {
peerstr = os.Getenv("ETCDCTL_PEERS")
endpoints, err := getEndpoints(c)
if err != nil {
return nil, err
}
// If we still don't have peers, use a default
if peerstr == "" {
peerstr = "127.0.0.1:4001"
tr, err := getTransport(c)
if err != nil {
return nil, err
}
peers := strings.Split(peerstr, ",")
// If no sync, create http path for each peer address
if !sync {
revisedPeers := make([]string, 0)
for _, peer := range peers {
if revisedPeer, err := createHttpPath(peer); err != nil {
fmt.Fprintf(os.Stderr, "Unsupported url %v: %v\n", peer, err)
} else {
revisedPeers = append(revisedPeers, revisedPeer)
}
}
peers = revisedPeers
}
client := etcd.NewClient(peers)
client := etcd.NewClient(endpoints)
client.SetTransport(tr)
if c.GlobalBool("debug") {
go dumpCURL(client)
}
// Sync cluster.
if sync {
if !c.GlobalBool("no-sync") {
if ok := client.SyncCluster(); !ok {
handleError(FailedToConnectToHost, errors.New("Cannot sync with the cluster using peers "+strings.Join(peers, ", ")))
handleError(FailedToConnectToHost, errors.New("cannot sync with the cluster using endpoints "+strings.Join(endpoints, ", ")))
}
}
if c.GlobalBool("debug") {
fmt.Fprintf(os.Stderr, "Cluster-Peers: %s\n",
strings.Join(client.GetCluster(), " "))
fmt.Fprintf(os.Stderr, "Cluster-Endpoints: %s\n", strings.Join(client.GetCluster(), ", "))
}
// Execute handler function.
@ -106,6 +89,19 @@ func handlePrint(c *cli.Context, fn handlerFunc, pFn printFunc) {
}
}
// Just like handlePrint but also passed the context of the command
func handleContextualPrint(c *cli.Context, fn handlerFunc, pFn contextualPrintFunc) {
resp, err := rawhandle(c, fn)
if err != nil {
handleError(ErrorFromEtcd, err)
}
if resp != nil && pFn != nil {
pFn(c, resp, c.GlobalString("output"))
}
}
// handleDir handles a request that wants to do operations on a single dir.
// Dir cannot be printed out, so we set NIL print function here.
func handleDir(c *cli.Context, fn handlerFunc) {

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 (
@ -12,7 +28,9 @@ func NewLsCommand() cli.Command {
Name: "ls",
Usage: "retrieve a directory",
Flags: []cli.Flag{
cli.BoolFlag{Name: "sort", Usage: "returns result in sorted order"},
cli.BoolFlag{Name: "recursive", Usage: "returns all values for key and child keys"},
cli.BoolFlag{Name: "p", Usage: "append slash (/) to directories"},
},
Action: func(c *cli.Context) {
handleLs(c, lsCommandFunc)
@ -22,17 +40,17 @@ func NewLsCommand() cli.Command {
// handleLs handles a request that intends to do ls-like operations.
func handleLs(c *cli.Context, fn handlerFunc) {
handlePrint(c, fn, printLs)
handleContextualPrint(c, fn, printLs)
}
// printLs writes a response out in a manner similar to the `ls` command in unix.
// Non-empty directories list their contents and files list their name.
func printLs(resp *etcd.Response, format string) {
func printLs(c *cli.Context, resp *etcd.Response, format string) {
if !resp.Node.Dir {
fmt.Println(resp.Node.Key)
}
for _, node := range resp.Node.Nodes {
rPrint(node)
rPrint(c, node)
}
}
@ -43,15 +61,22 @@ func lsCommandFunc(c *cli.Context, client *etcd.Client) (*etcd.Response, error)
key = c.Args()[0]
}
recursive := c.Bool("recursive")
sort := c.Bool("sort")
// Retrieve the value from the server.
return client.Get(key, false, recursive)
return client.Get(key, sort, recursive)
}
// rPrint recursively prints out the nodes in the node structure.
func rPrint(n *etcd.Node) {
fmt.Println(n.Key)
func rPrint(c *cli.Context, n *etcd.Node) {
if n.Dir && c.Bool("p") {
fmt.Println(fmt.Sprintf("%v/", n.Key))
} else {
fmt.Println(n.Key)
}
for _, node := range n.Nodes {
rPrint(node)
rPrint(c, node)
}
}

View File

@ -0,0 +1,173 @@
/*
Copyright 2014 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"
"strings"
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
"github.com/coreos/etcd/client"
)
func NewMemberCommand() cli.Command {
return cli.Command{
Name: "member",
Usage: "member add, remove and list subcommands",
Subcommands: []cli.Command{
cli.Command{
Name: "list",
Usage: "enumerate existing cluster members",
Action: actionMemberList,
},
cli.Command{
Name: "add",
Usage: "add a new member to the etcd cluster",
Action: actionMemberAdd,
},
cli.Command{
Name: "remove",
Usage: "remove an existing member from the etcd cluster",
Action: actionMemberRemove,
},
},
}
}
func mustNewMembersAPI(c *cli.Context) client.MembersAPI {
eps, err := getEndpoints(c)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
tr, err := getTransport(c)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
hc, err := client.NewHTTPClient(tr, eps)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
if !c.GlobalBool("no-sync") {
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
err := hc.Sync(ctx)
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
}
if c.GlobalBool("debug") {
fmt.Fprintf(os.Stderr, "Cluster-Endpoints: %s\n", strings.Join(hc.Endpoints(), ", "))
}
return client.NewMembersAPI(hc)
}
func actionMemberList(c *cli.Context) {
if len(c.Args()) != 0 {
fmt.Fprintln(os.Stderr, "No arguments accepted")
os.Exit(1)
}
mAPI := mustNewMembersAPI(c)
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
members, err := mAPI.List(ctx)
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
for _, m := range members {
fmt.Printf("%s: name=%s peerURLs=%s clientURLs=%s\n", m.ID, m.Name, strings.Join(m.PeerURLs, ","), strings.Join(m.ClientURLs, ","))
}
}
func actionMemberAdd(c *cli.Context) {
args := c.Args()
if len(args) != 2 {
fmt.Fprintln(os.Stderr, "Provide a name and a single member peerURL")
os.Exit(1)
}
mAPI := mustNewMembersAPI(c)
url := args[1]
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
m, err := mAPI.Add(ctx, url)
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
newID := m.ID
newName := args[0]
fmt.Printf("Added member named %s with ID %s to cluster\n", newName, newID)
ctx, cancel = context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
members, err := mAPI.List(ctx)
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
conf := []string{}
for _, m := range members {
for _, u := range m.PeerURLs {
n := m.Name
if m.ID == newID {
n = newName
}
conf = append(conf, fmt.Sprintf("%s=%s", n, u))
}
}
fmt.Print("\n")
fmt.Printf("ETCD_NAME=%q\n", newName)
fmt.Printf("ETCD_INITIAL_CLUSTER=%q\n", strings.Join(conf, ","))
fmt.Printf("ETCD_INITIAL_CLUSTER_STATE=\"existing\"\n")
}
func actionMemberRemove(c *cli.Context) {
args := c.Args()
if len(args) != 1 {
fmt.Fprintln(os.Stderr, "Provide a single member ID")
os.Exit(1)
}
mAPI := mustNewMembersAPI(c)
mID := args[0]
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
err := mAPI.Remove(ctx, mID)
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
fmt.Printf("Removed member %s from cluster\n", mID)
}

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 (

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 (

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 (

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 (

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 (

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 (

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 (

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 (

View File

@ -1,10 +1,32 @@
/*
Copyright 2014 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"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
"github.com/coreos/etcd/pkg/transport"
)
var (
@ -33,3 +55,47 @@ func argOrStdin(args []string, stdin io.Reader, i int) (string, error) {
}
return string(bytes), nil
}
func getPeersFlagValue(c *cli.Context) []string {
peerstr := c.GlobalString("peers")
// Use an environment variable if nothing was supplied on the
// command line
if peerstr == "" {
peerstr = os.Getenv("ETCDCTL_PEERS")
}
// If we still don't have peers, use a default
if peerstr == "" {
peerstr = "127.0.0.1:4001"
}
return strings.Split(peerstr, ",")
}
func getEndpoints(c *cli.Context) ([]string, error) {
eps := getPeersFlagValue(c)
for i, ep := range eps {
u, err := url.Parse(ep)
if err != nil {
return nil, err
}
if u.Scheme == "" {
u.Scheme = "http"
}
eps[i] = u.String()
}
return eps, nil
}
func getTransport(c *cli.Context) (*http.Transport, error) {
tls := transport.TLSInfo{
CAFile: c.GlobalString("ca-file"),
CertFile: c.GlobalString("cert-file"),
KeyFile: c.GlobalString("key-file"),
}
return transport.NewTransport(tls)
}

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 (

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 (

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 main
import (
@ -19,8 +35,12 @@ func main() {
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` or `json`)"},
cli.StringFlag{Name: "peers, C", Value: "", Usage: "a comma-delimited list of machine addresses in the cluster (default: \"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"},
}
app.Commands = []cli.Command{
command.NewBackupCommand(),
command.NewMakeCommand(),
command.NewMakeDirCommand(),
command.NewRemoveCommand(),
@ -33,6 +53,8 @@ func main() {
command.NewUpdateDirCommand(),
command.NewWatchCommand(),
command.NewExecWatchCommand(),
command.NewMemberCommand(),
}
app.Run(os.Args)
}

19
etcdmain/doc.go Normal file
View File

@ -0,0 +1,19 @@
/*
Copyright 2014 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 etcd contains the main entry point for the etcd binary. */
package etcdmain

417
etcdmain/etcd.go Normal file
View File

@ -0,0 +1,417 @@
/*
Copyright 2014 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 etcdmain
import (
"flag"
"fmt"
"log"
"net"
"net/http"
"net/url"
"os"
"strings"
"github.com/coreos/etcd/discovery"
"github.com/coreos/etcd/etcdserver"
"github.com/coreos/etcd/etcdserver/etcdhttp"
"github.com/coreos/etcd/pkg/cors"
"github.com/coreos/etcd/pkg/fileutil"
"github.com/coreos/etcd/pkg/flags"
"github.com/coreos/etcd/pkg/transport"
"github.com/coreos/etcd/pkg/types"
"github.com/coreos/etcd/proxy"
"github.com/coreos/etcd/version"
)
const (
// the owner can make/remove files inside the directory
privateDirMode = 0700
proxyFlagOff = "off"
proxyFlagReadonly = "readonly"
proxyFlagOn = "on"
fallbackFlagExit = "exit"
fallbackFlagProxy = "proxy"
clusterStateFlagNew = "new"
clusterStateFlagExisting = "existing"
)
var (
fs = flag.NewFlagSet("etcd", flag.ContinueOnError)
name = fs.String("name", "default", "Unique human-readable name for this node")
dir = fs.String("data-dir", "", "Path to the data directory")
durl = fs.String("discovery", "", "Discovery service used to bootstrap the cluster")
dproxy = fs.String("discovery-proxy", "", "HTTP proxy to use for traffic to discovery service")
snapCount = fs.Uint64("snapshot-count", etcdserver.DefaultSnapCount, "Number of committed transactions to trigger a snapshot")
printVersion = fs.Bool("version", false, "Print the version and exit")
forceNewCluster = fs.Bool("force-new-cluster", false, "Force to create a new one member cluster")
initialCluster = fs.String("initial-cluster", "default=http://localhost:2380,default=http://localhost:7001", "Initial cluster configuration for bootstrapping")
initialClusterToken = fs.String("initial-cluster-token", "etcd-cluster", "Initial cluster token for the etcd cluster during bootstrap")
corsInfo = &cors.CORSInfo{}
clientTLSInfo = transport.TLSInfo{}
peerTLSInfo = transport.TLSInfo{}
proxyFlag = flags.NewStringsFlag(
proxyFlagOff,
proxyFlagReadonly,
proxyFlagOn,
)
fallbackFlag = flags.NewStringsFlag(
fallbackFlagExit,
fallbackFlagProxy,
)
clusterStateFlag = flags.NewStringsFlag(
clusterStateFlagNew,
clusterStateFlagExisting,
)
ignored = []string{
"cluster-active-size",
"cluster-remove-delay",
"cluster-sync-interval",
"config",
"force",
"max-result-buffer",
"max-retry-attempts",
"peer-heartbeat-interval",
"peer-election-timeout",
"retry-interval",
"snapshot",
"v",
"vv",
}
)
func init() {
fs.Var(clusterStateFlag, "initial-cluster-state", "Initial cluster configuration for bootstrapping")
if err := clusterStateFlag.Set(clusterStateFlagNew); err != nil {
// Should never happen.
log.Panicf("unexpected error setting up clusterStateFlag: %v", err)
}
fs.Var(flags.NewURLsValue("http://localhost:2380,http://localhost:7001"), "initial-advertise-peer-urls", "List of this member's peer URLs to advertise to the rest of the cluster")
fs.Var(flags.NewURLsValue("http://localhost:2379,http://localhost:4001"), "advertise-client-urls", "List of this member's client URLs to advertise to the rest of the cluster")
fs.Var(flags.NewURLsValue("http://localhost:2380,http://localhost:7001"), "listen-peer-urls", "List of URLs to listen on for peer traffic")
fs.Var(flags.NewURLsValue("http://localhost:2379,http://localhost:4001"), "listen-client-urls", "List of URLs to listen on for client traffic")
fs.Var(corsInfo, "cors", "Comma-separated white list of origins for CORS (cross-origin resource sharing).")
fs.Var(proxyFlag, "proxy", fmt.Sprintf("Valid values include %s", strings.Join(proxyFlag.Values, ", ")))
if err := proxyFlag.Set(proxyFlagOff); err != nil {
// Should never happen.
log.Panicf("unexpected error setting up proxyFlag: %v", err)
}
fs.Var(fallbackFlag, "discovery-fallback", fmt.Sprintf("Valid values include %s", strings.Join(fallbackFlag.Values, ", ")))
if err := fallbackFlag.Set(fallbackFlagProxy); err != nil {
// Should never happen.
log.Panicf("unexpected error setting up discovery-fallback flag: %v", err)
}
fs.StringVar(&clientTLSInfo.CAFile, "ca-file", "", "Path to the client server TLS CA file.")
fs.StringVar(&clientTLSInfo.CertFile, "cert-file", "", "Path to the client server TLS cert file.")
fs.StringVar(&clientTLSInfo.KeyFile, "key-file", "", "Path to the client server TLS key file.")
fs.StringVar(&peerTLSInfo.CAFile, "peer-ca-file", "", "Path to the peer server TLS CA file.")
fs.StringVar(&peerTLSInfo.CertFile, "peer-cert-file", "", "Path to the peer server TLS cert file.")
fs.StringVar(&peerTLSInfo.KeyFile, "peer-key-file", "", "Path to the peer server TLS key file.")
// backwards-compatibility with v0.4.6
fs.Var(&flags.IPAddressPort{}, "addr", "DEPRECATED: Use -advertise-client-urls instead.")
fs.Var(&flags.IPAddressPort{}, "bind-addr", "DEPRECATED: Use -listen-client-urls instead.")
fs.Var(&flags.IPAddressPort{}, "peer-addr", "DEPRECATED: Use -initial-advertise-peer-urls instead.")
fs.Var(&flags.IPAddressPort{}, "peer-bind-addr", "DEPRECATED: Use -listen-peer-urls instead.")
for _, f := range ignored {
fs.Var(&flags.IgnoredFlag{Name: f}, f, "")
}
fs.Var(&flags.DeprecatedFlag{Name: "peers"}, "peers", "DEPRECATED: Use -initial-cluster instead")
fs.Var(&flags.DeprecatedFlag{Name: "peers-file"}, "peers-file", "DEPRECATED: Use -initial-cluster instead")
}
func Main() {
fs.Usage = flags.UsageWithIgnoredFlagsFunc(fs, ignored)
perr := fs.Parse(os.Args[1:])
switch perr {
case nil:
case flag.ErrHelp:
os.Exit(0)
default:
os.Exit(2)
}
if *printVersion {
fmt.Println("etcd version", version.Version)
os.Exit(0)
}
err := flags.SetFlagsFromEnv(fs)
if err != nil {
log.Fatalf("etcd: %v", err)
}
shouldProxy := proxyFlag.String() != proxyFlagOff
var stopped <-chan struct{}
if !shouldProxy {
stopped, err = startEtcd()
if err == discovery.ErrFullCluster && fallbackFlag.String() == fallbackFlagProxy {
log.Printf("etcd: discovery cluster full, falling back to %s", fallbackFlagProxy)
shouldProxy = true
}
}
if shouldProxy {
err = startProxy()
}
if err != nil {
log.Fatalf("etcd: %v", err)
}
<-stopped
}
// startEtcd launches the etcd server and HTTP handlers for client/server communication.
func startEtcd() (<-chan struct{}, error) {
apurls, err := flags.URLsFromFlags(fs, "initial-advertise-peer-urls", "addr", peerTLSInfo)
if err != nil {
return nil, err
}
cls, err := setupCluster(apurls)
if err != nil {
return nil, fmt.Errorf("error setting up initial cluster: %v", err)
}
if *dir == "" {
*dir = fmt.Sprintf("%v.etcd", *name)
log.Printf("no data-dir provided, using default data-dir ./%s", *dir)
}
if err := os.MkdirAll(*dir, privateDirMode); err != nil {
return nil, fmt.Errorf("cannot create data directory: %v", err)
}
if err := fileutil.IsDirWriteable(*dir); err != nil {
return nil, fmt.Errorf("cannot write to data directory: %v", err)
}
pt, err := transport.NewTransport(peerTLSInfo)
if err != nil {
return nil, err
}
acurls, err := flags.URLsFromFlags(fs, "advertise-client-urls", "addr", clientTLSInfo)
if err != nil {
return nil, err
}
lpurls, err := flags.URLsFromFlags(fs, "listen-peer-urls", "peer-bind-addr", peerTLSInfo)
if err != nil {
return nil, err
}
if !peerTLSInfo.Empty() {
log.Printf("etcd: peerTLS: %s", peerTLSInfo)
}
plns := make([]net.Listener, 0)
for _, u := range lpurls {
var l net.Listener
l, err = transport.NewListener(u.Host, u.Scheme, peerTLSInfo)
if err != nil {
return nil, err
}
urlStr := u.String()
log.Print("etcd: listening for peers on ", urlStr)
defer func() {
if err != nil {
l.Close()
log.Print("etcd: stopping listening for peers on ", urlStr)
}
}()
plns = append(plns, l)
}
lcurls, err := flags.URLsFromFlags(fs, "listen-client-urls", "bind-addr", clientTLSInfo)
if err != nil {
return nil, err
}
if !clientTLSInfo.Empty() {
log.Printf("etcd: clientTLS: %s", clientTLSInfo)
}
clns := make([]net.Listener, 0)
for _, u := range lcurls {
var l net.Listener
l, err = transport.NewListener(u.Host, u.Scheme, clientTLSInfo)
if err != nil {
return nil, err
}
urlStr := u.String()
log.Print("etcd: listening for client requests on ", urlStr)
defer func() {
if err != nil {
l.Close()
log.Print("etcd: stopping listening for client requests on ", urlStr)
}
}()
clns = append(clns, l)
}
cfg := &etcdserver.ServerConfig{
Name: *name,
ClientURLs: acurls,
PeerURLs: apurls,
DataDir: *dir,
SnapCount: *snapCount,
Cluster: cls,
DiscoveryURL: *durl,
DiscoveryProxy: *dproxy,
NewCluster: clusterStateFlag.String() == clusterStateFlagNew,
ForceNewCluster: *forceNewCluster,
Transport: pt,
}
var s *etcdserver.EtcdServer
s, err = etcdserver.NewServer(cfg)
if err != nil {
return nil, err
}
s.Start()
if corsInfo.String() != "" {
log.Printf("etcd: cors = %s", corsInfo)
}
ch := &cors.CORSHandler{
Handler: etcdhttp.NewClientHandler(s),
Info: corsInfo,
}
ph := etcdhttp.NewPeerHandler(s)
// Start the peer server in a goroutine
for _, l := range plns {
go func(l net.Listener) {
log.Fatal(http.Serve(l, ph))
}(l)
}
// Start a client server goroutine for each listen address
for _, l := range clns {
go func(l net.Listener) {
log.Fatal(http.Serve(l, ch))
}(l)
}
return s.StopNotify(), nil
}
// startProxy launches an HTTP proxy for client communication which proxies to other etcd nodes.
func startProxy() error {
apurls, err := flags.URLsFromFlags(fs, "initial-advertise-peer-urls", "addr", peerTLSInfo)
if err != nil {
return err
}
cls, err := setupCluster(apurls)
if err != nil {
return fmt.Errorf("error setting up initial cluster: %v", err)
}
if *durl != "" {
s, err := discovery.GetCluster(*durl, *dproxy)
if err != nil {
return err
}
if cls, err = etcdserver.NewClusterFromString(*durl, s); err != nil {
return err
}
}
pt, err := transport.NewTransport(clientTLSInfo)
if err != nil {
return err
}
// TODO(jonboulle): update peerURLs dynamically (i.e. when updating
// clientURLs) instead of just using the initial fixed list here
peerURLs := cls.PeerURLs()
uf := func() []string {
cls, err := etcdserver.GetClusterFromPeers(peerURLs)
if err != nil {
log.Printf("proxy: %v", err)
return []string{}
}
return cls.ClientURLs()
}
ph := proxy.NewHandler(pt, uf)
ph = &cors.CORSHandler{
Handler: ph,
Info: corsInfo,
}
if proxyFlag.String() == proxyFlagReadonly {
ph = proxy.NewReadonlyHandler(ph)
}
lcurls, err := flags.URLsFromFlags(fs, "listen-client-urls", "bind-addr", clientTLSInfo)
if err != nil {
return err
}
// Start a proxy server goroutine for each listen address
for _, u := range lcurls {
l, err := transport.NewListener(u.Host, u.Scheme, clientTLSInfo)
if err != nil {
return err
}
host := u.Host
go func() {
log.Print("proxy: listening for client requests on ", host)
log.Fatal(http.Serve(l, ph))
}()
}
return nil
}
// setupCluster sets up an initial cluster definition for bootstrap or discovery.
func setupCluster(apurls []url.URL) (*etcdserver.Cluster, error) {
set := make(map[string]bool)
fs.Visit(func(f *flag.Flag) {
set[f.Name] = true
})
if set["discovery"] && set["initial-cluster"] {
return nil, fmt.Errorf("both discovery and bootstrap-config are set")
}
var cls *etcdserver.Cluster
var err error
switch {
case set["discovery"]:
// If using discovery, generate a temporary cluster based on
// self's advertised peer URLs
clusterStr := genClusterString(*name, apurls)
cls, err = etcdserver.NewClusterFromString(*durl, clusterStr)
case set["initial-cluster"]:
fallthrough
default:
// We're statically configured, and cluster has appropriately been set.
cls, err = etcdserver.NewClusterFromString(*initialClusterToken, *initialCluster)
}
return cls, err
}
func genClusterString(name string, urls types.URLs) string {
addrs := make([]string, 0)
for _, u := range urls {
addrs = append(addrs, fmt.Sprintf("%v=%v", name, u.String()))
}
return strings.Join(addrs, ",")
}

View File

@ -14,19 +14,28 @@
limitations under the License.
*/
package main
package etcdmain
import (
"net/url"
"testing"
"github.com/coreos/etcd/pkg/types"
)
func mustNewURLs(t *testing.T, urls []string) []url.URL {
u, err := types.NewURLs(urls)
if err != nil {
t.Fatalf("unexpected new urls error: %v", err)
}
return u
}
func TestGenClusterString(t *testing.T) {
tests := []struct {
name string
urls []string
wstr string
token string
urls []string
wstr string
}{
{
"default", []string{"http://127.0.0.1:4001"},
@ -38,11 +47,8 @@ func TestGenClusterString(t *testing.T) {
},
}
for i, tt := range tests {
urls, err := types.NewURLs(tt.urls)
if err != nil {
t.Fatalf("unexpected new urls error: %v", err)
}
str := genClusterString(tt.name, urls)
urls := mustNewURLs(t, tt.urls)
str := genClusterString(tt.token, urls)
if str != tt.wstr {
t.Errorf("#%d: cluster = %s, want %s", i, str, tt.wstr)
}

View File

@ -27,10 +27,12 @@ import (
"reflect"
"sort"
"strings"
"sync"
etcdErr "github.com/coreos/etcd/error"
"github.com/coreos/etcd/pkg/flags"
"github.com/coreos/etcd/pkg/types"
"github.com/coreos/etcd/raft/raftpb"
"github.com/coreos/etcd/store"
)
@ -40,29 +42,39 @@ const (
)
type ClusterInfo interface {
ID() uint64
// ID returns the cluster ID
ID() types.ID
// ClientURLs returns an aggregate set of all URLs on which this
// cluster is listening for client requests
ClientURLs() []string
// Members returns a slice of members sorted by their ID
Members() []*Member
Member(id uint64) *Member
// Member retrieves a particular member based on ID, or nil if the
// member does not exist in the cluster
Member(id types.ID) *Member
// IsIDRemoved checks whether the given ID has been removed from this
// cluster at some point in the past
IsIDRemoved(id types.ID) bool
}
// Cluster is a list of Members that belong to the same raft cluster
type Cluster struct {
id uint64
name string
members map[uint64]*Member
id types.ID
token string
members map[types.ID]*Member
// removed contains the ids of removed members in the cluster.
// removed id cannot be reused.
removed map[uint64]bool
removed map[types.ID]bool
store store.Store
sync.Mutex
}
// NewClusterFromString returns Cluster through given clusterName and parsing
// members from a sets of names to IPs discovery formatted like:
// NewClusterFromString returns a Cluster instantiated from the given cluster token
// and cluster string, by parsing members from a set of discovery-formatted
// names-to-IPs, like:
// mach0=http://1.1.1.1,mach0=http://2.2.2.2,mach1=http://3.3.3.3,mach2=http://4.4.4.4
func NewClusterFromString(name string, cluster string) (*Cluster, error) {
c := newCluster(name)
func NewClusterFromString(token string, cluster string) (*Cluster, error) {
c := newCluster(token)
v, err := url.ParseQuery(strings.Replace(cluster, ",", "&", -1))
if err != nil {
@ -76,7 +88,7 @@ func NewClusterFromString(name string, cluster string) (*Cluster, error) {
if err := purls.Set(strings.Join(urls, ",")); err != nil {
return nil, err
}
m := NewMember(name, types.URLs(*purls), c.name, nil)
m := NewMember(name, types.URLs(*purls), c.token, nil)
if _, ok := c.members[m.ID]; ok {
return nil, fmt.Errorf("Member exists with identical ID %v", m)
}
@ -86,41 +98,15 @@ func NewClusterFromString(name string, cluster string) (*Cluster, error) {
return c, nil
}
func NewClusterFromStore(name string, st store.Store) *Cluster {
c := newCluster(name)
func NewClusterFromStore(token string, st store.Store) *Cluster {
c := newCluster(token)
c.store = st
e, err := c.store.Get(storeMembersPrefix, true, true)
if err != nil {
if isKeyNotFound(err) {
return c
}
log.Panicf("get storeMembers should never fail: %v", err)
}
for _, n := range e.Node.Nodes {
m, err := nodeToMember(n)
if err != nil {
log.Panicf("nodeToMember should never fail: %v", err)
}
c.members[m.ID] = m
}
e, err = c.store.Get(storeRemovedMembersPrefix, true, true)
if err != nil {
if isKeyNotFound(err) {
return c
}
log.Panicf("get storeRemovedMembers should never fail: %v", err)
}
for _, n := range e.Node.Nodes {
c.removed[parseMemberID(n.Key)] = true
}
c.members, c.removed = membersFromStore(c.store)
return c
}
func NewClusterFromMembers(name string, id uint64, membs []*Member) *Cluster {
c := newCluster(name)
func NewClusterFromMembers(token string, id types.ID, membs []*Member) *Cluster {
c := newCluster(token)
c.id = id
for _, m := range membs {
c.members[m.ID] = m
@ -128,20 +114,22 @@ func NewClusterFromMembers(name string, id uint64, membs []*Member) *Cluster {
return c
}
func newCluster(name string) *Cluster {
func newCluster(token string) *Cluster {
return &Cluster{
name: name,
members: make(map[uint64]*Member),
removed: make(map[uint64]bool),
token: token,
members: make(map[types.ID]*Member),
removed: make(map[types.ID]bool),
}
}
func (c Cluster) ID() uint64 { return c.id }
func (c *Cluster) ID() types.ID { return c.id }
func (c Cluster) Members() []*Member {
func (c *Cluster) Members() []*Member {
c.Lock()
defer c.Unlock()
var sms SortableMemberSlice
for _, m := range c.members {
sms = append(sms, m)
sms = append(sms, m.Clone())
}
sort.Sort(sms)
return []*Member(sms)
@ -153,42 +141,52 @@ func (s SortableMemberSlice) Len() int { return len(s) }
func (s SortableMemberSlice) Less(i, j int) bool { return s[i].ID < s[j].ID }
func (s SortableMemberSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (c *Cluster) Member(id uint64) *Member {
return c.members[id]
func (c *Cluster) Member(id types.ID) *Member {
c.Lock()
defer c.Unlock()
return c.members[id].Clone()
}
// MemberByName returns a Member with the given name if exists.
// If more than one member has the given name, it will panic.
func (c *Cluster) MemberByName(name string) *Member {
c.Lock()
defer c.Unlock()
var memb *Member
for _, m := range c.members {
if m.Name == name {
if memb != nil {
panic("two members with the given name exist in the cluster")
log.Panicf("two members with the given name %q exist", name)
}
memb = m
}
}
return memb
return memb.Clone()
}
func (c Cluster) MemberIDs() []uint64 {
var ids []uint64
func (c *Cluster) MemberIDs() []types.ID {
c.Lock()
defer c.Unlock()
var ids []types.ID
for _, m := range c.members {
ids = append(ids, m.ID)
}
sort.Sort(types.Uint64Slice(ids))
sort.Sort(types.IDSlice(ids))
return ids
}
func (c *Cluster) IsIDRemoved(id uint64) bool {
func (c *Cluster) IsIDRemoved(id types.ID) bool {
c.Lock()
defer c.Unlock()
return c.removed[id]
}
// PeerURLs returns a list of all peer addresses. Each address is prefixed
// with the scheme (currently "http://"). The returned list is sorted in
// ascending lexicographical order.
func (c Cluster) PeerURLs() []string {
func (c *Cluster) PeerURLs() []string {
c.Lock()
defer c.Unlock()
endpoints := make([]string, 0)
for _, p := range c.members {
for _, addr := range p.PeerURLs {
@ -202,7 +200,9 @@ func (c Cluster) PeerURLs() []string {
// ClientURLs returns a list of all client addresses. Each address is prefixed
// with the scheme (currently "http://"). The returned list is sorted in
// ascending lexicographical order.
func (c Cluster) ClientURLs() []string {
func (c *Cluster) ClientURLs() []string {
c.Lock()
defer c.Unlock()
urls := make([]string, 0)
for _, p := range c.members {
for _, url := range p.ClientURLs {
@ -213,7 +213,9 @@ func (c Cluster) ClientURLs() []string {
return urls
}
func (c Cluster) String() string {
func (c *Cluster) String() string {
c.Lock()
defer c.Unlock()
sl := []string{}
for _, m := range c.members {
for _, u := range m.PeerURLs {
@ -230,7 +232,7 @@ func (c Cluster) String() string {
// cluster. If the validation fails, an error will be returned.
func (c *Cluster) ValidateAndAssignIDs(membs []*Member) error {
if len(c.members) != len(membs) {
return fmt.Errorf("cannot update %v from %v because the member count is unequal", c.members, membs)
return fmt.Errorf("member count is unequal")
}
omembs := make([]*Member, 0)
for _, m := range c.members {
@ -244,7 +246,7 @@ func (c *Cluster) ValidateAndAssignIDs(membs []*Member) error {
}
omembs[i].ID = membs[i].ID
}
c.members = make(map[uint64]*Member)
c.members = make(map[types.ID]*Member)
for _, m := range omembs {
c.members[m.ID] = m
}
@ -255,73 +257,191 @@ func (c *Cluster) genID() {
mIDs := c.MemberIDs()
b := make([]byte, 8*len(mIDs))
for i, id := range mIDs {
binary.BigEndian.PutUint64(b[8*i:], id)
binary.BigEndian.PutUint64(b[8*i:], uint64(id))
}
hash := sha1.Sum(b)
c.id = binary.BigEndian.Uint64(hash[:8])
c.id = types.ID(binary.BigEndian.Uint64(hash[:8]))
}
func (c *Cluster) SetID(id uint64) { c.id = id }
func (c *Cluster) SetID(id types.ID) { c.id = id }
func (c *Cluster) SetStore(st store.Store) { c.store = st }
// AddMember puts a new Member into the store.
func (c *Cluster) Recover() {
c.members, c.removed = membersFromStore(c.store)
}
// ValidateConfigurationChange takes a proposed ConfChange and
// ensures that it is still valid.
func (c *Cluster) ValidateConfigurationChange(cc raftpb.ConfChange) error {
members, removed := membersFromStore(c.store)
id := types.ID(cc.NodeID)
if removed[id] {
return ErrIDRemoved
}
switch cc.Type {
case raftpb.ConfChangeAddNode:
if members[id] != nil {
return ErrIDExists
}
urls := make(map[string]bool)
for _, m := range members {
for _, u := range m.PeerURLs {
urls[u] = true
}
}
m := new(Member)
if err := json.Unmarshal(cc.Context, m); err != nil {
log.Panicf("unmarshal member should never fail: %v", err)
}
for _, u := range m.PeerURLs {
if urls[u] {
return ErrPeerURLexists
}
}
case raftpb.ConfChangeRemoveNode:
if members[id] == nil {
return ErrIDNotFound
}
case raftpb.ConfChangeUpdateNode:
if members[id] == nil {
return ErrIDNotFound
}
urls := make(map[string]bool)
for _, m := range members {
if m.ID == id {
continue
}
for _, u := range m.PeerURLs {
urls[u] = true
}
}
m := new(Member)
if err := json.Unmarshal(cc.Context, m); err != nil {
log.Panicf("unmarshal member should never fail: %v", err)
}
for _, u := range m.PeerURLs {
if urls[u] {
return ErrPeerURLexists
}
}
default:
log.Panicf("ConfChange type should be either AddNode, RemoveNode or UpdateNode")
}
return nil
}
// AddMember adds a new Member into the cluster, and saves the given member's
// raftAttributes into the store. The given member should have empty attributes.
// A Member with a matching id must not exist.
func (c *Cluster) AddMember(m *Member) {
c.Lock()
defer c.Unlock()
b, err := json.Marshal(m.RaftAttributes)
if err != nil {
log.Panicf("marshal error: %v", err)
log.Panicf("marshal raftAttributes should never fail: %v", err)
}
p := path.Join(memberStoreKey(m.ID), raftAttributesSuffix)
if _, err := c.store.Create(p, false, string(b), false, store.Permanent); err != nil {
log.Panicf("add raftAttributes should never fail: %v", err)
}
b, err = json.Marshal(m.Attributes)
if err != nil {
log.Panicf("marshal error: %v", err)
}
p = path.Join(memberStoreKey(m.ID), attributesSuffix)
if _, err := c.store.Create(p, false, string(b), false, store.Permanent); err != nil {
log.Panicf("add attributes should never fail: %v", err)
log.Panicf("create raftAttributes should never fail: %v", err)
}
c.members[m.ID] = m
}
// RemoveMember removes a member from the store.
// The given id MUST exist, or the function panics.
func (c *Cluster) RemoveMember(id uint64) {
func (c *Cluster) RemoveMember(id types.ID) {
c.Lock()
defer c.Unlock()
if _, err := c.store.Delete(memberStoreKey(id), true, true); err != nil {
log.Panicf("delete peer should never fail: %v", err)
log.Panicf("delete member should never fail: %v", err)
}
delete(c.members, id)
if _, err := c.store.Create(removedMemberStoreKey(id), false, "", false, store.Permanent); err != nil {
log.Panicf("creating RemovedMember should never fail: %v", err)
log.Panicf("create removedMember should never fail: %v", err)
}
c.removed[id] = true
}
func (c *Cluster) UpdateMemberAttributes(id types.ID, attr Attributes) {
c.Lock()
defer c.Unlock()
c.members[id].Attributes = attr
}
func (c *Cluster) UpdateMember(nm *Member) {
c.Lock()
defer c.Unlock()
b, err := json.Marshal(nm.RaftAttributes)
if err != nil {
log.Panicf("marshal raftAttributes should never fail: %v", err)
}
p := path.Join(memberStoreKey(nm.ID), raftAttributesSuffix)
if _, err := c.store.Update(p, string(b), store.Permanent); err != nil {
log.Panicf("update raftAttributes should never fail: %v", err)
}
c.members[nm.ID].RaftAttributes = nm.RaftAttributes
}
// nodeToMember builds member through a store node.
// the child nodes of the given node should be sorted by key.
func nodeToMember(n *store.NodeExtern) (*Member, error) {
m := &Member{ID: parseMemberID(n.Key)}
if len(n.Nodes) != 2 {
return m, fmt.Errorf("len(nodes) = %d, want 2", len(n.Nodes))
m := &Member{ID: mustParseMemberIDFromKey(n.Key)}
attrs := make(map[string][]byte)
raftAttrKey := path.Join(n.Key, raftAttributesSuffix)
attrKey := path.Join(n.Key, attributesSuffix)
for _, nn := range n.Nodes {
if nn.Key != raftAttrKey && nn.Key != attrKey {
return nil, fmt.Errorf("unknown key %q", nn.Key)
}
attrs[nn.Key] = []byte(*nn.Value)
}
if w := path.Join(n.Key, attributesSuffix); n.Nodes[0].Key != w {
return m, fmt.Errorf("key = %v, want %v", n.Nodes[0].Key, w)
if data := attrs[raftAttrKey]; data != nil {
if err := json.Unmarshal(data, &m.RaftAttributes); err != nil {
return nil, fmt.Errorf("unmarshal raftAttributes error: %v", err)
}
} else {
return nil, fmt.Errorf("raftAttributes key doesn't exist")
}
if err := json.Unmarshal([]byte(*n.Nodes[0].Value), &m.Attributes); err != nil {
return m, fmt.Errorf("unmarshal attributes error: %v", err)
}
if w := path.Join(n.Key, raftAttributesSuffix); n.Nodes[1].Key != w {
return m, fmt.Errorf("key = %v, want %v", n.Nodes[1].Key, w)
}
if err := json.Unmarshal([]byte(*n.Nodes[1].Value), &m.RaftAttributes); err != nil {
return m, fmt.Errorf("unmarshal raftAttributes error: %v", err)
if data := attrs[attrKey]; data != nil {
if err := json.Unmarshal(data, &m.Attributes); err != nil {
return m, fmt.Errorf("unmarshal attributes error: %v", err)
}
}
return m, nil
}
func membersFromStore(st store.Store) (map[types.ID]*Member, map[types.ID]bool) {
members := make(map[types.ID]*Member)
removed := make(map[types.ID]bool)
e, err := st.Get(storeMembersPrefix, true, true)
if err != nil {
if isKeyNotFound(err) {
return members, removed
}
log.Panicf("get storeMembers should never fail: %v", err)
}
for _, n := range e.Node.Nodes {
m, err := nodeToMember(n)
if err != nil {
log.Panicf("nodeToMember should never fail: %v", err)
}
members[m.ID] = m
}
e, err = st.Get(storeRemovedMembersPrefix, true, true)
if err != nil {
if isKeyNotFound(err) {
return members, removed
}
log.Panicf("get storeRemovedMembers should never fail: %v", err)
}
for _, n := range e.Node.Nodes {
removed[mustParseMemberIDFromKey(n.Key)] = true
}
return members, removed
}
func isKeyNotFound(err error) bool {
e, ok := err.(*etcdErr.Error)
return ok && e.ErrorCode == etcdErr.EcodeKeyNotFound

View File

@ -1,53 +0,0 @@
/*
Copyright 2014 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 etcdserver
import (
"errors"
)
const (
ClusterStateValueNew = "new"
ClusterStateValueExisting = "existing"
)
var (
ClusterStateValues = []string{
ClusterStateValueNew,
ClusterStateValueExisting,
}
)
// ClusterState implements the flag.Value interface.
type ClusterState string
// Set verifies the argument to be a valid member of ClusterStateFlagValues
// before setting the underlying flag value.
func (cs *ClusterState) Set(s string) error {
for _, v := range ClusterStateValues {
if s == v {
*cs = ClusterState(s)
return nil
}
}
return errors.New("invalid value")
}
func (cs *ClusterState) String() string {
return string(*cs)
}

View File

@ -17,23 +17,27 @@
package etcdserver
import (
"encoding/json"
"fmt"
"path"
"reflect"
"testing"
"github.com/coreos/etcd/pkg/types"
"github.com/coreos/etcd/raft/raftpb"
"github.com/coreos/etcd/store"
)
func TestClusterFromString(t *testing.T) {
tests := []struct {
f string
mems []Member
mems []*Member
}{
{
"mem1=http://10.0.0.1:2379,mem1=http://128.193.4.20:2379,mem2=http://10.0.0.2:2379,default=http://127.0.0.1:2379",
[]Member{
newTestMember(4322322643958477905, []string{"http://10.0.0.1:2379", "http://128.193.4.20:2379"}, "mem1", nil),
[]*Member{
newTestMember(3141198903430435750, []string{"http://10.0.0.2:2379"}, "mem2", nil),
newTestMember(4322322643958477905, []string{"http://10.0.0.1:2379", "http://128.193.4.20:2379"}, "mem1", nil),
newTestMember(12762790032478827328, []string{"http://127.0.0.1:2379"}, "default", nil),
},
},
@ -43,12 +47,11 @@ func TestClusterFromString(t *testing.T) {
if err != nil {
t.Fatalf("#%d: unexpected new error: %v", i, err)
}
if c.name != "abc" {
t.Errorf("#%d: name = %v, want abc", i, c.name)
if c.token != "abc" {
t.Errorf("#%d: token = %v, want abc", i, c.token)
}
wc := newTestCluster(tt.mems)
if !reflect.DeepEqual(c.members, wc.members) {
t.Errorf("#%d: members = %+v, want %+v", i, c.members, wc.members)
if !reflect.DeepEqual(c.Members(), tt.mems) {
t.Errorf("#%d: members = %+v, want %+v", i, c.Members(), tt.mems)
}
}
}
@ -76,46 +79,44 @@ func TestClusterFromStringBad(t *testing.T) {
func TestClusterFromStore(t *testing.T) {
tests := []struct {
mems []Member
mems []*Member
}{
{
[]Member{newTestMember(1, nil, "node1", nil)},
[]*Member{newTestMember(1, nil, "", nil)},
},
{
[]Member{},
nil,
},
{
[]Member{
newTestMember(1, nil, "node1", nil),
newTestMember(2, nil, "node2", nil),
[]*Member{
newTestMember(1, nil, "", nil),
newTestMember(2, nil, "", nil),
},
},
}
for i, tt := range tests {
st := store.New()
hc := newTestCluster(nil)
hc.SetStore(st)
hc.SetStore(store.New())
for _, m := range tt.mems {
hc.AddMember(&m)
hc.AddMember(m)
}
c := NewClusterFromStore("abc", st)
if c.name != "abc" {
t.Errorf("#%d: name = %v, want %v", i, c.name, "abc")
c := NewClusterFromStore("abc", hc.store)
if c.token != "abc" {
t.Errorf("#%d: token = %v, want %v", i, c.token, "abc")
}
wc := newTestCluster(tt.mems)
if !reflect.DeepEqual(c.members, wc.members) {
t.Errorf("#%d: members = %v, want %v", i, c.members, wc.members)
if !reflect.DeepEqual(c.Members(), tt.mems) {
t.Errorf("#%d: members = %v, want %v", i, c.Members(), tt.mems)
}
}
}
func TestClusterMember(t *testing.T) {
membs := []Member{
membs := []*Member{
newTestMember(1, nil, "node1", nil),
newTestMember(2, nil, "node2", nil),
}
tests := []struct {
id uint64
id types.ID
match bool
}{
{1, true},
@ -135,7 +136,7 @@ func TestClusterMember(t *testing.T) {
}
func TestClusterMemberByName(t *testing.T) {
membs := []Member{
membs := []*Member{
newTestMember(1, nil, "node1", nil),
newTestMember(2, nil, "node2", nil),
}
@ -160,12 +161,12 @@ func TestClusterMemberByName(t *testing.T) {
}
func TestClusterMemberIDs(t *testing.T) {
c := newTestCluster([]Member{
c := newTestCluster([]*Member{
newTestMember(1, nil, "", nil),
newTestMember(4, nil, "", nil),
newTestMember(100, nil, "", nil),
})
w := []uint64{1, 4, 100}
w := []types.ID{1, 4, 100}
g := c.MemberIDs()
if !reflect.DeepEqual(w, g) {
t.Errorf("IDs = %+v, want %+v", g, w)
@ -174,12 +175,12 @@ func TestClusterMemberIDs(t *testing.T) {
func TestClusterPeerURLs(t *testing.T) {
tests := []struct {
mems []Member
mems []*Member
wurls []string
}{
// single peer with a single address
{
mems: []Member{
mems: []*Member{
newTestMember(1, []string{"http://192.0.2.1"}, "", nil),
},
wurls: []string{"http://192.0.2.1"},
@ -187,7 +188,7 @@ func TestClusterPeerURLs(t *testing.T) {
// single peer with a single address with a port
{
mems: []Member{
mems: []*Member{
newTestMember(1, []string{"http://192.0.2.1:8001"}, "", nil),
},
wurls: []string{"http://192.0.2.1:8001"},
@ -195,7 +196,7 @@ func TestClusterPeerURLs(t *testing.T) {
// several members explicitly unsorted
{
mems: []Member{
mems: []*Member{
newTestMember(2, []string{"http://192.0.2.3", "http://192.0.2.4"}, "", nil),
newTestMember(3, []string{"http://192.0.2.5", "http://192.0.2.6"}, "", nil),
newTestMember(1, []string{"http://192.0.2.1", "http://192.0.2.2"}, "", nil),
@ -205,13 +206,13 @@ func TestClusterPeerURLs(t *testing.T) {
// no members
{
mems: []Member{},
mems: []*Member{},
wurls: []string{},
},
// peer with no peer urls
{
mems: []Member{
mems: []*Member{
newTestMember(3, []string{}, "", nil),
},
wurls: []string{},
@ -229,12 +230,12 @@ func TestClusterPeerURLs(t *testing.T) {
func TestClusterClientURLs(t *testing.T) {
tests := []struct {
mems []Member
mems []*Member
wurls []string
}{
// single peer with a single address
{
mems: []Member{
mems: []*Member{
newTestMember(1, nil, "", []string{"http://192.0.2.1"}),
},
wurls: []string{"http://192.0.2.1"},
@ -242,7 +243,7 @@ func TestClusterClientURLs(t *testing.T) {
// single peer with a single address with a port
{
mems: []Member{
mems: []*Member{
newTestMember(1, nil, "", []string{"http://192.0.2.1:8001"}),
},
wurls: []string{"http://192.0.2.1:8001"},
@ -250,7 +251,7 @@ func TestClusterClientURLs(t *testing.T) {
// several members explicitly unsorted
{
mems: []Member{
mems: []*Member{
newTestMember(2, nil, "", []string{"http://192.0.2.3", "http://192.0.2.4"}),
newTestMember(3, nil, "", []string{"http://192.0.2.5", "http://192.0.2.6"}),
newTestMember(1, nil, "", []string{"http://192.0.2.1", "http://192.0.2.2"}),
@ -260,13 +261,13 @@ func TestClusterClientURLs(t *testing.T) {
// no members
{
mems: []Member{},
mems: []*Member{},
wurls: []string{},
},
// peer with no client urls
{
mems: []Member{
mems: []*Member{
newTestMember(3, nil, "", []string{}),
},
wurls: []string{},
@ -284,34 +285,34 @@ func TestClusterClientURLs(t *testing.T) {
func TestClusterValidateAndAssignIDsBad(t *testing.T) {
tests := []struct {
clmembs []Member
clmembs []*Member
membs []*Member
}{
{
// unmatched length
[]Member{
[]*Member{
newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
},
[]*Member{},
},
{
// unmatched peer urls
[]Member{
[]*Member{
newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
},
[]*Member{
newTestMemberp(1, []string{"http://127.0.0.1:4001"}, "", nil),
newTestMember(1, []string{"http://127.0.0.1:4001"}, "", nil),
},
},
{
// unmatched peer urls
[]Member{
[]*Member{
newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
newTestMember(2, []string{"http://127.0.0.2:2379"}, "", nil),
},
[]*Member{
newTestMemberp(1, []string{"http://127.0.0.1:2379"}, "", nil),
newTestMemberp(2, []string{"http://127.0.0.2:4001"}, "", nil),
newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
newTestMember(2, []string{"http://127.0.0.2:4001"}, "", nil),
},
},
}
@ -325,20 +326,20 @@ func TestClusterValidateAndAssignIDsBad(t *testing.T) {
func TestClusterValidateAndAssignIDs(t *testing.T) {
tests := []struct {
clmembs []Member
clmembs []*Member
membs []*Member
wids []uint64
wids []types.ID
}{
{
[]Member{
[]*Member{
newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
newTestMember(2, []string{"http://127.0.0.2:2379"}, "", nil),
},
[]*Member{
newTestMemberp(3, []string{"http://127.0.0.1:2379"}, "", nil),
newTestMemberp(4, []string{"http://127.0.0.2:2379"}, "", nil),
newTestMember(3, []string{"http://127.0.0.1:2379"}, "", nil),
newTestMember(4, []string{"http://127.0.0.2:2379"}, "", nil),
},
[]uint64{3, 4},
[]types.ID{3, 4},
},
}
for i, tt := range tests {
@ -352,8 +353,130 @@ func TestClusterValidateAndAssignIDs(t *testing.T) {
}
}
func TestClusterValidateConfigurationChange(t *testing.T) {
cl := newCluster("")
cl.SetStore(store.New())
for i := 1; i <= 4; i++ {
attr := RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", i)}}
cl.AddMember(&Member{ID: types.ID(i), RaftAttributes: attr})
}
cl.RemoveMember(4)
attr := RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 1)}}
ctx, err := json.Marshal(&Member{ID: types.ID(5), RaftAttributes: attr})
if err != nil {
t.Fatal(err)
}
attr = RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 5)}}
ctx5, err := json.Marshal(&Member{ID: types.ID(5), RaftAttributes: attr})
if err != nil {
t.Fatal(err)
}
attr = RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 3)}}
ctx2to3, err := json.Marshal(&Member{ID: types.ID(2), RaftAttributes: attr})
if err != nil {
t.Fatal(err)
}
attr = RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 5)}}
ctx2to5, err := json.Marshal(&Member{ID: types.ID(2), RaftAttributes: attr})
if err != nil {
t.Fatal(err)
}
tests := []struct {
cc raftpb.ConfChange
werr error
}{
{
raftpb.ConfChange{
Type: raftpb.ConfChangeRemoveNode,
NodeID: 3,
},
nil,
},
{
raftpb.ConfChange{
Type: raftpb.ConfChangeAddNode,
NodeID: 4,
},
ErrIDRemoved,
},
{
raftpb.ConfChange{
Type: raftpb.ConfChangeRemoveNode,
NodeID: 4,
},
ErrIDRemoved,
},
{
raftpb.ConfChange{
Type: raftpb.ConfChangeAddNode,
NodeID: 1,
},
ErrIDExists,
},
{
raftpb.ConfChange{
Type: raftpb.ConfChangeAddNode,
NodeID: 5,
Context: ctx,
},
ErrPeerURLexists,
},
{
raftpb.ConfChange{
Type: raftpb.ConfChangeRemoveNode,
NodeID: 5,
},
ErrIDNotFound,
},
{
raftpb.ConfChange{
Type: raftpb.ConfChangeAddNode,
NodeID: 5,
Context: ctx5,
},
nil,
},
{
raftpb.ConfChange{
Type: raftpb.ConfChangeUpdateNode,
NodeID: 5,
Context: ctx,
},
ErrIDNotFound,
},
// try to change the peer url of 2 to the peer url of 3
{
raftpb.ConfChange{
Type: raftpb.ConfChangeUpdateNode,
NodeID: 2,
Context: ctx2to3,
},
ErrPeerURLexists,
},
{
raftpb.ConfChange{
Type: raftpb.ConfChangeUpdateNode,
NodeID: 2,
Context: ctx2to5,
},
nil,
},
}
for i, tt := range tests {
err := cl.ValidateConfigurationChange(tt.cc)
if err != tt.werr {
t.Errorf("#%d: validateConfigurationChange error = %v, want %v", i, err, tt.werr)
}
}
}
func TestClusterGenID(t *testing.T) {
cs := newTestCluster([]Member{
cs := newTestCluster([]*Member{
newTestMember(1, nil, "", nil),
newTestMember(2, nil, "", nil),
})
@ -365,7 +488,7 @@ func TestClusterGenID(t *testing.T) {
previd := cs.ID()
cs.SetStore(&storeRecorder{})
cs.AddMember(newTestMemberp(3, nil, "", nil))
cs.AddMember(newTestMember(3, nil, "", nil))
cs.genID()
if cs.ID() == previd {
t.Fatalf("cluster.ID = %v, want not %v", cs.ID(), previd)
@ -378,22 +501,22 @@ func TestNodeToMemberBad(t *testing.T) {
{Key: "/1234/strange"},
}},
{Key: "/1234", Nodes: []*store.NodeExtern{
{Key: "/1234/dynamic", Value: stringp("garbage")},
{Key: "/1234/raftAttributes", Value: stringp("garbage")},
}},
{Key: "/1234", Nodes: []*store.NodeExtern{
{Key: "/1234/dynamic", Value: stringp(`{"peerURLs":null}`)},
{Key: "/1234/attributes", Value: stringp(`{"name":"node1","clientURLs":null}`)},
}},
{Key: "/1234", Nodes: []*store.NodeExtern{
{Key: "/1234/dynamic", Value: stringp(`{"peerURLs":null}`)},
{Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)},
{Key: "/1234/strange"},
}},
{Key: "/1234", Nodes: []*store.NodeExtern{
{Key: "/1234/dynamic", Value: stringp(`{"peerURLs":null}`)},
{Key: "/1234/static", Value: stringp("garbage")},
{Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)},
{Key: "/1234/attributes", Value: stringp("garbage")},
}},
{Key: "/1234", Nodes: []*store.NodeExtern{
{Key: "/1234/dynamic", Value: stringp(`{"peerURLs":null}`)},
{Key: "/1234/static", Value: stringp(`{"name":"node1","clientURLs":null}`)},
{Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)},
{Key: "/1234/attributes", Value: stringp(`{"name":"node1","clientURLs":null}`)},
{Key: "/1234/strange"},
}},
}
@ -408,7 +531,7 @@ func TestClusterAddMember(t *testing.T) {
st := &storeRecorder{}
c := newTestCluster(nil)
c.SetStore(st)
c.AddMember(newTestMemberp(1, nil, "node1", nil))
c.AddMember(newTestMember(1, nil, "node1", nil))
wactions := []action{
{
@ -421,16 +544,6 @@ func TestClusterAddMember(t *testing.T) {
store.Permanent,
},
},
{
name: "Create",
params: []interface{}{
path.Join(storeMembersPrefix, "1", "attributes"),
false,
`{"name":"node1"}`,
false,
store.Permanent,
},
},
}
if g := st.Action(); !reflect.DeepEqual(g, wactions) {
t.Errorf("actions = %v, want %v", g, wactions)
@ -439,7 +552,7 @@ func TestClusterAddMember(t *testing.T) {
func TestClusterMembers(t *testing.T) {
cls := &Cluster{
members: map[uint64]*Member{
members: map[types.ID]*Member{
1: &Member{ID: 1},
20: &Member{ID: 20},
100: &Member{ID: 100},
@ -461,33 +574,33 @@ func TestClusterMembers(t *testing.T) {
func TestClusterString(t *testing.T) {
cls := &Cluster{
members: map[uint64]*Member{
1: newTestMemberp(
members: map[types.ID]*Member{
1: newTestMember(
1,
[]string{"http://1.1.1.1:1111", "http://0.0.0.0:0000"},
"abc",
nil,
),
2: newTestMemberp(
2: newTestMember(
2,
[]string{"http://2.2.2.2:2222"},
"def",
nil,
),
3: newTestMemberp(
3: newTestMember(
3,
[]string{"http://3.3.3.3:1234", "http://127.0.0.1:7001"},
"ghi",
nil,
),
// no PeerURLs = not included
4: newTestMemberp(
4: newTestMember(
4,
[]string{},
"four",
nil,
),
5: newTestMemberp(
5: newTestMember(
5,
nil,
"five",
@ -532,23 +645,10 @@ func TestNodeToMember(t *testing.T) {
}
}
func newTestCluster(membs []Member) *Cluster {
c := &Cluster{members: make(map[uint64]*Member), removed: make(map[uint64]bool)}
for i, m := range membs {
c.members[m.ID] = &membs[i]
func newTestCluster(membs []*Member) *Cluster {
c := &Cluster{members: make(map[types.ID]*Member), removed: make(map[types.ID]bool)}
for _, m := range membs {
c.members[m.ID] = m
}
return c
}
func newTestMember(id uint64, peerURLs []string, name string, clientURLs []string) Member {
return Member{
ID: id,
RaftAttributes: RaftAttributes{PeerURLs: peerURLs},
Attributes: Attributes{Name: name, ClientURLs: clientURLs},
}
}
func newTestMemberp(id uint64, peerURLs []string, name string, clientURLs []string) *Member {
m := newTestMember(id, peerURLs, name, clientURLs)
return &m
}

View File

@ -18,8 +18,11 @@ package etcdserver
import (
"fmt"
"log"
"net/http"
"path"
"reflect"
"sort"
"github.com/coreos/etcd/pkg/types"
"github.com/coreos/etcd/raft"
@ -27,14 +30,17 @@ import (
// ServerConfig holds the configuration of etcd as taken from the command line or discovery.
type ServerConfig struct {
Name string
DiscoveryURL string
ClientURLs types.URLs
DataDir string
SnapCount uint64
Cluster *Cluster
ClusterState ClusterState
Transport *http.Transport
Name string
DiscoveryURL string
DiscoveryProxy string
ClientURLs types.URLs
PeerURLs types.URLs
DataDir string
SnapCount uint64
Cluster *Cluster
NewCluster bool
ForceNewCluster bool
Transport *http.Transport
}
// VerifyBootstrapConfig sanity-checks the initial config and returns an error
@ -43,13 +49,13 @@ func (c *ServerConfig) VerifyBootstrapConfig() error {
m := c.Cluster.MemberByName(c.Name)
// Make sure the cluster at least contains the local server.
if m == nil {
return fmt.Errorf("couldn't find local name %s in the initial cluster configuration", c.Name)
return fmt.Errorf("couldn't find local name %q in the initial cluster configuration", c.Name)
}
if m.ID == raft.None {
if uint64(m.ID) == raft.None {
return fmt.Errorf("cannot use %x as member id", raft.None)
}
if c.DiscoveryURL == "" && c.ClusterState != ClusterStateValueNew {
if c.DiscoveryURL == "" && !c.NewCluster {
return fmt.Errorf("initial cluster state unset and no wal or discovery URL found")
}
@ -63,6 +69,13 @@ func (c *ServerConfig) VerifyBootstrapConfig() error {
urlMap[url] = true
}
}
// Advertised peer URLs must match those in the cluster peer list
apurls := c.PeerURLs.StringSlice()
sort.Strings(apurls)
if !reflect.DeepEqual(apurls, m.PeerURLs) {
return fmt.Errorf("%s has different advertised URLs in the cluster and advertised peer URLs list", c.Name)
}
return nil
}
@ -73,3 +86,31 @@ func (c *ServerConfig) SnapDir() string { return path.Join(c.DataDir, "snap") }
func (c *ServerConfig) ShouldDiscover() bool {
return c.DiscoveryURL != ""
}
func (c *ServerConfig) PrintWithInitial() {
c.print(true)
}
func (c *ServerConfig) Print() {
c.print(false)
}
func (c *ServerConfig) print(initial bool) {
log.Printf("etcdserver: name = %s", c.Name)
if c.ForceNewCluster {
log.Println("etcdserver: force new cluster")
}
log.Printf("etcdserver: data dir = %s", c.DataDir)
log.Printf("etcdserver: snapshot count = %d", c.SnapCount)
if len(c.DiscoveryURL) != 0 {
log.Printf("etcdserver: discovery URL= %s", c.DiscoveryURL)
if len(c.DiscoveryProxy) != 0 {
log.Printf("etcdserver: discovery proxy = %s", c.DiscoveryProxy)
}
}
log.Printf("etcdserver: advertise client URLs = %s", c.ClientURLs)
if initial {
log.Printf("etcdserver: initial advertise peer URLs = %s", c.PeerURLs)
log.Printf("etcdserver: initial cluster = %s", c.Cluster)
}
}

View File

@ -16,50 +16,90 @@
package etcdserver
import "testing"
import (
"net/url"
"testing"
"github.com/coreos/etcd/pkg/types"
)
func mustNewURLs(t *testing.T, urls []string) []url.URL {
u, err := types.NewURLs(urls)
if err != nil {
t.Fatalf("error creating new URLs from %q: %v", urls, err)
}
return u
}
func TestBootstrapConfigVerify(t *testing.T) {
tests := []struct {
clusterSetting string
clst ClusterState
newclst bool
apurls []string
disc string
shouldError bool
}{
{
// Node must exist in cluster
"",
ClusterStateValueNew,
true,
nil,
"",
true,
},
{
// Cannot have duplicate URLs in cluster config
"node1=http://localhost:7001,node2=http://localhost:7001,node2=http://localhost:7002",
ClusterStateValueNew,
true,
nil,
"",
true,
},
{
// Node defined, ClusterState OK
"node1=http://localhost:7001,node2=http://localhost:7002",
ClusterStateValueNew,
true,
[]string{"http://localhost:7001"},
"",
false,
},
{
// Node defined, discovery OK
"node1=http://localhost:7001",
// TODO(jonboulle): replace with ClusterStateExisting once it exists
"",
false,
[]string{"http://localhost:7001"},
"http://discovery",
false,
},
{
// Cannot have ClusterState!=new && !discovery
"node1=http://localhost:7001",
// TODO(jonboulle): replace with ClusterStateExisting once it exists
ClusterState("foo"),
false,
nil,
"",
true,
},
{
// Advertised peer URLs must match those in cluster-state
"node1=http://localhost:7001",
true,
[]string{"http://localhost:12345"},
"",
true,
},
{
// Advertised peer URLs must match those in cluster-state
"node1=http://localhost:7001,node1=http://localhost:12345",
true,
[]string{"http://localhost:12345"},
"",
true,
},
}
@ -69,12 +109,14 @@ func TestBootstrapConfigVerify(t *testing.T) {
if err != nil {
t.Fatalf("#%d: Got unexpected error: %v", i, err)
}
cfg := ServerConfig{
Name: "node1",
DiscoveryURL: tt.disc,
Cluster: cluster,
ClusterState: tt.clst,
NewCluster: tt.newclst,
}
if tt.apurls != nil {
cfg.PeerURLs = mustNewURLs(t, tt.apurls)
}
err = cfg.VerifyBootstrapConfig()
if (err == nil) && tt.shouldError {

View File

@ -0,0 +1,600 @@
/*
Copyright 2014 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 etcdhttp
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/jonboulle/clockwork"
etcdErr "github.com/coreos/etcd/error"
"github.com/coreos/etcd/etcdserver"
"github.com/coreos/etcd/etcdserver/etcdhttp/httptypes"
"github.com/coreos/etcd/etcdserver/etcdserverpb"
"github.com/coreos/etcd/pkg/types"
"github.com/coreos/etcd/store"
"github.com/coreos/etcd/version"
)
const (
keysPrefix = "/v2/keys"
deprecatedMachinesPrefix = "/v2/machines"
membersPrefix = "/v2/members"
statsPrefix = "/v2/stats"
versionPrefix = "/version"
)
// NewClientHandler generates a muxed http.Handler with the given parameters to serve etcd client requests.
func NewClientHandler(server *etcdserver.EtcdServer) http.Handler {
kh := &keysHandler{
server: server,
clusterInfo: server.Cluster,
timer: server,
timeout: defaultServerTimeout,
}
sh := &statsHandler{
stats: server,
}
mh := &membersHandler{
server: server,
clusterInfo: server.Cluster,
clock: clockwork.NewRealClock(),
}
dmh := &deprecatedMachinesHandler{
clusterInfo: server.Cluster,
}
mux := http.NewServeMux()
mux.HandleFunc("/", http.NotFound)
mux.HandleFunc(versionPrefix, serveVersion)
mux.Handle(keysPrefix, kh)
mux.Handle(keysPrefix+"/", kh)
mux.HandleFunc(statsPrefix+"/store", sh.serveStore)
mux.HandleFunc(statsPrefix+"/self", sh.serveSelf)
mux.HandleFunc(statsPrefix+"/leader", sh.serveLeader)
mux.Handle(membersPrefix, mh)
mux.Handle(membersPrefix+"/", mh)
mux.Handle(deprecatedMachinesPrefix, dmh)
return mux
}
type keysHandler struct {
server etcdserver.Server
clusterInfo etcdserver.ClusterInfo
timer etcdserver.RaftTimer
timeout time.Duration
}
func (h *keysHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "HEAD", "GET", "PUT", "POST", "DELETE") {
return
}
w.Header().Set("X-Etcd-Cluster-ID", h.clusterInfo.ID().String())
ctx, cancel := context.WithTimeout(context.Background(), h.timeout)
defer cancel()
rr, err := parseKeyRequest(r, etcdserver.GenID(), clockwork.NewRealClock())
if err != nil {
writeError(w, err)
return
}
resp, err := h.server.Do(ctx, rr)
if err != nil {
err = trimErrorPrefix(err, etcdserver.StoreKeysPrefix)
writeError(w, err)
return
}
switch {
case resp.Event != nil:
if err := writeKeyEvent(w, resp.Event, h.timer); err != nil {
// Should never be reached
log.Printf("error writing event: %v", err)
}
case resp.Watcher != nil:
ctx, cancel := context.WithTimeout(context.Background(), defaultWatchTimeout)
defer cancel()
handleKeyWatch(ctx, w, resp.Watcher, rr.Stream, h.timer)
default:
writeError(w, errors.New("received response with no Event/Watcher!"))
}
}
type deprecatedMachinesHandler struct {
clusterInfo etcdserver.ClusterInfo
}
func (h *deprecatedMachinesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET", "HEAD") {
return
}
endpoints := h.clusterInfo.ClientURLs()
w.Write([]byte(strings.Join(endpoints, ", ")))
}
type membersHandler struct {
server etcdserver.Server
clusterInfo etcdserver.ClusterInfo
clock clockwork.Clock
}
func (h *membersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET", "POST", "DELETE", "PUT") {
return
}
w.Header().Set("X-Etcd-Cluster-ID", h.clusterInfo.ID().String())
ctx, cancel := context.WithTimeout(context.Background(), defaultServerTimeout)
defer cancel()
switch r.Method {
case "GET":
if trimPrefix(r.URL.Path, membersPrefix) != "" {
writeError(w, httptypes.NewHTTPError(http.StatusNotFound, "Not found"))
return
}
mc := newMemberCollection(h.clusterInfo.Members())
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(mc); err != nil {
log.Printf("etcdhttp: %v", err)
}
case "POST":
req := httptypes.MemberCreateRequest{}
if ok := unmarshalRequest(r, &req, w); !ok {
return
}
now := h.clock.Now()
m := etcdserver.NewMember("", req.PeerURLs, "", &now)
err := h.server.AddMember(ctx, *m)
switch {
case err == etcdserver.ErrIDExists || err == etcdserver.ErrPeerURLexists:
writeError(w, httptypes.NewHTTPError(http.StatusConflict, err.Error()))
return
case err != nil:
log.Printf("etcdhttp: error adding node %s: %v", m.ID, err)
writeError(w, err)
return
}
res := newMember(m)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(res); err != nil {
log.Printf("etcdhttp: %v", err)
}
case "DELETE":
id, ok := getID(r.URL.Path, w)
if !ok {
return
}
err := h.server.RemoveMember(ctx, uint64(id))
switch {
case err == etcdserver.ErrIDRemoved:
writeError(w, httptypes.NewHTTPError(http.StatusGone, fmt.Sprintf("Member permanently removed: %s", id)))
case err == etcdserver.ErrIDNotFound:
writeError(w, httptypes.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No such member: %s", id)))
case err != nil:
log.Printf("etcdhttp: error removing node %s: %v", id, err)
writeError(w, err)
default:
w.WriteHeader(http.StatusNoContent)
}
case "PUT":
id, ok := getID(r.URL.Path, w)
if !ok {
return
}
req := httptypes.MemberUpdateRequest{}
if ok := unmarshalRequest(r, &req, w); !ok {
return
}
m := etcdserver.Member{
ID: id,
RaftAttributes: etcdserver.RaftAttributes{PeerURLs: req.PeerURLs.StringSlice()},
}
err := h.server.UpdateMember(ctx, m)
switch {
case err == etcdserver.ErrPeerURLexists:
writeError(w, httptypes.NewHTTPError(http.StatusConflict, err.Error()))
case err == etcdserver.ErrIDNotFound:
writeError(w, httptypes.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No such member: %s", id)))
case err != nil:
log.Printf("etcdhttp: error updating node %s: %v", m.ID, err)
writeError(w, err)
default:
w.WriteHeader(http.StatusNoContent)
}
}
}
type statsHandler struct {
stats etcdserver.Stats
}
func (h *statsHandler) serveStore(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET") {
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(h.stats.StoreStats())
}
func (h *statsHandler) serveSelf(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET") {
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(h.stats.SelfStats())
}
func (h *statsHandler) serveLeader(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET") {
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(h.stats.LeaderStats())
}
func serveVersion(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET") {
return
}
w.Write([]byte("etcd " + version.Version))
}
// parseKeyRequest converts a received http.Request on keysPrefix to
// a server Request, performing validation of supplied fields as appropriate.
// If any validation fails, an empty Request and non-nil error is returned.
func parseKeyRequest(r *http.Request, id uint64, clock clockwork.Clock) (etcdserverpb.Request, error) {
emptyReq := etcdserverpb.Request{}
err := r.ParseForm()
if err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidForm,
err.Error(),
)
}
if !strings.HasPrefix(r.URL.Path, keysPrefix) {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidForm,
"incorrect key prefix",
)
}
p := path.Join(etcdserver.StoreKeysPrefix, r.URL.Path[len(keysPrefix):])
var pIdx, wIdx uint64
if pIdx, err = getUint64(r.Form, "prevIndex"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeIndexNaN,
`invalid value for "prevIndex"`,
)
}
if wIdx, err = getUint64(r.Form, "waitIndex"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeIndexNaN,
`invalid value for "waitIndex"`,
)
}
var rec, sort, wait, dir, quorum, stream bool
if rec, err = getBool(r.Form, "recursive"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`invalid value for "recursive"`,
)
}
if sort, err = getBool(r.Form, "sorted"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`invalid value for "sorted"`,
)
}
if wait, err = getBool(r.Form, "wait"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`invalid value for "wait"`,
)
}
// TODO(jonboulle): define what parameters dir is/isn't compatible with?
if dir, err = getBool(r.Form, "dir"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`invalid value for "dir"`,
)
}
if quorum, err = getBool(r.Form, "quorum"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`invalid value for "quorum"`,
)
}
if stream, err = getBool(r.Form, "stream"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`invalid value for "stream"`,
)
}
if wait && r.Method != "GET" {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`"wait" can only be used with GET requests`,
)
}
pV := r.FormValue("prevValue")
if _, ok := r.Form["prevValue"]; ok && pV == "" {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodePrevValueRequired,
`"prevValue" cannot be empty`,
)
}
// TTL is nullable, so leave it null if not specified
// or an empty string
var ttl *uint64
if len(r.FormValue("ttl")) > 0 {
i, err := getUint64(r.Form, "ttl")
if err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeTTLNaN,
`invalid value for "ttl"`,
)
}
ttl = &i
}
// prevExist is nullable, so leave it null if not specified
var pe *bool
if _, ok := r.Form["prevExist"]; ok {
bv, err := getBool(r.Form, "prevExist")
if err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
"invalid value for prevExist",
)
}
pe = &bv
}
rr := etcdserverpb.Request{
ID: id,
Method: r.Method,
Path: p,
Val: r.FormValue("value"),
Dir: dir,
PrevValue: pV,
PrevIndex: pIdx,
PrevExist: pe,
Wait: wait,
Since: wIdx,
Recursive: rec,
Sorted: sort,
Quorum: quorum,
Stream: stream,
}
if pe != nil {
rr.PrevExist = pe
}
// Null TTL is equivalent to unset Expiration
if ttl != nil {
expr := time.Duration(*ttl) * time.Second
rr.Expiration = clock.Now().Add(expr).UnixNano()
}
return rr, nil
}
// writeKeyEvent trims the prefix of key path in a single Event under
// StoreKeysPrefix, serializes it and writes the resulting JSON to the given
// ResponseWriter, along with the appropriate headers.
func writeKeyEvent(w http.ResponseWriter, ev *store.Event, rt etcdserver.RaftTimer) error {
if ev == nil {
return errors.New("cannot write empty Event!")
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Etcd-Index", fmt.Sprint(ev.EtcdIndex))
w.Header().Set("X-Raft-Index", fmt.Sprint(rt.Index()))
w.Header().Set("X-Raft-Term", fmt.Sprint(rt.Term()))
if ev.IsCreated() {
w.WriteHeader(http.StatusCreated)
}
ev = trimEventPrefix(ev, etcdserver.StoreKeysPrefix)
return json.NewEncoder(w).Encode(ev)
}
func handleKeyWatch(ctx context.Context, w http.ResponseWriter, wa store.Watcher, stream bool, rt etcdserver.RaftTimer) {
defer wa.Remove()
ech := wa.EventChan()
var nch <-chan bool
if x, ok := w.(http.CloseNotifier); ok {
nch = x.CloseNotify()
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Etcd-Index", fmt.Sprint(wa.StartIndex()))
w.Header().Set("X-Raft-Index", fmt.Sprint(rt.Index()))
w.Header().Set("X-Raft-Term", fmt.Sprint(rt.Term()))
w.WriteHeader(http.StatusOK)
// Ensure headers are flushed early, in case of long polling
w.(http.Flusher).Flush()
for {
select {
case <-nch:
// Client closed connection. Nothing to do.
return
case <-ctx.Done():
// Timed out. net/http will close the connection for us, so nothing to do.
return
case ev, ok := <-ech:
if !ok {
// If the channel is closed this may be an indication of
// that notifications are much more than we are able to
// send to the client in time. Then we simply end streaming.
return
}
ev = trimEventPrefix(ev, etcdserver.StoreKeysPrefix)
if err := json.NewEncoder(w).Encode(ev); err != nil {
// Should never be reached
log.Printf("error writing event: %v\n", err)
return
}
if !stream {
return
}
w.(http.Flusher).Flush()
}
}
}
func trimEventPrefix(ev *store.Event, prefix string) *store.Event {
if ev == nil {
return nil
}
// Since the *Event may reference one in the store history
// history, we must copy it before modifying
e := ev.Clone()
e.Node = trimNodeExternPrefix(e.Node, prefix)
e.PrevNode = trimNodeExternPrefix(e.PrevNode, prefix)
return e
}
func trimNodeExternPrefix(n *store.NodeExtern, prefix string) *store.NodeExtern {
if n == nil {
return nil
}
n.Key = strings.TrimPrefix(n.Key, prefix)
for _, nn := range n.Nodes {
nn = trimNodeExternPrefix(nn, prefix)
}
return n
}
func trimErrorPrefix(err error, prefix string) error {
if e, ok := err.(*etcdErr.Error); ok {
e.Cause = strings.TrimPrefix(e.Cause, prefix)
}
return err
}
func unmarshalRequest(r *http.Request, req json.Unmarshaler, w http.ResponseWriter) bool {
ctype := r.Header.Get("Content-Type")
if ctype != "application/json" {
writeError(w, httptypes.NewHTTPError(http.StatusUnsupportedMediaType, fmt.Sprintf("Bad Content-Type %s, accept application/json", ctype)))
return false
}
b, err := ioutil.ReadAll(r.Body)
if err != nil {
writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, err.Error()))
return false
}
if err := req.UnmarshalJSON(b); err != nil {
writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, err.Error()))
return false
}
return true
}
func getID(p string, w http.ResponseWriter) (types.ID, bool) {
idStr := trimPrefix(p, membersPrefix)
if idStr == "" {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return 0, false
}
id, err := types.IDFromString(idStr)
if err != nil {
writeError(w, httptypes.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No such member: %s", idStr)))
return 0, false
}
return id, true
}
// getUint64 extracts a uint64 by the given key from a Form. If the key does
// not exist in the form, 0 is returned. If the key exists but the value is
// badly formed, an error is returned. If multiple values are present only the
// first is considered.
func getUint64(form url.Values, key string) (i uint64, err error) {
if vals, ok := form[key]; ok {
i, err = strconv.ParseUint(vals[0], 10, 64)
}
return
}
// getBool extracts a bool by the given key from a Form. If the key does not
// exist in the form, false is returned. If the key exists but the value is
// badly formed, an error is returned. If multiple values are present only the
// first is considered.
func getBool(form url.Values, key string) (b bool, err error) {
if vals, ok := form[key]; ok {
b, err = strconv.ParseBool(vals[0])
}
return
}
// trimPrefix removes a given prefix and any slash following the prefix
// e.g.: trimPrefix("foo", "foo") == trimPrefix("foo/", "foo") == ""
func trimPrefix(p, prefix string) (s string) {
s = strings.TrimPrefix(p, prefix)
s = strings.TrimPrefix(s, "/")
return
}
func newMemberCollection(ms []*etcdserver.Member) *httptypes.MemberCollection {
c := httptypes.MemberCollection(make([]httptypes.Member, len(ms)))
for i, m := range ms {
c[i] = newMember(m)
}
return &c
}
func newMember(m *etcdserver.Member) httptypes.Member {
tm := httptypes.Member{
ID: m.ID.String(),
Name: m.Name,
PeerURLs: make([]string, len(m.PeerURLs)),
ClientURLs: make([]string, len(m.ClientURLs)),
}
copy(tm.PeerURLs, m.PeerURLs)
copy(tm.ClientURLs, m.ClientURLs)
return tm
}

File diff suppressed because it is too large Load Diff

View File

@ -17,42 +17,19 @@
package etcdhttp
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/jonboulle/clockwork"
etcdErr "github.com/coreos/etcd/error"
"github.com/coreos/etcd/etcdserver"
"github.com/coreos/etcd/etcdserver/etcdserverpb"
"github.com/coreos/etcd/pkg/types"
"github.com/coreos/etcd/raft/raftpb"
"github.com/coreos/etcd/store"
"github.com/coreos/etcd/version"
"github.com/coreos/etcd/etcdserver/etcdhttp/httptypes"
)
const (
// prefixes of client endpoint
keysPrefix = "/v2/keys"
deprecatedMachinesPrefix = "/v2/machines"
adminMembersPrefix = "/v2/admin/members/"
statsPrefix = "/v2/stats"
versionPrefix = "/version"
// prefixes of peer endpoint
raftPrefix = "/raft"
membersPrefix = "/members"
// time to wait for response from EtcdServer requests
defaultServerTimeout = 500 * time.Millisecond
defaultServerTimeout = 5 * time.Minute
// time to wait for a Watch request
defaultWatchTimeout = 5 * time.Minute
@ -60,433 +37,6 @@ const (
var errClosed = errors.New("etcdhttp: client closed connection")
// NewClientHandler generates a muxed http.Handler with the given parameters to serve etcd client requests.
func NewClientHandler(server *etcdserver.EtcdServer) http.Handler {
sh := &serverHandler{
server: server,
clusterInfo: server.Cluster,
stats: server,
timer: server,
timeout: defaultServerTimeout,
clock: clockwork.NewRealClock(),
}
mux := http.NewServeMux()
mux.HandleFunc(keysPrefix, sh.serveKeys)
mux.HandleFunc(keysPrefix+"/", sh.serveKeys)
mux.HandleFunc(statsPrefix+"/store", sh.serveStoreStats)
mux.HandleFunc(statsPrefix+"/self", sh.serveSelfStats)
mux.HandleFunc(statsPrefix+"/leader", sh.serveLeaderStats)
mux.HandleFunc(deprecatedMachinesPrefix, sh.serveMachines)
mux.HandleFunc(adminMembersPrefix, sh.serveAdminMembers)
mux.HandleFunc(versionPrefix, sh.serveVersion)
mux.HandleFunc("/", http.NotFound)
return mux
}
// NewPeerHandler generates an http.Handler to handle etcd peer (raft) requests.
func NewPeerHandler(server *etcdserver.EtcdServer) http.Handler {
sh := &serverHandler{
server: server,
stats: server,
clusterInfo: server.Cluster,
clock: clockwork.NewRealClock(),
}
mux := http.NewServeMux()
mux.HandleFunc(raftPrefix, sh.serveRaft)
mux.HandleFunc(membersPrefix, sh.serveMembers)
mux.HandleFunc("/", http.NotFound)
return mux
}
// serverHandler provides http.Handlers for etcd client and raft communication.
type serverHandler struct {
timeout time.Duration
server etcdserver.Server
stats etcdserver.Stats
timer etcdserver.RaftTimer
clusterInfo etcdserver.ClusterInfo
clock clockwork.Clock
}
func (h serverHandler) serveKeys(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET", "PUT", "POST", "DELETE") {
return
}
ctx, cancel := context.WithTimeout(context.Background(), h.timeout)
defer cancel()
rr, err := parseKeyRequest(r, etcdserver.GenID(), clockwork.NewRealClock())
if err != nil {
writeError(w, err)
return
}
resp, err := h.server.Do(ctx, rr)
if err != nil {
err = trimErrorPrefix(err, etcdserver.StoreKeysPrefix)
writeError(w, err)
return
}
switch {
case resp.Event != nil:
if err := writeKeyEvent(w, resp.Event, h.timer); err != nil {
// Should never be reached
log.Printf("error writing event: %v", err)
}
case resp.Watcher != nil:
ctx, cancel := context.WithTimeout(context.Background(), defaultWatchTimeout)
defer cancel()
handleKeyWatch(ctx, w, resp.Watcher, rr.Stream, h.timer)
default:
writeError(w, errors.New("received response with no Event/Watcher!"))
}
}
// serveMachines responds address list in the format '0.0.0.0, 1.1.1.1'.
func (h serverHandler) serveMachines(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET", "HEAD") {
return
}
endpoints := h.clusterInfo.ClientURLs()
w.Write([]byte(strings.Join(endpoints, ", ")))
}
func (h serverHandler) serveAdminMembers(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET", "POST", "DELETE") {
return
}
ctx, cancel := context.WithTimeout(context.Background(), defaultServerTimeout)
defer cancel()
switch r.Method {
case "GET":
if s := strings.TrimPrefix(r.URL.Path, adminMembersPrefix); s != "" {
http.NotFound(w, r)
return
}
ms := struct {
Members []*etcdserver.Member `json:"members"`
}{
Members: h.clusterInfo.Members(),
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(ms); err != nil {
log.Printf("etcdhttp: %v", err)
}
case "POST":
ctype := r.Header.Get("Content-Type")
if ctype != "application/json" {
http.Error(w, fmt.Sprintf("bad Content-Type %s, accept application/json", ctype), http.StatusBadRequest)
return
}
b, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
raftAttr := etcdserver.RaftAttributes{}
if err := json.Unmarshal(b, &raftAttr); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
validURLs, err := types.NewURLs(raftAttr.PeerURLs)
if err != nil {
http.Error(w, "bad peer urls", http.StatusBadRequest)
return
}
now := h.clock.Now()
m := etcdserver.NewMember("", validURLs, "", &now)
if err := h.server.AddMember(ctx, *m); err != nil {
log.Printf("etcdhttp: error adding node %x: %v", m.ID, err)
writeError(w, err)
return
}
log.Printf("etcdhttp: added node %x with peer urls %v", m.ID, raftAttr.PeerURLs)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(m); err != nil {
log.Printf("etcdhttp: %v", err)
}
case "DELETE":
idStr := strings.TrimPrefix(r.URL.Path, adminMembersPrefix)
id, err := strconv.ParseUint(idStr, 16, 64)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Printf("etcdhttp: remove node %x", id)
if err := h.server.RemoveMember(ctx, id); err != nil {
log.Printf("etcdhttp: error removing node %x: %v", id, err)
writeError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
}
func (h serverHandler) serveStoreStats(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET") {
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(h.stats.StoreStats())
}
func (h serverHandler) serveSelfStats(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET") {
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(h.stats.SelfStats())
}
func (h serverHandler) serveLeaderStats(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET") {
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(h.stats.LeaderStats())
}
func (h serverHandler) serveVersion(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET") {
return
}
w.Write([]byte("etcd " + version.Version))
}
func (h serverHandler) serveRaft(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "POST") {
return
}
wcid := strconv.FormatUint(h.clusterInfo.ID(), 16)
w.Header().Set("X-Etcd-Cluster-ID", wcid)
gcid := r.Header.Get("X-Etcd-Cluster-ID")
if gcid != wcid {
log.Printf("etcdhttp: request ignored due to cluster ID mismatch got %s want %s", gcid, wcid)
http.Error(w, "clusterID mismatch", http.StatusPreconditionFailed)
return
}
b, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Println("etcdhttp: error reading raft message:", err)
http.Error(w, "error reading raft message", http.StatusBadRequest)
return
}
var m raftpb.Message
if err := m.Unmarshal(b); err != nil {
log.Println("etcdhttp: error unmarshaling raft message:", err)
http.Error(w, "error unmarshaling raft message", http.StatusBadRequest)
return
}
if err := h.server.Process(context.TODO(), m); err != nil {
log.Println("etcdhttp: error processing raft message:", err)
switch err {
case etcdserver.ErrRemoved:
http.Error(w, "cannot process message from removed node", http.StatusForbidden)
default:
writeError(w, err)
}
return
}
if m.Type == raftpb.MsgApp {
h.stats.UpdateRecvApp(m.From, r.ContentLength)
}
w.WriteHeader(http.StatusNoContent)
}
func (h serverHandler) serveMembers(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET") {
return
}
cid := strconv.FormatUint(h.clusterInfo.ID(), 16)
w.Header().Set("X-Etcd-Cluster-ID", cid)
if r.URL.Path != membersPrefix {
http.Error(w, "bad path", http.StatusBadRequest)
return
}
ms := h.clusterInfo.Members()
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(ms); err != nil {
log.Printf("etcdhttp: %v", err)
}
}
// parseKeyRequest converts a received http.Request on keysPrefix to
// a server Request, performing validation of supplied fields as appropriate.
// If any validation fails, an empty Request and non-nil error is returned.
func parseKeyRequest(r *http.Request, id uint64, clock clockwork.Clock) (etcdserverpb.Request, error) {
emptyReq := etcdserverpb.Request{}
err := r.ParseForm()
if err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidForm,
err.Error(),
)
}
if !strings.HasPrefix(r.URL.Path, keysPrefix) {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidForm,
"incorrect key prefix",
)
}
p := path.Join(etcdserver.StoreKeysPrefix, r.URL.Path[len(keysPrefix):])
var pIdx, wIdx uint64
if pIdx, err = getUint64(r.Form, "prevIndex"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeIndexNaN,
`invalid value for "prevIndex"`,
)
}
if wIdx, err = getUint64(r.Form, "waitIndex"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeIndexNaN,
`invalid value for "waitIndex"`,
)
}
var rec, sort, wait, dir, quorum, stream bool
if rec, err = getBool(r.Form, "recursive"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`invalid value for "recursive"`,
)
}
if sort, err = getBool(r.Form, "sorted"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`invalid value for "sorted"`,
)
}
if wait, err = getBool(r.Form, "wait"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`invalid value for "wait"`,
)
}
// TODO(jonboulle): define what parameters dir is/isn't compatible with?
if dir, err = getBool(r.Form, "dir"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`invalid value for "dir"`,
)
}
if quorum, err = getBool(r.Form, "quorum"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`invalid value for "quorum"`,
)
}
if stream, err = getBool(r.Form, "stream"); err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`invalid value for "stream"`,
)
}
if wait && r.Method != "GET" {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`"wait" can only be used with GET requests`,
)
}
pV := r.FormValue("prevValue")
if _, ok := r.Form["prevValue"]; ok && pV == "" {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
`"prevValue" cannot be empty`,
)
}
// TTL is nullable, so leave it null if not specified
// or an empty string
var ttl *uint64
if len(r.FormValue("ttl")) > 0 {
i, err := getUint64(r.Form, "ttl")
if err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeTTLNaN,
`invalid value for "ttl"`,
)
}
ttl = &i
}
// prevExist is nullable, so leave it null if not specified
var pe *bool
if _, ok := r.Form["prevExist"]; ok {
bv, err := getBool(r.Form, "prevExist")
if err != nil {
return emptyReq, etcdErr.NewRequestError(
etcdErr.EcodeInvalidField,
"invalid value for prevExist",
)
}
pe = &bv
}
rr := etcdserverpb.Request{
ID: id,
Method: r.Method,
Path: p,
Val: r.FormValue("value"),
Dir: dir,
PrevValue: pV,
PrevIndex: pIdx,
PrevExist: pe,
Wait: wait,
Since: wIdx,
Recursive: rec,
Sorted: sort,
Quorum: quorum,
Stream: stream,
}
if pe != nil {
rr.PrevExist = pe
}
// Null TTL is equivalent to unset Expiration
if ttl != nil {
expr := time.Duration(*ttl) * time.Second
rr.Expiration = clock.Now().Add(expr).UnixNano()
}
return rr, nil
}
// getUint64 extracts a uint64 by the given key from a Form. If the key does
// not exist in the form, 0 is returned. If the key exists but the value is
// badly formed, an error is returned. If multiple values are present only the
// first is considered.
func getUint64(form url.Values, key string) (i uint64, err error) {
if vals, ok := form[key]; ok {
i, err = strconv.ParseUint(vals[0], 10, 64)
}
return
}
// getBool extracts a bool by the given key from a Form. If the key does not
// exist in the form, false is returned. If the key exists but the value is
// badly formed, an error is returned. If multiple values are present only the
// first is considered.
func getBool(form url.Values, key string) (b bool, err error) {
if vals, ok := form[key]; ok {
b, err = strconv.ParseBool(vals[0])
}
return
}
// writeError logs and writes the given Error to the ResponseWriter
// If Error is an etcdErr, it is rendered to the ResponseWriter
// Otherwise, it is assumed to be an InternalServerError
@ -494,77 +44,15 @@ func writeError(w http.ResponseWriter, err error) {
if err == nil {
return
}
log.Println(err)
if e, ok := err.(*etcdErr.Error); ok {
e.Write(w)
} else {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
// writeKeyEvent trims the prefix of key path in a single Event under
// StoreKeysPrefix, serializes it and writes the resulting JSON to the given
// ResponseWriter, along with the appropriate headers.
func writeKeyEvent(w http.ResponseWriter, ev *store.Event, rt etcdserver.RaftTimer) error {
if ev == nil {
return errors.New("cannot write empty Event!")
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Etcd-Index", fmt.Sprint(ev.EtcdIndex))
w.Header().Set("X-Raft-Index", fmt.Sprint(rt.Index()))
w.Header().Set("X-Raft-Term", fmt.Sprint(rt.Term()))
if ev.IsCreated() {
w.WriteHeader(http.StatusCreated)
}
ev = trimEventPrefix(ev, etcdserver.StoreKeysPrefix)
return json.NewEncoder(w).Encode(ev)
}
func handleKeyWatch(ctx context.Context, w http.ResponseWriter, wa store.Watcher, stream bool, rt etcdserver.RaftTimer) {
defer wa.Remove()
ech := wa.EventChan()
var nch <-chan bool
if x, ok := w.(http.CloseNotifier); ok {
nch = x.CloseNotify()
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Etcd-Index", fmt.Sprint(wa.StartIndex()))
w.Header().Set("X-Raft-Index", fmt.Sprint(rt.Index()))
w.Header().Set("X-Raft-Term", fmt.Sprint(rt.Term()))
w.WriteHeader(http.StatusOK)
// Ensure headers are flushed early, in case of long polling
w.(http.Flusher).Flush()
for {
select {
case <-nch:
// Client closed connection. Nothing to do.
return
case <-ctx.Done():
// Timed out. net/http will close the connection for us, so nothing to do.
return
case ev, ok := <-ech:
if !ok {
// If the channel is closed this may be an indication of
// that notifications are much more than we are able to
// send to the client in time. Then we simply end streaming.
return
}
ev = trimEventPrefix(ev, etcdserver.StoreKeysPrefix)
if err := json.NewEncoder(w).Encode(ev); err != nil {
// Should never be reached
log.Printf("error writing event: %v\n", err)
return
}
if !stream {
return
}
w.(http.Flusher).Flush()
}
switch e := err.(type) {
case *etcdErr.Error:
e.WriteTo(w)
case *httptypes.HTTPError:
e.WriteTo(w)
default:
log.Printf("etcdhttp: unexpected error: %v", err)
herr := httptypes.NewHTTPError(http.StatusInternalServerError, "Internal Server Error")
herr.WriteTo(w)
}
}
@ -581,30 +69,3 @@ func allowMethod(w http.ResponseWriter, m string, ms ...string) bool {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return false
}
func trimEventPrefix(ev *store.Event, prefix string) *store.Event {
if ev == nil {
return nil
}
ev.Node = trimNodeExternPrefix(ev.Node, prefix)
ev.PrevNode = trimNodeExternPrefix(ev.PrevNode, prefix)
return ev
}
func trimNodeExternPrefix(n *store.NodeExtern, prefix string) *store.NodeExtern {
if n == nil {
return nil
}
n.Key = strings.TrimPrefix(n.Key, prefix)
for _, nn := range n.Nodes {
nn = trimNodeExternPrefix(nn, prefix)
}
return n
}
func trimErrorPrefix(err error, prefix string) error {
if e, ok := err.(*etcdErr.Error); ok {
e.Cause = strings.TrimPrefix(e.Cause, prefix)
}
return err
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
/*
Copyright 2014 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 httptypes defines how etcd's HTTP API entities are serialized to and deserialized from JSON.
*/
package httptypes

View File

@ -0,0 +1,51 @@
/*
Copyright 2014 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 httptypes
import (
"encoding/json"
"log"
"net/http"
)
type HTTPError struct {
Message string `json:"message"`
// HTTP return code
Code int `json:"-"`
}
func (e HTTPError) Error() string {
return e.Message
}
// TODO(xiangli): handle http write errors
func (e HTTPError) WriteTo(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(e.Code)
b, err := json.Marshal(e)
if err != nil {
log.Panicf("marshal HTTPError should never fail: %v", err)
}
w.Write(b)
}
func NewHTTPError(code int, m string) *HTTPError {
return &HTTPError{
Message: m,
Code: code,
}
}

View File

@ -0,0 +1,49 @@
/*
Copyright 2014 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 httptypes
import (
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
func TestHTTPErrorWriteTo(t *testing.T) {
err := NewHTTPError(http.StatusBadRequest, "what a bad request you made!")
rr := httptest.NewRecorder()
err.WriteTo(rr)
wcode := http.StatusBadRequest
wheader := http.Header(map[string][]string{
"Content-Type": []string{"application/json"},
})
wbody := `{"message":"what a bad request you made!"}`
if wcode != rr.Code {
t.Errorf("HTTP status code %d, want %d", rr.Code, wcode)
}
if !reflect.DeepEqual(wheader, rr.HeaderMap) {
t.Errorf("HTTP headers %v, want %v", rr.HeaderMap, wheader)
}
gbody := rr.Body.String()
if wbody != gbody {
t.Errorf("HTTP body %q, want %q", gbody, wbody)
}
}

View File

@ -0,0 +1,101 @@
/*
Copyright 2014 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 httptypes
import (
"encoding/json"
"github.com/coreos/etcd/pkg/types"
)
type Member struct {
ID string `json:"id"`
Name string `json:"name"`
PeerURLs []string `json:"peerURLs"`
ClientURLs []string `json:"clientURLs"`
}
type MemberCreateRequest struct {
PeerURLs types.URLs
}
type MemberUpdateRequest struct {
MemberCreateRequest
}
func (m *MemberCreateRequest) MarshalJSON() ([]byte, error) {
s := struct {
PeerURLs []string `json:"peerURLs"`
}{
PeerURLs: make([]string, len(m.PeerURLs)),
}
for i, u := range m.PeerURLs {
s.PeerURLs[i] = u.String()
}
return json.Marshal(&s)
}
func (m *MemberCreateRequest) UnmarshalJSON(data []byte) error {
s := struct {
PeerURLs []string `json:"peerURLs"`
}{}
err := json.Unmarshal(data, &s)
if err != nil {
return err
}
urls, err := types.NewURLs(s.PeerURLs)
if err != nil {
return err
}
m.PeerURLs = urls
return nil
}
type MemberCollection []Member
func (c *MemberCollection) MarshalJSON() ([]byte, error) {
d := struct {
Members []Member `json:"members"`
}{
Members: []Member(*c),
}
return json.Marshal(d)
}
func (c *MemberCollection) UnmarshalJSON(data []byte) error {
d := struct {
Members []Member
}{}
if err := json.Unmarshal(data, &d); err != nil {
return err
}
if d.Members == nil {
*c = make([]Member, 0)
return nil
}
*c = d.Members
return nil
}

View File

@ -0,0 +1,220 @@
/*
Copyright 2014 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 httptypes
import (
"encoding/json"
"net/url"
"reflect"
"testing"
"github.com/coreos/etcd/pkg/types"
)
func TestMemberUnmarshal(t *testing.T) {
tests := []struct {
body []byte
wantMember Member
wantError bool
}{
// no URLs, just check ID & Name
{
body: []byte(`{"id": "c", "name": "dungarees"}`),
wantMember: Member{ID: "c", Name: "dungarees", PeerURLs: nil, ClientURLs: nil},
},
// both client and peer URLs
{
body: []byte(`{"peerURLs": ["http://127.0.0.1:4001"], "clientURLs": ["http://127.0.0.1:4001"]}`),
wantMember: Member{
PeerURLs: []string{
"http://127.0.0.1:4001",
},
ClientURLs: []string{
"http://127.0.0.1:4001",
},
},
},
// multiple peer URLs
{
body: []byte(`{"peerURLs": ["http://127.0.0.1:4001", "https://example.com"]}`),
wantMember: Member{
PeerURLs: []string{
"http://127.0.0.1:4001",
"https://example.com",
},
ClientURLs: nil,
},
},
// multiple client URLs
{
body: []byte(`{"clientURLs": ["http://127.0.0.1:4001", "https://example.com"]}`),
wantMember: Member{
PeerURLs: nil,
ClientURLs: []string{
"http://127.0.0.1:4001",
"https://example.com",
},
},
},
// invalid JSON
{
body: []byte(`{"peerU`),
wantError: true,
},
}
for i, tt := range tests {
got := Member{}
err := json.Unmarshal(tt.body, &got)
if tt.wantError != (err != nil) {
t.Errorf("#%d: want error %t, got %v", i, tt.wantError, err)
continue
}
if !reflect.DeepEqual(tt.wantMember, got) {
t.Errorf("#%d: incorrect output: want=%#v, got=%#v", i, tt.wantMember, got)
}
}
}
func TestMemberCollectionUnmarshal(t *testing.T) {
tests := []struct {
body []byte
want MemberCollection
}{
{
body: []byte(`{"members":[]}`),
want: MemberCollection([]Member{}),
},
{
body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
want: MemberCollection(
[]Member{
{
ID: "2745e2525fce8fe",
Name: "node3",
PeerURLs: []string{
"http://127.0.0.1:7003",
},
ClientURLs: []string{
"http://127.0.0.1:4003",
},
},
{
ID: "42134f434382925",
Name: "node1",
PeerURLs: []string{
"http://127.0.0.1:2380",
"http://127.0.0.1:7001",
},
ClientURLs: []string{
"http://127.0.0.1:2379",
"http://127.0.0.1:4001",
},
},
{
ID: "94088180e21eb87b",
Name: "node2",
PeerURLs: []string{
"http://127.0.0.1:7002",
},
ClientURLs: []string{
"http://127.0.0.1:4002",
},
},
},
),
},
}
for i, tt := range tests {
var got MemberCollection
err := json.Unmarshal(tt.body, &got)
if err != nil {
t.Errorf("#%d: unexpected error: %v", i, err)
continue
}
if !reflect.DeepEqual(tt.want, got) {
t.Errorf("#%d: incorrect output: want=%#v, got=%#v", i, tt.want, got)
}
}
}
func TestMemberCreateRequestUnmarshal(t *testing.T) {
body := []byte(`{"peerURLs": ["http://127.0.0.1:8081", "https://127.0.0.1:8080"]}`)
want := MemberCreateRequest{
PeerURLs: types.URLs([]url.URL{
url.URL{Scheme: "http", Host: "127.0.0.1:8081"},
url.URL{Scheme: "https", Host: "127.0.0.1:8080"},
}),
}
var req MemberCreateRequest
if err := json.Unmarshal(body, &req); err != nil {
t.Fatalf("Unmarshal returned unexpected err=%v", err)
}
if !reflect.DeepEqual(want, req) {
t.Fatalf("Failed to unmarshal MemberCreateRequest: want=%#v, got=%#v", want, req)
}
}
func TestMemberCreateRequestUnmarshalFail(t *testing.T) {
tests := [][]byte{
// invalid JSON
[]byte(``),
[]byte(`{`),
// spot-check validation done in types.NewURLs
[]byte(`{"peerURLs": "foo"}`),
[]byte(`{"peerURLs": ["."]}`),
[]byte(`{"peerURLs": []}`),
[]byte(`{"peerURLs": ["http://127.0.0.1:4001/foo"]}`),
[]byte(`{"peerURLs": ["http://127.0.0.1"]}`),
}
for i, tt := range tests {
var req MemberCreateRequest
if err := json.Unmarshal(tt, &req); err == nil {
t.Errorf("#%d: expected err, got nil", i)
}
}
}
func TestMemberCreateRequestMarshal(t *testing.T) {
req := MemberCreateRequest{
PeerURLs: types.URLs([]url.URL{
url.URL{Scheme: "http", Host: "127.0.0.1:8081"},
url.URL{Scheme: "https", Host: "127.0.0.1:8080"},
}),
}
want := []byte(`{"peerURLs":["http://127.0.0.1:8081","https://127.0.0.1:8080"]}`)
got, err := json.Marshal(&req)
if err != nil {
t.Fatalf("Marshal returned unexpected err=%v", err)
}
if !reflect.DeepEqual(want, got) {
t.Fatalf("Failed to marshal MemberCreateRequest: want=%s, got=%s", want, got)
}
}

123
etcdserver/etcdhttp/peer.go Normal file
View File

@ -0,0 +1,123 @@
/*
Copyright 2014 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 etcdhttp
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
"github.com/coreos/etcd/etcdserver"
"github.com/coreos/etcd/pkg/types"
"github.com/coreos/etcd/raft/raftpb"
)
const (
raftPrefix = "/raft"
peerMembersPrefix = "/members"
)
// NewPeerHandler generates an http.Handler to handle etcd peer (raft) requests.
func NewPeerHandler(server *etcdserver.EtcdServer) http.Handler {
rh := &raftHandler{
stats: server,
server: server,
clusterInfo: server.Cluster,
}
mh := &peerMembersHandler{
clusterInfo: server.Cluster,
}
mux := http.NewServeMux()
mux.HandleFunc("/", http.NotFound)
mux.Handle(raftPrefix, rh)
mux.Handle(peerMembersPrefix, mh)
return mux
}
type raftHandler struct {
stats etcdserver.Stats
server etcdserver.Server
clusterInfo etcdserver.ClusterInfo
}
func (h *raftHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "POST") {
return
}
wcid := h.clusterInfo.ID().String()
w.Header().Set("X-Etcd-Cluster-ID", wcid)
gcid := r.Header.Get("X-Etcd-Cluster-ID")
if gcid != wcid {
log.Printf("etcdhttp: request ignored due to cluster ID mismatch got %s want %s", gcid, wcid)
http.Error(w, "clusterID mismatch", http.StatusPreconditionFailed)
return
}
b, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Println("etcdhttp: error reading raft message:", err)
http.Error(w, "error reading raft message", http.StatusBadRequest)
return
}
var m raftpb.Message
if err := m.Unmarshal(b); err != nil {
log.Println("etcdhttp: error unmarshaling raft message:", err)
http.Error(w, "error unmarshaling raft message", http.StatusBadRequest)
return
}
if err := h.server.Process(context.TODO(), m); err != nil {
switch err {
case etcdserver.ErrRemoved:
log.Printf("etcdhttp: reject message from removed member %s", types.ID(m.From).String())
http.Error(w, "cannot process message from removed member", http.StatusForbidden)
default:
writeError(w, err)
}
return
}
if m.Type == raftpb.MsgApp {
h.stats.UpdateRecvApp(types.ID(m.From), r.ContentLength)
}
w.WriteHeader(http.StatusNoContent)
}
type peerMembersHandler struct {
clusterInfo etcdserver.ClusterInfo
}
func (h *peerMembersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET") {
return
}
w.Header().Set("X-Etcd-Cluster-ID", h.clusterInfo.ID().String())
if r.URL.Path != peerMembersPrefix {
http.Error(w, "bad path", http.StatusBadRequest)
return
}
ms := h.clusterInfo.Members()
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(ms); err != nil {
log.Printf("etcdhttp: %v", err)
}
}

View File

@ -0,0 +1,254 @@
/*
Copyright 2014 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 etcdhttp
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"path"
"strings"
"testing"
"github.com/coreos/etcd/etcdserver"
"github.com/coreos/etcd/raft/raftpb"
)
func mustMarshalMsg(t *testing.T, m raftpb.Message) []byte {
json, err := m.Marshal()
if err != nil {
t.Fatalf("error marshalling raft Message: %#v", err)
}
return json
}
// errReader implements io.Reader to facilitate a broken request.
type errReader struct{}
func (er *errReader) Read(_ []byte) (int, error) { return 0, errors.New("some error") }
func TestServeRaft(t *testing.T) {
testCases := []struct {
method string
body io.Reader
serverErr error
clusterID string
wcode int
}{
{
// bad method
"GET",
bytes.NewReader(
mustMarshalMsg(
t,
raftpb.Message{},
),
),
nil,
"0",
http.StatusMethodNotAllowed,
},
{
// bad method
"PUT",
bytes.NewReader(
mustMarshalMsg(
t,
raftpb.Message{},
),
),
nil,
"0",
http.StatusMethodNotAllowed,
},
{
// bad method
"DELETE",
bytes.NewReader(
mustMarshalMsg(
t,
raftpb.Message{},
),
),
nil,
"0",
http.StatusMethodNotAllowed,
},
{
// bad request body
"POST",
&errReader{},
nil,
"0",
http.StatusBadRequest,
},
{
// bad request protobuf
"POST",
strings.NewReader("malformed garbage"),
nil,
"0",
http.StatusBadRequest,
},
{
// good request, etcdserver.Server internal error
"POST",
bytes.NewReader(
mustMarshalMsg(
t,
raftpb.Message{},
),
),
errors.New("some error"),
"0",
http.StatusInternalServerError,
},
{
// good request from removed member
"POST",
bytes.NewReader(
mustMarshalMsg(
t,
raftpb.Message{},
),
),
etcdserver.ErrRemoved,
"0",
http.StatusForbidden,
},
{
// good request
"POST",
bytes.NewReader(
mustMarshalMsg(
t,
raftpb.Message{},
),
),
nil,
"1",
http.StatusPreconditionFailed,
},
{
// good request
"POST",
bytes.NewReader(
mustMarshalMsg(
t,
raftpb.Message{},
),
),
nil,
"0",
http.StatusNoContent,
},
}
for i, tt := range testCases {
req, err := http.NewRequest(tt.method, "foo", tt.body)
if err != nil {
t.Fatalf("#%d: could not create request: %#v", i, err)
}
req.Header.Set("X-Etcd-Cluster-ID", tt.clusterID)
rw := httptest.NewRecorder()
h := &raftHandler{stats: nil, server: &errServer{tt.serverErr}, clusterInfo: &fakeCluster{id: 0}}
h.ServeHTTP(rw, req)
if rw.Code != tt.wcode {
t.Errorf("#%d: got code=%d, want %d", i, rw.Code, tt.wcode)
}
}
}
func TestServeMembersFails(t *testing.T) {
tests := []struct {
method string
wcode int
}{
{
"POST",
http.StatusMethodNotAllowed,
},
{
"DELETE",
http.StatusMethodNotAllowed,
},
{
"BAD",
http.StatusMethodNotAllowed,
},
}
for i, tt := range tests {
rw := httptest.NewRecorder()
h := &peerMembersHandler{clusterInfo: nil}
h.ServeHTTP(rw, &http.Request{Method: tt.method})
if rw.Code != tt.wcode {
t.Errorf("#%d: code=%d, want %d", i, rw.Code, tt.wcode)
}
}
}
func TestServeMembersGet(t *testing.T) {
memb1 := etcdserver.Member{ID: 1, Attributes: etcdserver.Attributes{ClientURLs: []string{"http://localhost:8080"}}}
memb2 := etcdserver.Member{ID: 2, Attributes: etcdserver.Attributes{ClientURLs: []string{"http://localhost:8081"}}}
cluster := &fakeCluster{
id: 1,
members: map[uint64]*etcdserver.Member{1: &memb1, 2: &memb2},
}
h := &peerMembersHandler{clusterInfo: cluster}
msb, err := json.Marshal([]etcdserver.Member{memb1, memb2})
if err != nil {
t.Fatal(err)
}
wms := string(msb) + "\n"
tests := []struct {
path string
wcode int
wct string
wbody string
}{
{peerMembersPrefix, http.StatusOK, "application/json", wms},
{path.Join(peerMembersPrefix, "bad"), http.StatusBadRequest, "text/plain; charset=utf-8", "bad path\n"},
}
for i, tt := range tests {
req, err := http.NewRequest("GET", mustNewURL(t, tt.path).String(), nil)
if err != nil {
t.Fatal(err)
}
rw := httptest.NewRecorder()
h.ServeHTTP(rw, req)
if rw.Code != tt.wcode {
t.Errorf("#%d: code=%d, want %d", i, rw.Code, tt.wcode)
}
if gct := rw.Header().Get("Content-Type"); gct != tt.wct {
t.Errorf("#%d: content-type = %s, want %s", i, gct, tt.wct)
}
if rw.Body.String() != tt.wbody {
t.Errorf("#%d: body = %s, want %s", i, rw.Body.String(), tt.wbody)
}
gcid := rw.Header().Get("X-Etcd-Cluster-ID")
wcid := cluster.ID().String()
if gcid != wcid {
t.Errorf("#%d: cid = %s, want %s", i, gcid, wcid)
}
}
}

145
etcdserver/force_cluster.go Normal file
View File

@ -0,0 +1,145 @@
/*
Copyright 2014 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 etcdserver
import (
"encoding/json"
"log"
"sort"
"github.com/coreos/etcd/pkg/pbutil"
"github.com/coreos/etcd/pkg/types"
"github.com/coreos/etcd/raft"
"github.com/coreos/etcd/raft/raftpb"
"github.com/coreos/etcd/wal"
)
func restartAsStandaloneNode(cfg *ServerConfig, index uint64, snapshot *raftpb.Snapshot) (types.ID, raft.Node, *wal.WAL) {
w, id, cid, st, ents := readWAL(cfg.WALDir(), index)
cfg.Cluster.SetID(cid)
// discard the previously uncommitted entries
if len(ents) != 0 {
ents = ents[:st.Commit+1]
}
// force append the configuration change entries
toAppEnts := createConfigChangeEnts(getIDs(snapshot, ents), uint64(id), st.Term, st.Commit)
ents = append(ents, toAppEnts...)
// force commit newly appended entries
for _, e := range toAppEnts {
err := w.SaveEntry(&e)
if err != nil {
log.Fatalf("etcdserver: %v", err)
}
}
if len(ents) != 0 {
st.Commit = ents[len(ents)-1].Index
}
log.Printf("etcdserver: forcing restart of member %s in cluster %s at commit index %d", id, cfg.Cluster.ID(), st.Commit)
n := raft.RestartNode(uint64(id), 10, 1, snapshot, st, ents)
return id, n, w
}
// getIDs returns an ordered set of IDs included in the given snapshot and
// the entries. The given snapshot/entries can contain two kinds of
// ID-related entry:
// - ConfChangeAddNode, in which case the contained ID will be added into the set.
// - ConfChangeAddRemove, in which case the contained ID will be removed from the set.
func getIDs(snap *raftpb.Snapshot, ents []raftpb.Entry) []uint64 {
ids := make(map[uint64]bool)
if snap != nil {
for _, id := range snap.Nodes {
ids[id] = true
}
}
for _, e := range ents {
if e.Type != raftpb.EntryConfChange {
continue
}
var cc raftpb.ConfChange
pbutil.MustUnmarshal(&cc, e.Data)
switch cc.Type {
case raftpb.ConfChangeAddNode:
ids[cc.NodeID] = true
case raftpb.ConfChangeRemoveNode:
delete(ids, cc.NodeID)
default:
log.Panicf("ConfChange Type should be either ConfChangeAddNode or ConfChangeRemoveNode!")
}
}
sids := make(types.Uint64Slice, 0)
for id := range ids {
sids = append(sids, id)
}
sort.Sort(sids)
return []uint64(sids)
}
// createConfigChangeEnts creates a series of Raft entries (i.e.
// EntryConfChange) to remove the set of given IDs from the cluster. The ID
// `self` is _not_ removed, even if present in the set.
// If `self` is not inside the given ids, it creates a Raft entry to add a
// default member with the given `self`.
func createConfigChangeEnts(ids []uint64, self uint64, term, index uint64) []raftpb.Entry {
ents := make([]raftpb.Entry, 0)
next := index + 1
found := false
for _, id := range ids {
if id == self {
found = true
continue
}
cc := &raftpb.ConfChange{
Type: raftpb.ConfChangeRemoveNode,
NodeID: id,
}
e := raftpb.Entry{
Type: raftpb.EntryConfChange,
Data: pbutil.MustMarshal(cc),
Term: term,
Index: next,
}
ents = append(ents, e)
next++
}
if !found {
m := Member{
ID: types.ID(self),
RaftAttributes: RaftAttributes{PeerURLs: []string{"http://localhost:7001", "http://localhost:2380"}},
}
ctx, err := json.Marshal(m)
if err != nil {
log.Panicf("marshal member should never fail: %v", err)
}
cc := &raftpb.ConfChange{
Type: raftpb.ConfChangeAddNode,
NodeID: self,
Context: ctx,
}
e := raftpb.Entry{
Type: raftpb.EntryConfChange,
Data: pbutil.MustMarshal(cc),
Term: term,
Index: next,
}
ents = append(ents, e)
}
return ents
}

View File

@ -0,0 +1,136 @@
/*
Copyright 2014 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 etcdserver
import (
"encoding/json"
"reflect"
"testing"
"github.com/coreos/etcd/pkg/pbutil"
"github.com/coreos/etcd/pkg/types"
"github.com/coreos/etcd/raft/raftpb"
)
func TestGetIDs(t *testing.T) {
addcc := &raftpb.ConfChange{Type: raftpb.ConfChangeAddNode, NodeID: 2}
addEntry := raftpb.Entry{Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(addcc)}
removecc := &raftpb.ConfChange{Type: raftpb.ConfChangeRemoveNode, NodeID: 2}
removeEntry := raftpb.Entry{Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc)}
normalEntry := raftpb.Entry{Type: raftpb.EntryNormal}
tests := []struct {
snap *raftpb.Snapshot
ents []raftpb.Entry
widSet []uint64
}{
{nil, []raftpb.Entry{}, []uint64{}},
{&raftpb.Snapshot{Nodes: []uint64{1}}, []raftpb.Entry{}, []uint64{1}},
{&raftpb.Snapshot{Nodes: []uint64{1}}, []raftpb.Entry{addEntry}, []uint64{1, 2}},
{&raftpb.Snapshot{Nodes: []uint64{1}}, []raftpb.Entry{addEntry, removeEntry}, []uint64{1}},
{&raftpb.Snapshot{Nodes: []uint64{1}}, []raftpb.Entry{addEntry, normalEntry}, []uint64{1, 2}},
{&raftpb.Snapshot{Nodes: []uint64{1}}, []raftpb.Entry{addEntry, removeEntry, normalEntry}, []uint64{1}},
}
for i, tt := range tests {
idSet := getIDs(tt.snap, tt.ents)
if !reflect.DeepEqual(idSet, tt.widSet) {
t.Errorf("#%d: idset = %#v, want %#v", i, idSet, tt.widSet)
}
}
}
func TestCreateConfigChangeEnts(t *testing.T) {
m := Member{
ID: types.ID(1),
RaftAttributes: RaftAttributes{PeerURLs: []string{"http://localhost:7001", "http://localhost:2380"}},
}
ctx, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
addcc1 := &raftpb.ConfChange{Type: raftpb.ConfChangeAddNode, NodeID: 1, Context: ctx}
removecc2 := &raftpb.ConfChange{Type: raftpb.ConfChangeRemoveNode, NodeID: 2}
removecc3 := &raftpb.ConfChange{Type: raftpb.ConfChangeRemoveNode, NodeID: 3}
tests := []struct {
ids []uint64
self uint64
term, index uint64
wents []raftpb.Entry
}{
{
[]uint64{1},
1,
1, 1,
[]raftpb.Entry{},
},
{
[]uint64{1, 2},
1,
1, 1,
[]raftpb.Entry{{Term: 1, Index: 2, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc2)}},
},
{
[]uint64{1, 2},
1,
2, 2,
[]raftpb.Entry{{Term: 2, Index: 3, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc2)}},
},
{
[]uint64{1, 2, 3},
1,
2, 2,
[]raftpb.Entry{
{Term: 2, Index: 3, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc2)},
{Term: 2, Index: 4, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc3)},
},
},
{
[]uint64{2, 3},
2,
2, 2,
[]raftpb.Entry{
{Term: 2, Index: 3, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc3)},
},
},
{
[]uint64{2, 3},
1,
2, 2,
[]raftpb.Entry{
{Term: 2, Index: 3, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc2)},
{Term: 2, Index: 4, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc3)},
{Term: 2, Index: 5, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(addcc1)},
},
},
}
for i, tt := range tests {
gents := createConfigChangeEnts(tt.ids, tt.self, tt.term, tt.index)
if !reflect.DeepEqual(gents, tt.wents) {
t.Errorf("#%d: ents = %v, want %v", i, gents, tt.wents)
}
}
}

View File

@ -24,7 +24,6 @@ import (
"math/rand"
"path"
"sort"
"strconv"
"time"
"github.com/coreos/etcd/pkg/types"
@ -43,12 +42,12 @@ type Attributes struct {
}
type Member struct {
ID uint64 `json:"id"`
ID types.ID `json:"id"`
RaftAttributes
Attributes
}
// newMember creates a Member without an ID and generates one based on the
// NewMember creates a Member without an ID and generates one based on the
// name, peer URLs. This is used for bootstrapping/adding new member.
func NewMember(name string, peerURLs types.URLs, clusterName string, now *time.Time) *Member {
m := &Member{
@ -68,7 +67,7 @@ func NewMember(name string, peerURLs types.URLs, clusterName string, now *time.T
}
hash := sha1.Sum(b)
m.ID = binary.BigEndian.Uint64(hash[:8])
m.ID = types.ID(binary.BigEndian.Uint64(hash[:8]))
return m
}
@ -76,25 +75,50 @@ func NewMember(name string, peerURLs types.URLs, clusterName string, now *time.T
// It will panic if there is no PeerURLs available in Member.
func (m *Member) PickPeerURL() string {
if len(m.PeerURLs) == 0 {
panic("member should always have some peer url")
log.Panicf("member should always have some peer url")
}
return m.PeerURLs[rand.Intn(len(m.PeerURLs))]
}
func memberStoreKey(id uint64) string {
return path.Join(storeMembersPrefix, idAsHex(id))
func (m *Member) Clone() *Member {
if m == nil {
return nil
}
mm := &Member{
ID: m.ID,
Attributes: Attributes{
Name: m.Name,
},
}
if m.PeerURLs != nil {
mm.PeerURLs = make([]string, len(m.PeerURLs))
copy(mm.PeerURLs, m.PeerURLs)
}
if m.ClientURLs != nil {
mm.ClientURLs = make([]string, len(m.ClientURLs))
copy(mm.ClientURLs, m.ClientURLs)
}
return mm
}
func parseMemberID(key string) uint64 {
id, err := strconv.ParseUint(path.Base(key), 16, 64)
func memberStoreKey(id types.ID) string {
return path.Join(storeMembersPrefix, id.String())
}
func MemberAttributesStorePath(id types.ID) string {
return path.Join(memberStoreKey(id), attributesSuffix)
}
func mustParseMemberIDFromKey(key string) types.ID {
id, err := types.IDFromString(path.Base(key))
if err != nil {
log.Panicf("unexpected parse member id error: %v", err)
}
return id
}
func removedMemberStoreKey(id uint64) string {
return path.Join(storeRemovedMembersPrefix, idAsHex(id))
func removedMemberStoreKey(id types.ID) string {
return path.Join(storeRemovedMembersPrefix, id.String())
}
type SortableMemberSliceByPeerURLs []*Member

View File

@ -18,8 +18,11 @@ package etcdserver
import (
"net/url"
"reflect"
"testing"
"time"
"github.com/coreos/etcd/pkg/types"
)
func timeParse(value string) *time.Time {
@ -33,7 +36,7 @@ func timeParse(value string) *time.Time {
func TestMemberTime(t *testing.T) {
tests := []struct {
mem *Member
id uint64
id types.ID
}{
{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.8:2379"}}, "", nil), 14544069596553697298},
// Same ID, different name (names shouldn't matter)
@ -60,7 +63,7 @@ func TestMemberPick(t *testing.T) {
urls map[string]bool
}{
{
newTestMemberp(1, []string{"abc", "def", "ghi", "jkl", "mno", "pqr", "stu"}, "", nil),
newTestMember(1, []string{"abc", "def", "ghi", "jkl", "mno", "pqr", "stu"}, "", nil),
map[string]bool{
"abc": true,
"def": true,
@ -72,7 +75,7 @@ func TestMemberPick(t *testing.T) {
},
},
{
newTestMemberp(2, []string{"xyz"}, "", nil),
newTestMember(2, []string{"xyz"}, "", nil),
map[string]bool{"xyz": true},
},
}
@ -86,3 +89,29 @@ func TestMemberPick(t *testing.T) {
}
}
}
func TestMemberClone(t *testing.T) {
tests := []*Member{
newTestMember(1, nil, "abc", nil),
newTestMember(1, []string{"http://a"}, "abc", nil),
newTestMember(1, nil, "abc", []string{"http://b"}),
newTestMember(1, []string{"http://a"}, "abc", []string{"http://b"}),
}
for i, tt := range tests {
nm := tt.Clone()
if nm == tt {
t.Errorf("#%d: the pointers are the same, and clone doesn't happen", i)
}
if !reflect.DeepEqual(nm, tt) {
t.Errorf("#%d: member = %+v, want %+v", i, nm, tt)
}
}
}
func newTestMember(id uint64, peerURLs []string, name string, clientURLs []string) *Member {
return &Member{
ID: types.ID(id),
RaftAttributes: RaftAttributes{PeerURLs: peerURLs},
Attributes: Attributes{Name: name, ClientURLs: clientURLs},
}
}

View File

@ -21,101 +21,212 @@ import (
"fmt"
"log"
"net/http"
"strconv"
"net/url"
"path"
"sync"
"time"
"github.com/coreos/etcd/etcdserver/stats"
"github.com/coreos/etcd/pkg/types"
"github.com/coreos/etcd/raft/raftpb"
)
const raftPrefix = "/raft"
const (
raftPrefix = "/raft"
connPerSender = 4
senderBufSize = connPerSender * 4
)
// Sender creates the default production sender used to transport raft messages
// in the cluster. The returned sender will update the given ServerStats and
// LeaderStats appropriately.
func Sender(t *http.Transport, cl *Cluster, ss *stats.ServerStats, ls *stats.LeaderStats) func(msgs []raftpb.Message) {
c := &http.Client{Transport: t}
return func(msgs []raftpb.Message) {
for _, m := range msgs {
// TODO: reuse go routines
// limit the number of outgoing connections for the same receiver
go send(c, cl, m, ss, ls)
}
}
type sendHub struct {
tr http.RoundTripper
cl ClusterInfo
ss *stats.ServerStats
ls *stats.LeaderStats
senders map[types.ID]*sender
shouldstop chan struct{}
}
// send uses the given client to send a message to a member in the given
// ClusterStore, retrying up to 3 times for each message. The given
// ServerStats and LeaderStats are updated appropriately
func send(c *http.Client, cl *Cluster, m raftpb.Message, ss *stats.ServerStats, ls *stats.LeaderStats) {
cid := cl.ID()
// TODO (xiangli): reasonable retry logic
for i := 0; i < 3; i++ {
memb := cl.Member(m.To)
if memb == nil {
// TODO: unknown peer id.. what do we do? I
// don't think his should ever happen, need to
// look into this further.
log.Printf("etcdhttp: no member for %d", m.To)
return
// newSendHub creates the default send hub used to transport raft messages
// to other members. The returned sendHub will update the given ServerStats and
// LeaderStats appropriately.
func newSendHub(t http.RoundTripper, cl ClusterInfo, ss *stats.ServerStats, ls *stats.LeaderStats) *sendHub {
h := &sendHub{
tr: t,
cl: cl,
ss: ss,
ls: ls,
senders: make(map[types.ID]*sender),
shouldstop: make(chan struct{}, 1),
}
for _, m := range cl.Members() {
h.Add(m)
}
return h
}
func (h *sendHub) Send(msgs []raftpb.Message) {
for _, m := range msgs {
to := types.ID(m.To)
s, ok := h.senders[to]
if !ok {
if !h.cl.IsIDRemoved(to) {
log.Printf("etcdserver: send message to unknown receiver %s", to)
}
continue
}
u := fmt.Sprintf("%s%s", memb.PickPeerURL(), raftPrefix)
// TODO: don't block. we should be able to have 1000s
// of messages out at a time.
data, err := m.Marshal()
if err != nil {
log.Println("etcdhttp: dropping message:", err)
log.Println("sender: dropping message:", err)
return // drop bad message
}
if m.Type == raftpb.MsgApp {
ss.SendAppendReq(len(data))
h.ss.SendAppendReq(len(data))
}
to := idAsHex(m.To)
fs := ls.Follower(to)
start := time.Now()
sent := httpPost(c, u, cid, data)
end := time.Now()
if sent {
fs.Succ(end.Sub(start))
return
}
fs.Fail()
// TODO: backoff
// TODO (xiangli): reasonable retry logic
s.send(data)
}
}
// httpPost POSTs a data payload to a url using the given client. Returns true
// if the POST succeeds, false on any failure.
func httpPost(c *http.Client, url string, cid uint64, data []byte) bool {
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
func (h *sendHub) Stop() {
for _, s := range h.senders {
s.stop()
}
}
func (h *sendHub) ShouldStopNotify() <-chan struct{} {
return h.shouldstop
}
func (h *sendHub) Add(m *Member) {
if _, ok := h.senders[m.ID]; ok {
return
}
// TODO: considering how to switch between all available peer urls
u := fmt.Sprintf("%s%s", m.PickPeerURL(), raftPrefix)
fs := h.ls.Follower(m.ID.String())
s := newSender(h.tr, u, h.cl.ID(), fs, h.shouldstop)
h.senders[m.ID] = s
}
func (h *sendHub) Remove(id types.ID) {
h.senders[id].stop()
delete(h.senders, id)
}
func (h *sendHub) Update(m *Member) {
// TODO: return error or just panic?
if _, ok := h.senders[m.ID]; !ok {
return
}
peerURL := m.PickPeerURL()
u, err := url.Parse(peerURL)
if err != nil {
// TODO: log the error?
return false
log.Panicf("unexpect peer url %s", peerURL)
}
u.Path = path.Join(u.Path, raftPrefix)
s := h.senders[m.ID]
s.mu.Lock()
defer s.mu.Unlock()
s.u = u.String()
}
type sender struct {
tr http.RoundTripper
u string
cid types.ID
fs *stats.FollowerStats
q chan []byte
mu sync.RWMutex
wg sync.WaitGroup
shouldstop chan struct{}
}
func newSender(tr http.RoundTripper, u string, cid types.ID, fs *stats.FollowerStats, shouldstop chan struct{}) *sender {
s := &sender{
tr: tr,
u: u,
cid: cid,
fs: fs,
q: make(chan []byte, senderBufSize),
shouldstop: shouldstop,
}
s.wg.Add(connPerSender)
for i := 0; i < connPerSender; i++ {
go s.handle()
}
return s
}
func (s *sender) send(data []byte) error {
select {
case s.q <- data:
return nil
default:
log.Printf("sender: reach the maximal serving to %s", s.u)
return fmt.Errorf("reach maximal serving")
}
}
func (s *sender) stop() {
close(s.q)
s.wg.Wait()
}
func (s *sender) handle() {
defer s.wg.Done()
for d := range s.q {
start := time.Now()
err := s.post(d)
end := time.Now()
if err != nil {
s.fs.Fail()
log.Printf("sender: %v", err)
continue
}
s.fs.Succ(end.Sub(start))
}
}
// post POSTs a data payload to a url. Returns nil if the POST succeeds,
// error on any failure.
func (s *sender) post(data []byte) error {
s.mu.RLock()
req, err := http.NewRequest("POST", s.u, bytes.NewBuffer(data))
s.mu.RUnlock()
if err != nil {
return fmt.Errorf("new request to %s error: %v", s.u, err)
}
req.Header.Set("Content-Type", "application/protobuf")
req.Header.Set("X-Etcd-Cluster-ID", strconv.FormatUint(cid, 16))
resp, err := c.Do(req)
req.Header.Set("X-Etcd-Cluster-ID", s.cid.String())
resp, err := s.tr.RoundTrip(req)
if err != nil {
// TODO: log the error?
return false
return fmt.Errorf("error posting to %q: %v", req.URL.String(), err)
}
resp.Body.Close()
switch resp.StatusCode {
case http.StatusPreconditionFailed:
// TODO: shutdown the etcdserver gracefully?
log.Panicf("clusterID mismatch")
return false
select {
case s.shouldstop <- struct{}{}:
default:
}
log.Printf("etcdserver: conflicting cluster ID with the target cluster (%s != %s)", resp.Header.Get("X-Etcd-Cluster-ID"), s.cid)
return nil
case http.StatusForbidden:
// TODO: stop the server
log.Panicf("the member has been removed")
return false
select {
case s.shouldstop <- struct{}{}:
default:
}
log.Println("etcdserver: this member has been permanently removed from the cluster")
log.Println("etcdserver: the data-dir used by this member must be removed so that this host can be re-added with a new member ID")
return nil
case http.StatusNoContent:
return true
return nil
default:
return false
return fmt.Errorf("unhandled status %s", http.StatusText(resp.StatusCode))
}
}

312
etcdserver/sender_test.go Normal file
View File

@ -0,0 +1,312 @@
/*
Copyright 2014 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 etcdserver
import (
"errors"
"io/ioutil"
"net/http"
"sync"
"testing"
"time"
"github.com/coreos/etcd/etcdserver/stats"
"github.com/coreos/etcd/pkg/testutil"
"github.com/coreos/etcd/pkg/types"
)
func TestSendHubInitSenders(t *testing.T) {
membs := []*Member{
newTestMember(1, []string{"http://a"}, "", nil),
newTestMember(2, []string{"http://b"}, "", nil),
newTestMember(3, []string{"http://c"}, "", nil),
}
cl := newTestCluster(membs)
ls := stats.NewLeaderStats("")
h := newSendHub(nil, cl, nil, ls)
ids := cl.MemberIDs()
if len(h.senders) != len(ids) {
t.Errorf("len(ids) = %d, want %d", len(h.senders), len(ids))
}
for _, id := range ids {
if _, ok := h.senders[id]; !ok {
t.Errorf("senders[%s] is nil, want exists", id)
}
}
}
func TestSendHubAdd(t *testing.T) {
cl := newTestCluster(nil)
ls := stats.NewLeaderStats("")
h := newSendHub(nil, cl, nil, ls)
m := newTestMember(1, []string{"http://a"}, "", nil)
h.Add(m)
if _, ok := ls.Followers["1"]; !ok {
t.Errorf("FollowerStats[1] is nil, want exists")
}
s, ok := h.senders[types.ID(1)]
if !ok {
t.Fatalf("senders[1] is nil, want exists")
}
if s.u != "http://a/raft" {
t.Errorf("url = %s, want %s", s.u, "http://a/raft")
}
h.Add(m)
ns := h.senders[types.ID(1)]
if s != ns {
t.Errorf("sender = %p, want %p", ns, s)
}
}
func TestSendHubRemove(t *testing.T) {
membs := []*Member{
newTestMember(1, []string{"http://a"}, "", nil),
}
cl := newTestCluster(membs)
ls := stats.NewLeaderStats("")
h := newSendHub(nil, cl, nil, ls)
h.Remove(types.ID(1))
if _, ok := h.senders[types.ID(1)]; ok {
t.Fatalf("senders[1] exists, want removed")
}
}
func TestSendHubShouldStop(t *testing.T) {
membs := []*Member{
newTestMember(1, []string{"http://a"}, "", nil),
}
tr := newRespRoundTripper(http.StatusForbidden, nil)
cl := newTestCluster(membs)
ls := stats.NewLeaderStats("")
h := newSendHub(tr, cl, nil, ls)
shouldstop := h.ShouldStopNotify()
select {
case <-shouldstop:
t.Fatalf("received unexpected shouldstop notification")
case <-time.After(10 * time.Millisecond):
}
h.senders[1].send([]byte("somedata"))
testutil.ForceGosched()
select {
case <-shouldstop:
default:
t.Fatalf("cannot receive stop notification")
}
}
// TestSenderSend tests that send func could post data using roundtripper
// and increase success count in stats.
func TestSenderSend(t *testing.T) {
tr := &roundTripperRecorder{}
fs := &stats.FollowerStats{}
s := newSender(tr, "http://10.0.0.1", types.ID(1), fs, nil)
if err := s.send([]byte("some data")); err != nil {
t.Fatalf("unexpect send error: %v", err)
}
s.stop()
if tr.Request() == nil {
t.Errorf("sender fails to post the data")
}
fs.Lock()
defer fs.Unlock()
if fs.Counts.Success != 1 {
t.Errorf("success = %d, want 1", fs.Counts.Success)
}
}
func TestSenderExceedMaximalServing(t *testing.T) {
tr := newRoundTripperBlocker()
fs := &stats.FollowerStats{}
s := newSender(tr, "http://10.0.0.1", types.ID(1), fs, nil)
// keep the sender busy and make the buffer full
// nothing can go out as we block the sender
for i := 0; i < connPerSender+senderBufSize; i++ {
if err := s.send([]byte("some data")); err != nil {
t.Errorf("send err = %v, want nil", err)
}
// force the sender to grab data
testutil.ForceGosched()
}
// try to send a data when we are sure the buffer is full
if err := s.send([]byte("some data")); err == nil {
t.Errorf("unexpect send success")
}
// unblock the senders and force them to send out the data
tr.unblock()
testutil.ForceGosched()
// It could send new data after previous ones succeed
if err := s.send([]byte("some data")); err != nil {
t.Errorf("send err = %v, want nil", err)
}
s.stop()
}
// TestSenderSendFailed tests that when send func meets the post error,
// it increases fail count in stats.
func TestSenderSendFailed(t *testing.T) {
fs := &stats.FollowerStats{}
s := newSender(newRespRoundTripper(0, errors.New("blah")), "http://10.0.0.1", types.ID(1), fs, nil)
if err := s.send([]byte("some data")); err != nil {
t.Fatalf("unexpect send error: %v", err)
}
s.stop()
fs.Lock()
defer fs.Unlock()
if fs.Counts.Fail != 1 {
t.Errorf("fail = %d, want 1", fs.Counts.Fail)
}
}
func TestSenderPost(t *testing.T) {
tr := &roundTripperRecorder{}
s := newSender(tr, "http://10.0.0.1", types.ID(1), nil, nil)
if err := s.post([]byte("some data")); err != nil {
t.Fatalf("unexpect post error: %v", err)
}
s.stop()
if g := tr.Request().Method; g != "POST" {
t.Errorf("method = %s, want %s", g, "POST")
}
if g := tr.Request().URL.String(); g != "http://10.0.0.1" {
t.Errorf("url = %s, want %s", g, "http://10.0.0.1")
}
if g := tr.Request().Header.Get("Content-Type"); g != "application/protobuf" {
t.Errorf("content type = %s, want %s", g, "application/protobuf")
}
if g := tr.Request().Header.Get("X-Etcd-Cluster-ID"); g != "1" {
t.Errorf("cluster id = %s, want %s", g, "1")
}
b, err := ioutil.ReadAll(tr.Request().Body)
if err != nil {
t.Fatalf("unexpected ReadAll error: %v", err)
}
if string(b) != "some data" {
t.Errorf("body = %s, want %s", b, "some data")
}
}
func TestSenderPostBad(t *testing.T) {
tests := []struct {
u string
code int
err error
}{
// bad url
{":bad url", http.StatusNoContent, nil},
// RoundTrip returns error
{"http://10.0.0.1", 0, errors.New("blah")},
// unexpected response status code
{"http://10.0.0.1", http.StatusOK, nil},
{"http://10.0.0.1", http.StatusCreated, nil},
}
for i, tt := range tests {
shouldstop := make(chan struct{})
s := newSender(newRespRoundTripper(tt.code, tt.err), tt.u, types.ID(1), nil, shouldstop)
err := s.post([]byte("some data"))
s.stop()
if err == nil {
t.Errorf("#%d: err = nil, want not nil", i)
}
}
}
func TestSenderPostShouldStop(t *testing.T) {
tests := []struct {
u string
code int
err error
}{
{"http://10.0.0.1", http.StatusForbidden, nil},
{"http://10.0.0.1", http.StatusPreconditionFailed, nil},
}
for i, tt := range tests {
shouldstop := make(chan struct{}, 1)
s := newSender(newRespRoundTripper(tt.code, tt.err), tt.u, types.ID(1), nil, shouldstop)
s.post([]byte("some data"))
s.stop()
select {
case <-shouldstop:
default:
t.Fatalf("#%d: cannot receive shouldstop notification", i)
}
}
}
type roundTripperBlocker struct {
c chan struct{}
}
func newRoundTripperBlocker() *roundTripperBlocker {
return &roundTripperBlocker{c: make(chan struct{})}
}
func (t *roundTripperBlocker) RoundTrip(req *http.Request) (*http.Response, error) {
<-t.c
return &http.Response{StatusCode: http.StatusNoContent, Body: &nopReadCloser{}}, nil
}
func (t *roundTripperBlocker) unblock() {
close(t.c)
}
type respRoundTripper struct {
code int
err error
}
func newRespRoundTripper(code int, err error) *respRoundTripper {
return &respRoundTripper{code: code, err: err}
}
func (t *respRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return &http.Response{StatusCode: t.code, Body: &nopReadCloser{}}, t.err
}
type roundTripperRecorder struct {
req *http.Request
sync.Mutex
}
func (t *roundTripperRecorder) RoundTrip(req *http.Request) (*http.Response, error) {
t.Lock()
defer t.Unlock()
t.req = req
return &http.Response{StatusCode: http.StatusNoContent, Body: &nopReadCloser{}}, nil
}
func (t *roundTripperRecorder) Request() *http.Request {
t.Lock()
defer t.Unlock()
return t.req
}
type nopReadCloser struct{}
func (n *nopReadCloser) Read(p []byte) (int, error) { return 0, nil }
func (n *nopReadCloser) Close() error { return nil }

View File

@ -27,7 +27,7 @@ import (
"os"
"path"
"regexp"
"strconv"
"sort"
"sync/atomic"
"time"
@ -36,11 +36,12 @@ import (
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
"github.com/coreos/etcd/etcdserver/stats"
"github.com/coreos/etcd/pkg/pbutil"
"github.com/coreos/etcd/pkg/types"
"github.com/coreos/etcd/pkg/wait"
"github.com/coreos/etcd/raft"
"github.com/coreos/etcd/raft/raftpb"
"github.com/coreos/etcd/snap"
"github.com/coreos/etcd/store"
"github.com/coreos/etcd/wait"
"github.com/coreos/etcd/wal"
)
@ -64,6 +65,9 @@ var (
ErrIDRemoved = errors.New("etcdserver: ID removed")
ErrIDExists = errors.New("etcdserver: ID exists")
ErrIDNotFound = errors.New("etcdserver: ID not found")
ErrPeerURLexists = errors.New("etcdserver: peerURL exists")
ErrCanceled = errors.New("etcdserver: request cancelled")
ErrTimeout = errors.New("etcdserver: request timed out")
storeMembersPrefix = path.Join(StoreAdminPrefix, "members")
storeRemovedMembersPrefix = path.Join(StoreAdminPrefix, "removed_members")
@ -75,20 +79,27 @@ func init() {
rand.Seed(time.Now().UnixNano())
}
type sendFunc func(m []raftpb.Message)
type Response struct {
Event *store.Event
Watcher store.Watcher
err error
}
type Sender interface {
Send(m []raftpb.Message)
Add(m *Member)
Remove(id types.ID)
Update(m *Member)
Stop()
ShouldStopNotify() <-chan struct{}
}
type Storage interface {
// Save function saves ents and state to the underlying stable storage.
// Save MUST block until st and ents are on stable storage.
Save(st raftpb.HardState, ents []raftpb.Entry)
Save(st raftpb.HardState, ents []raftpb.Entry) error
// SaveSnap function saves snapshot to the underlying stable storage.
SaveSnap(snap raftpb.Snapshot)
SaveSnap(snap raftpb.Snapshot) error
// TODO: WAL should be able to control cut itself. After implement self-controled cut,
// remove it in this interface.
@ -105,7 +116,9 @@ type Server interface {
// Stop terminates the Server and performs any necessary finalization.
// Do and Process cannot be called after Stop has been invoked.
Stop()
// Do takes a request and attempts to fulfil it, returning a Response.
// ID returns the ID of the Server.
ID() types.ID
// Do takes a request and attempts to fulfill it, returning a Response.
Do(ctx context.Context, r pb.Request) (Response, error)
// Process takes a raft message and applies it to the server's raft state
// machine, respecting any timeout of the given context.
@ -118,6 +131,10 @@ type Server interface {
// return ErrIDRemoved if member ID is removed from the cluster, or return
// ErrIDNotFound if member ID is not in the cluster.
RemoveMember(ctx context.Context, id uint64) error
// UpdateMember attempts to update a existing member in the cluster. It will
// return ErrIDNotFound if the member ID does not exist.
UpdateMember(ctx context.Context, updateMemb Member) error
}
type Stats interface {
@ -129,7 +146,7 @@ type Stats interface {
// StoreStats returns statistics of the store backing this EtcdServer
StoreStats() []byte
// UpdateRecvApp updates the underlying statistics in response to a receiving an Append request
UpdateRecvApp(from uint64, length int64)
UpdateRecvApp(from types.ID, length int64)
}
type RaftTimer interface {
@ -141,8 +158,8 @@ type RaftTimer interface {
type EtcdServer struct {
w wait.Wait
done chan struct{}
stopped chan struct{}
id uint64
stop chan struct{}
id types.ID
attributes Attributes
Cluster *Cluster
@ -153,11 +170,11 @@ type EtcdServer struct {
stats *stats.ServerStats
lstats *stats.LeaderStats
// send specifies the send function for sending msgs to members. send
// sender specifies the sender to send msgs to members. sending msgs
// MUST NOT block. It is okay to drop messages, since clients should
// timeout and reissue their messages. If send is nil, server will
// panic.
send sendFunc
sender Sender
storage Storage
@ -173,47 +190,49 @@ type EtcdServer struct {
// NewServer creates a new EtcdServer from the supplied configuration. The
// configuration is considered static for the lifetime of the EtcdServer.
func NewServer(cfg *ServerConfig) *EtcdServer {
func NewServer(cfg *ServerConfig) (*EtcdServer, error) {
if err := os.MkdirAll(cfg.SnapDir(), privateDirMode); err != nil {
log.Fatalf("etcdserver: cannot create snapshot directory: %v", err)
return nil, fmt.Errorf("cannot create snapshot directory: %v", err)
}
ss := snap.New(cfg.SnapDir())
st := store.New()
var w *wal.WAL
var n raft.Node
var id uint64
var id types.ID
haveWAL := wal.Exist(cfg.WALDir())
switch {
case !haveWAL && cfg.ClusterState == ClusterStateValueExisting:
cl, err := GetClusterFromPeers(cfg.Cluster.PeerURLs())
case !haveWAL && !cfg.NewCluster:
us := getOtherPeerURLs(cfg.Cluster, cfg.Name)
cl, err := GetClusterFromPeers(us)
if err != nil {
log.Fatal(err)
return nil, fmt.Errorf("cannot fetch cluster info from peer urls: %v", err)
}
if err := cfg.Cluster.ValidateAndAssignIDs(cl.Members()); err != nil {
log.Fatalf("etcdserver: %v", err)
return nil, fmt.Errorf("error validating IDs from cluster %s: %v", cl, err)
}
cfg.Cluster.SetID(cl.id)
cfg.Cluster.SetStore(st)
cfg.Print()
id, n, w = startNode(cfg, nil)
case !haveWAL && cfg.ClusterState == ClusterStateValueNew:
case !haveWAL && cfg.NewCluster:
if err := cfg.VerifyBootstrapConfig(); err != nil {
log.Fatalf("etcdserver: %v", err)
return nil, err
}
if err := checkClientURLsEmptyFromPeers(cfg.Cluster, cfg.Name); err != nil {
return nil, err
}
m := cfg.Cluster.MemberByName(cfg.Name)
if cfg.ShouldDiscover() {
d, err := discovery.New(cfg.DiscoveryURL, m.ID, cfg.Cluster.String())
s, err := discovery.JoinCluster(cfg.DiscoveryURL, cfg.DiscoveryProxy, m.ID, cfg.Cluster.String())
if err != nil {
log.Fatalf("etcdserver: cannot init discovery %v", err)
return nil, err
}
s, err := d.Discover()
if err != nil {
log.Fatalf("etcdserver: %v", err)
}
if cfg.Cluster, err = NewClusterFromString(cfg.Cluster.name, s); err != nil {
log.Fatalf("etcdserver: %v", err)
if cfg.Cluster, err = NewClusterFromString(cfg.Cluster.token, s); err != nil {
return nil, err
}
}
cfg.Cluster.SetStore(st)
cfg.PrintWithInitial()
id, n, w = startNode(cfg, cfg.Cluster.MemberIDs())
case haveWAL:
if cfg.ShouldDiscover() {
@ -222,25 +241,34 @@ func NewServer(cfg *ServerConfig) *EtcdServer {
var index uint64
snapshot, err := ss.Load()
if err != nil && err != snap.ErrNoSnapshot {
log.Fatal(err)
return nil, err
}
if snapshot != nil {
log.Printf("etcdserver: recovering from snapshot at index %d", snapshot.Index)
st.Recovery(snapshot.Data)
index = snapshot.Index
}
cfg.Cluster = NewClusterFromStore(cfg.Cluster.name, st)
id, n, w = restartNode(cfg, index, snapshot)
cfg.Cluster = NewClusterFromStore(cfg.Cluster.token, st)
cfg.Print()
if snapshot != nil {
log.Printf("etcdserver: loaded peers from snapshot: %s", cfg.Cluster)
}
if !cfg.ForceNewCluster {
id, n, w = restartNode(cfg, index, snapshot)
} else {
id, n, w = restartAsStandaloneNode(cfg, index, snapshot)
}
default:
log.Fatalf("etcdserver: unsupported bootstrap config")
return nil, fmt.Errorf("unsupported bootstrap config")
}
sstats := &stats.ServerStats{
Name: cfg.Name,
ID: idAsHex(id),
ID: id.String(),
}
lstats := stats.NewLeaderStats(idAsHex(id))
lstats := stats.NewLeaderStats(id.String())
shub := newSendHub(cfg.Transport, cfg.Cluster, sstats, lstats)
s := &EtcdServer{
store: st,
node: n,
@ -253,12 +281,12 @@ func NewServer(cfg *ServerConfig) *EtcdServer {
}{w, ss},
stats: sstats,
lstats: lstats,
send: Sender(cfg.Transport, cfg.Cluster, sstats, lstats),
sender: shub,
Ticker: time.Tick(100 * time.Millisecond),
SyncTicker: time.Tick(500 * time.Millisecond),
snapCount: cfg.SnapCount,
}
return s
return s, nil
}
// Start prepares and starts server in a new goroutine. It is no longer safe to
@ -279,15 +307,17 @@ func (s *EtcdServer) start() {
}
s.w = wait.New()
s.done = make(chan struct{})
s.stopped = make(chan struct{})
s.stop = make(chan struct{})
s.stats.Initialize()
// TODO: if this is an empty log, writes all peer infos
// into the first entry
go s.run()
}
func (s *EtcdServer) ID() types.ID { return s.id }
func (s *EtcdServer) Process(ctx context.Context, m raftpb.Message) error {
if s.Cluster.IsIDRemoved(m.From) {
if s.Cluster.IsIDRemoved(types.ID(m.From)) {
return ErrRemoved
}
return s.node.Step(ctx, m)
@ -298,6 +328,14 @@ func (s *EtcdServer) run() {
// snapi indicates the index of the last submitted snapshot request
var snapi, appliedi uint64
var nodes []uint64
var shouldstop bool
shouldstopC := s.sender.ShouldStopNotify()
defer func() {
s.node.Stop()
s.sender.Stop()
close(s.done)
}()
for {
select {
case <-s.Ticker:
@ -312,38 +350,56 @@ func (s *EtcdServer) run() {
}
}
s.storage.Save(rd.HardState, rd.Entries)
s.storage.SaveSnap(rd.Snapshot)
s.send(rd.Messages)
// TODO(bmizerany): do this in the background, but take
// care to apply entries in a single goroutine, and not
// race them.
// TODO: apply configuration change into ClusterStore.
if len(rd.CommittedEntries) != 0 {
appliedi = s.apply(rd.CommittedEntries, nodes)
if err := s.storage.Save(rd.HardState, rd.Entries); err != nil {
log.Fatalf("etcdserver: save state and entries error: %v", err)
}
if rd.Snapshot.Index > snapi {
snapi = rd.Snapshot.Index
if err := s.storage.SaveSnap(rd.Snapshot); err != nil {
log.Fatalf("etcdserver: create snapshot error: %v", err)
}
s.sender.Send(rd.Messages)
// recover from snapshot if it is more updated than current applied
if rd.Snapshot.Index > appliedi {
if err := s.store.Recovery(rd.Snapshot.Data); err != nil {
panic("TODO: this is bad, what do we do about it?")
log.Panicf("recovery store error: %v", err)
}
s.Cluster.Recover()
appliedi = rd.Snapshot.Index
}
// TODO(bmizerany): do this in the background, but take
// care to apply entries in a single goroutine, and not
// race them.
if len(rd.CommittedEntries) != 0 {
firsti := rd.CommittedEntries[0].Index
if appliedi == 0 {
appliedi = firsti - 1
}
if firsti > appliedi+1 {
log.Panicf("etcdserver: first index of committed entry[%d] should <= appliedi[%d] + 1", firsti, appliedi)
}
var ents []raftpb.Entry
if appliedi+1-firsti < uint64(len(rd.CommittedEntries)) {
ents = rd.CommittedEntries[appliedi+1-firsti:]
}
if appliedi, shouldstop = s.apply(ents); shouldstop {
return
}
}
s.node.Advance()
if rd.Snapshot.Index > snapi {
snapi = rd.Snapshot.Index
}
if appliedi-snapi > s.snapCount {
s.snapshot(appliedi, nodes)
snapi = appliedi
}
case <-syncC:
s.sync(defaultSyncTimeout)
case <-s.done:
close(s.stopped)
case <-shouldstopC:
return
case <-s.stop:
return
}
}
@ -352,11 +408,18 @@ func (s *EtcdServer) run() {
// Stop stops the server gracefully, and shuts down the running goroutine.
// Stop should be called after a Start(s), otherwise it will block forever.
func (s *EtcdServer) Stop() {
s.node.Stop()
close(s.done)
<-s.stopped
select {
case s.stop <- struct{}{}:
case <-s.done:
return
}
<-s.done
}
// StopNotify returns a channel that receives a empty struct
// when the server is stopped.
func (s *EtcdServer) StopNotify() <-chan struct{} { return s.done }
// Do interprets r and performs an operation on s.store according to r.Method
// and other fields. If r.Method is "POST", "PUT", "DELETE", or a "GET" with
// Quorum == true, r will be sent through consensus before performing its
@ -364,7 +427,7 @@ func (s *EtcdServer) Stop() {
// an error.
func (s *EtcdServer) Do(ctx context.Context, r pb.Request) (Response, error) {
if r.ID == 0 {
panic("r.ID cannot be 0")
log.Panicf("request ID should never be 0")
}
if r.Method == "GET" && r.Quorum {
r.Method = "QGET"
@ -383,7 +446,7 @@ func (s *EtcdServer) Do(ctx context.Context, r pb.Request) (Response, error) {
return resp, resp.err
case <-ctx.Done():
s.w.Trigger(r.ID, nil) // GC wait
return Response{}, ctx.Err()
return Response{}, parseCtxErr(ctx.Err())
case <-s.done:
return Response{}, ErrStopped
}
@ -402,26 +465,28 @@ func (s *EtcdServer) Do(ctx context.Context, r pb.Request) (Response, error) {
}
return Response{Event: ev}, nil
}
case "HEAD":
ev, err := s.store.Get(r.Path, r.Recursive, r.Sorted)
if err != nil {
return Response{}, err
}
return Response{Event: ev}, nil
default:
return Response{}, ErrUnknownMethod
}
}
func (s *EtcdServer) SelfStats() []byte {
return s.stats.JSON()
}
func (s *EtcdServer) SelfStats() []byte { return s.stats.JSON() }
func (s *EtcdServer) LeaderStats() []byte {
// TODO(jonboulle): need to lock access to lstats, set it to nil when not leader, ...
return s.lstats.JSON()
}
func (s *EtcdServer) StoreStats() []byte {
return s.store.JsonStats()
}
func (s *EtcdServer) StoreStats() []byte { return s.store.JsonStats() }
func (s *EtcdServer) UpdateRecvApp(from uint64, length int64) {
s.stats.RecvAppendReq(idAsHex(from), int(length))
func (s *EtcdServer) UpdateRecvApp(from types.ID, length int64) {
s.stats.RecvAppendReq(from.String(), int(length))
}
func (s *EtcdServer) AddMember(ctx context.Context, memb Member) error {
@ -433,7 +498,7 @@ func (s *EtcdServer) AddMember(ctx context.Context, memb Member) error {
cc := raftpb.ConfChange{
ID: GenID(),
Type: raftpb.ConfChangeAddNode,
NodeID: memb.ID,
NodeID: uint64(memb.ID),
Context: b,
}
return s.configure(ctx, cc)
@ -448,21 +513,31 @@ func (s *EtcdServer) RemoveMember(ctx context.Context, id uint64) error {
return s.configure(ctx, cc)
}
func (s *EtcdServer) UpdateMember(ctx context.Context, memb Member) error {
b, err := json.Marshal(memb)
if err != nil {
return err
}
cc := raftpb.ConfChange{
ID: GenID(),
Type: raftpb.ConfChangeUpdateNode,
NodeID: uint64(memb.ID),
Context: b,
}
return s.configure(ctx, cc)
}
// Implement the RaftTimer interface
func (s *EtcdServer) Index() uint64 {
return atomic.LoadUint64(&s.raftIndex)
}
func (s *EtcdServer) Index() uint64 { return atomic.LoadUint64(&s.raftIndex) }
func (s *EtcdServer) Term() uint64 {
return atomic.LoadUint64(&s.raftTerm)
}
func (s *EtcdServer) Term() uint64 { return atomic.LoadUint64(&s.raftTerm) }
// configure sends configuration change through consensus then performs it.
// It will block until the change is performed or there is an error.
// configure sends a configuration change through consensus and
// then waits for it to be applied to the server. It
// will block until the change is performed or there is an error.
func (s *EtcdServer) configure(ctx context.Context, cc raftpb.ConfChange) error {
ch := s.w.Register(cc.ID)
if err := s.node.ProposeConfChange(ctx, cc); err != nil {
log.Printf("configure error: %v", err)
s.w.Trigger(cc.ID, nil)
return err
}
@ -472,12 +547,12 @@ func (s *EtcdServer) configure(ctx context.Context, cc raftpb.ConfChange) error
return err
}
if x != nil {
log.Panicf("unexpected return type")
log.Panicf("return type should always be error")
}
return nil
case <-ctx.Done():
s.w.Trigger(cc.ID, nil) // GC wait
return ctx.Err()
return parseCtxErr(ctx.Err())
case <-s.done:
return ErrStopped
}
@ -516,7 +591,7 @@ func (s *EtcdServer) publish(retryInterval time.Duration) {
req := pb.Request{
ID: GenID(),
Method: "PUT",
Path: path.Join(memberStoreKey(s.id), attributesSuffix),
Path: MemberAttributesStorePath(s.id),
Val: string(b),
}
@ -526,7 +601,7 @@ func (s *EtcdServer) publish(retryInterval time.Duration) {
cancel()
switch err {
case nil:
log.Printf("etcdserver: published %+v to the cluster", s.attributes)
log.Printf("etcdserver: published %+v to cluster %s", s.attributes, s.Cluster.ID())
return
case ErrStopped:
log.Printf("etcdserver: aborting publish because server is stopped")
@ -545,7 +620,9 @@ func getExpirationTime(r *pb.Request) time.Time {
return t
}
func (s *EtcdServer) apply(es []raftpb.Entry, nodes []uint64) uint64 {
// apply takes an Entry received from Raft (after it has been committed) and
// applies it to the current state of the EtcdServer
func (s *EtcdServer) apply(es []raftpb.Entry) (uint64, bool) {
var applied uint64
for i := range es {
e := es[i]
@ -557,15 +634,19 @@ func (s *EtcdServer) apply(es []raftpb.Entry, nodes []uint64) uint64 {
case raftpb.EntryConfChange:
var cc raftpb.ConfChange
pbutil.MustUnmarshal(&cc, e.Data)
s.w.Trigger(cc.ID, s.applyConfChange(cc, nodes))
shouldstop, err := s.applyConfChange(cc)
s.w.Trigger(cc.ID, err)
if shouldstop {
return applied, true
}
default:
panic("unexpected entry type")
log.Panicf("entry type should be either EntryNormal or EntryConfChange")
}
atomic.StoreUint64(&s.raftIndex, e.Index)
atomic.StoreUint64(&s.raftTerm, e.Term)
applied = e.Index
}
return applied
return applied, false
}
// applyRequest interprets r as a call to store.X and returns a Response interpreted
@ -590,14 +671,12 @@ func (s *EtcdServer) applyRequest(r pb.Request) Response {
return f(s.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, expr))
default:
if storeMemberAttributeRegexp.MatchString(r.Path) {
id := parseMemberID(path.Dir(r.Path))
m := s.Cluster.Member(id)
if m == nil {
log.Fatalf("fetch member %x should never fail", id)
}
if err := json.Unmarshal([]byte(r.Val), &m.Attributes); err != nil {
log.Fatalf("unmarshal %s should never fail: %v", r.Val, err)
id := mustParseMemberIDFromKey(path.Dir(r.Path))
var attr Attributes
if err := json.Unmarshal([]byte(r.Val), &attr); err != nil {
log.Panicf("unmarshal %s should never fail: %v", r.Val, err)
}
s.Cluster.UpdateMemberAttributes(id, attr)
}
return f(s.store.Set(r.Path, r.Dir, r.Val, expr))
}
@ -619,46 +698,59 @@ func (s *EtcdServer) applyRequest(r pb.Request) Response {
}
}
func (s *EtcdServer) applyConfChange(cc raftpb.ConfChange, nodes []uint64) error {
if err := s.checkConfChange(cc, nodes); err != nil {
// applyConfChange applies a ConfChange to the server. It is only
// invoked with a ConfChange that has already passed through Raft
func (s *EtcdServer) applyConfChange(cc raftpb.ConfChange) (bool, error) {
if err := s.Cluster.ValidateConfigurationChange(cc); err != nil {
cc.NodeID = raft.None
s.node.ApplyConfChange(cc)
return err
return false, err
}
s.node.ApplyConfChange(cc)
switch cc.Type {
case raftpb.ConfChangeAddNode:
m := new(Member)
if err := json.Unmarshal(cc.Context, m); err != nil {
panic("unexpected unmarshal error")
log.Panicf("unmarshal member should never fail: %v", err)
}
if cc.NodeID != m.ID {
panic("unexpected nodeID mismatch")
if cc.NodeID != uint64(m.ID) {
log.Panicf("nodeID should always be equal to member ID")
}
s.Cluster.AddMember(m)
case raftpb.ConfChangeRemoveNode:
s.Cluster.RemoveMember(cc.NodeID)
}
return nil
}
func (s *EtcdServer) checkConfChange(cc raftpb.ConfChange, nodes []uint64) error {
if s.Cluster.IsIDRemoved(cc.NodeID) {
return ErrIDRemoved
}
switch cc.Type {
case raftpb.ConfChangeAddNode:
if containsUint64(nodes, cc.NodeID) {
return ErrIDExists
if m.ID == s.id {
log.Printf("etcdserver: added local member %s %v to cluster %s", m.ID, m.PeerURLs, s.Cluster.ID())
} else {
s.sender.Add(m)
log.Printf("etcdserver: added member %s %v to cluster %s", m.ID, m.PeerURLs, s.Cluster.ID())
}
case raftpb.ConfChangeRemoveNode:
if !containsUint64(nodes, cc.NodeID) {
return ErrIDNotFound
id := types.ID(cc.NodeID)
s.Cluster.RemoveMember(id)
if id == s.id {
log.Printf("etcdserver: removed local member %s from cluster %s", id, s.Cluster.ID())
log.Println("etcdserver: the data-dir used by this member must be removed so that this host can be re-added with a new member ID")
return true, nil
} else {
s.sender.Remove(id)
log.Printf("etcdserver: removed member %s from cluster %s", id, s.Cluster.ID())
}
case raftpb.ConfChangeUpdateNode:
m := new(Member)
if err := json.Unmarshal(cc.Context, m); err != nil {
log.Panicf("unmarshal member should never fail: %v", err)
}
if cc.NodeID != uint64(m.ID) {
log.Panicf("nodeID should always be equal to member ID")
}
s.Cluster.UpdateMember(m)
if m.ID == s.id {
log.Printf("etcdserver: update local member %s %v in cluster %s", m.ID, m.PeerURLs, s.Cluster.ID())
} else {
s.sender.Update(m)
log.Printf("etcdserver: update member %s %v in cluster %s", m.ID, m.PeerURLs, s.Cluster.ID())
}
default:
panic("unexpected ConfChange type")
}
return nil
return false, nil
}
// TODO: non-blocking snapshot
@ -667,32 +759,78 @@ func (s *EtcdServer) snapshot(snapi uint64, snapnodes []uint64) {
// TODO: current store will never fail to do a snapshot
// what should we do if the store might fail?
if err != nil {
panic("TODO: this is bad, what do we do about it?")
log.Panicf("store save should never fail: %v", err)
}
s.node.Compact(snapi, snapnodes, d)
s.storage.Cut()
if err := s.storage.Cut(); err != nil {
log.Panicf("rotate wal file should never fail: %v", err)
}
}
// checkClientURLsEmptyFromPeers does its best to get the cluster from peers,
// and if this succeeds, checks that the member of the given id exists in the
// cluster, and its ClientURLs is empty.
func checkClientURLsEmptyFromPeers(cl *Cluster, name string) error {
us := getOtherPeerURLs(cl, name)
rcl, err := getClusterFromPeers(us, false)
if err != nil {
return nil
}
id := cl.MemberByName(name).ID
m := rcl.Member(id)
if m == nil {
return nil
}
if len(m.ClientURLs) > 0 {
return fmt.Errorf("etcdserver: member with id %s has started and registered its client urls", id)
}
return nil
}
// GetClusterFromPeers takes a set of URLs representing etcd peers, and
// attempts to construct a Cluster by accessing the members endpoint on one of
// these URLs. The first URL to provide a response is used. If no URLs provide
// a response, or a Cluster cannot be successfully created from a received
// response, an error is returned.
func GetClusterFromPeers(urls []string) (*Cluster, error) {
return getClusterFromPeers(urls, true)
}
// If logerr is true, it prints out more error messages.
func getClusterFromPeers(urls []string, logerr bool) (*Cluster, error) {
cc := &http.Client{
Transport: &http.Transport{
ResponseHeaderTimeout: 500 * time.Millisecond,
},
Timeout: time.Second,
}
for _, u := range urls {
resp, err := http.Get(u + "/members")
resp, err := cc.Get(u + "/members")
if err != nil {
log.Printf("etcdserver: get /members on %s: %v", u, err)
if logerr {
log.Printf("etcdserver: could not get cluster response from %s: %v", u, err)
}
continue
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("etcdserver: read body error: %v", err)
if logerr {
log.Printf("etcdserver: could not read the body of cluster response: %v", err)
}
continue
}
var membs []*Member
if err := json.Unmarshal(b, &membs); err != nil {
log.Printf("etcdserver: unmarshal body error: %v", err)
if logerr {
log.Printf("etcdserver: could not unmarshal cluster response: %v", err)
}
continue
}
id, err := strconv.ParseUint(resp.Header.Get("X-Etcd-Cluster-ID"), 16, 64)
id, err := types.IDFromString(resp.Header.Get("X-Etcd-Cluster-ID"))
if err != nil {
log.Printf("etcdserver: parse uint error: %v", err)
if logerr {
log.Printf("etcdserver: could not parse the cluster ID from cluster res: %v", err)
}
continue
}
return NewClusterFromMembers("", id, membs), nil
@ -700,46 +838,68 @@ func GetClusterFromPeers(urls []string) (*Cluster, error) {
return nil, fmt.Errorf("etcdserver: could not retrieve cluster information from the given urls")
}
func startNode(cfg *ServerConfig, ids []uint64) (id uint64, n raft.Node, w *wal.WAL) {
func startNode(cfg *ServerConfig, ids []types.ID) (id types.ID, n raft.Node, w *wal.WAL) {
var err error
// TODO: remove the discoveryURL when it becomes part of the source for
// generating nodeID.
member := cfg.Cluster.MemberByName(cfg.Name)
metadata := pbutil.MustMarshal(&pb.Metadata{NodeID: member.ID, ClusterID: cfg.Cluster.ID()})
metadata := pbutil.MustMarshal(
&pb.Metadata{
NodeID: uint64(member.ID),
ClusterID: uint64(cfg.Cluster.ID()),
},
)
if w, err = wal.Create(cfg.WALDir(), metadata); err != nil {
log.Fatal(err)
log.Fatalf("etcdserver: create wal error: %v", err)
}
peers := make([]raft.Peer, len(ids))
for i, id := range ids {
ctx, err := json.Marshal((*cfg.Cluster).Member(id))
if err != nil {
log.Fatal(err)
log.Panicf("marshal member should never fail: %v", err)
}
peers[i] = raft.Peer{ID: id, Context: ctx}
peers[i] = raft.Peer{ID: uint64(id), Context: ctx}
}
id = member.ID
log.Printf("etcdserver: start node %x in cluster %x", id, cfg.Cluster.ID())
n = raft.StartNode(id, peers, 10, 1)
log.Printf("etcdserver: start member %s in cluster %s", id, cfg.Cluster.ID())
n = raft.StartNode(uint64(id), peers, 10, 1)
return
}
func restartNode(cfg *ServerConfig, index uint64, snapshot *raftpb.Snapshot) (id uint64, n raft.Node, w *wal.WAL) {
var err error
// restart a node from previous wal
if w, err = wal.OpenAtIndex(cfg.WALDir(), index); err != nil {
log.Fatal(err)
}
wmetadata, st, ents, err := w.ReadAll()
if err != nil {
log.Fatal(err)
// getOtherPeerURLs returns peer urls of other members in the cluster. The
// returned list is sorted in ascending lexicographical order.
func getOtherPeerURLs(cl ClusterInfo, self string) []string {
us := make([]string, 0)
for _, m := range cl.Members() {
if m.Name == self {
continue
}
us = append(us, m.PeerURLs...)
}
sort.Strings(us)
return us
}
func restartNode(cfg *ServerConfig, index uint64, snapshot *raftpb.Snapshot) (types.ID, raft.Node, *wal.WAL) {
w, id, cid, st, ents := readWAL(cfg.WALDir(), index)
cfg.Cluster.SetID(cid)
log.Printf("etcdserver: restart member %s in cluster %s at commit index %d", id, cfg.Cluster.ID(), st.Commit)
n := raft.RestartNode(uint64(id), 10, 1, snapshot, st, ents)
return id, n, w
}
func readWAL(waldir string, index uint64) (w *wal.WAL, id, cid types.ID, st raftpb.HardState, ents []raftpb.Entry) {
var err error
if w, err = wal.OpenAtIndex(waldir, index); err != nil {
log.Fatalf("etcdserver: open wal error: %v", err)
}
var wmetadata []byte
if wmetadata, st, ents, err = w.ReadAll(); err != nil {
log.Fatalf("etcdserver: read wal error: %v", err)
}
var metadata pb.Metadata
pbutil.MustUnmarshal(&metadata, wmetadata)
id = metadata.NodeID
cfg.Cluster.SetID(metadata.ClusterID)
log.Printf("etcdserver: restart member %x in cluster %x at commit index %d", id, cfg.Cluster.ID(), st.Commit)
n = raft.RestartNode(id, 10, 1, snapshot, st, ents)
id = types.ID(metadata.NodeID)
cid = types.ID(metadata.ClusterID)
return
}
@ -752,6 +912,17 @@ func GenID() (n uint64) {
return
}
func parseCtxErr(err error) error {
switch err {
case context.Canceled:
return ErrCanceled
case context.DeadlineExceeded:
return ErrTimeout
default:
return err
}
}
func getBool(v *bool) (vv bool, set bool) {
if v == nil {
return false, false
@ -767,7 +938,3 @@ func containsUint64(a []uint64, x uint64) bool {
}
return false
}
func idAsHex(id uint64) string {
return strconv.FormatUint(id, 16)
}

View File

@ -19,6 +19,8 @@ package etcdserver
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math/rand"
"path"
"reflect"
@ -29,12 +31,18 @@ import (
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
"github.com/coreos/etcd/pkg"
"github.com/coreos/etcd/pkg/pbutil"
"github.com/coreos/etcd/pkg/testutil"
"github.com/coreos/etcd/pkg/types"
"github.com/coreos/etcd/raft"
"github.com/coreos/etcd/raft/raftpb"
"github.com/coreos/etcd/store"
)
func init() {
log.SetOutput(ioutil.Discard)
}
func TestGetExpirationTime(t *testing.T) {
tests := []struct {
r pb.Request
@ -86,6 +94,16 @@ func TestDoLocalAction(t *testing.T) {
},
},
},
{
pb.Request{Method: "HEAD", ID: 1},
Response{Event: &store.Event{}}, nil,
[]action{
action{
name: "Get",
params: []interface{}{"", false, false},
},
},
},
{
pb.Request{Method: "BADMETHOD", ID: 1},
Response{}, ErrUnknownMethod, []action{},
@ -126,6 +144,10 @@ func TestDoBadLocalAction(t *testing.T) {
pb.Request{Method: "GET", ID: 1},
[]action{action{name: "Get"}},
},
{
pb.Request{Method: "HEAD", ID: 1},
[]action{action{name: "Get"}},
},
}
for i, tt := range tests {
st := &errStoreRecorder{err: storeErr}
@ -386,7 +408,7 @@ func TestApplyRequest(t *testing.T) {
}
func TestApplyRequestOnAdminMemberAttributes(t *testing.T) {
cl := newTestCluster([]Member{{ID: 1}})
cl := newTestCluster([]*Member{{ID: 1}})
srv := &EtcdServer{
store: &storeRecorder{},
Cluster: cl,
@ -406,8 +428,13 @@ func TestApplyRequestOnAdminMemberAttributes(t *testing.T) {
// TODO: test ErrIDRemoved
func TestApplyConfChangeError(t *testing.T) {
nodes := []uint64{1, 2, 3}
removed := map[uint64]bool{4: true}
cl := newCluster("")
cl.SetStore(store.New())
for i := 1; i <= 4; i++ {
cl.AddMember(&Member{ID: types.ID(i)})
}
cl.RemoveMember(4)
tests := []struct {
cc raftpb.ConfChange
werr error
@ -421,7 +448,7 @@ func TestApplyConfChangeError(t *testing.T) {
},
{
raftpb.ConfChange{
Type: raftpb.ConfChangeRemoveNode,
Type: raftpb.ConfChangeUpdateNode,
NodeID: 4,
},
ErrIDRemoved,
@ -443,12 +470,11 @@ func TestApplyConfChangeError(t *testing.T) {
}
for i, tt := range tests {
n := &nodeRecorder{}
cl := &Cluster{removed: removed}
srv := &EtcdServer{
node: n,
Cluster: cl,
}
err := srv.applyConfChange(tt.cc, nodes)
_, err := srv.applyConfChange(tt.cc)
if err != tt.werr {
t.Errorf("#%d: applyConfChange error = %v, want %v", i, err, tt.werr)
}
@ -465,22 +491,66 @@ func TestApplyConfChangeError(t *testing.T) {
}
}
func TestApplyConfChangeShouldStop(t *testing.T) {
cl := newCluster("")
cl.SetStore(store.New())
for i := 1; i <= 3; i++ {
cl.AddMember(&Member{ID: types.ID(i)})
}
srv := &EtcdServer{
id: 1,
node: &nodeRecorder{},
Cluster: cl,
sender: &nopSender{},
}
cc := raftpb.ConfChange{
Type: raftpb.ConfChangeRemoveNode,
NodeID: 2,
}
// remove non-local member
shouldStop, err := srv.applyConfChange(cc)
if err != nil {
t.Fatalf("unexpected error %v", err)
}
if shouldStop != false {
t.Errorf("shouldStop = %t, want %t", shouldStop, false)
}
// remove local member
cc.NodeID = 1
shouldStop, err = srv.applyConfChange(cc)
if err != nil {
t.Fatalf("unexpected error %v", err)
}
if shouldStop != true {
t.Errorf("shouldStop = %t, want %t", shouldStop, true)
}
}
func TestClusterOf1(t *testing.T) { testServer(t, 1) }
func TestClusterOf3(t *testing.T) { testServer(t, 3) }
type fakeSender struct {
ss []*EtcdServer
}
func (s *fakeSender) Send(msgs []raftpb.Message) {
for _, m := range msgs {
s.ss[m.To-1].node.Step(context.TODO(), m)
}
}
func (s *fakeSender) Add(m *Member) {}
func (s *fakeSender) Update(m *Member) {}
func (s *fakeSender) Remove(id types.ID) {}
func (s *fakeSender) Stop() {}
func (s *fakeSender) ShouldStopNotify() <-chan struct{} { return nil }
func testServer(t *testing.T, ns uint64) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ss := make([]*EtcdServer, ns)
send := func(msgs []raftpb.Message) {
for _, m := range msgs {
t.Logf("m = %+v\n", m)
ss[m.To-1].node.Step(ctx, m)
}
}
ids := make([]uint64, ns)
for i := uint64(0); i < ns; i++ {
ids[i] = i + 1
@ -491,12 +561,13 @@ func testServer(t *testing.T, ns uint64) {
n := raft.StartNode(id, members, 10, 1)
tk := time.NewTicker(10 * time.Millisecond)
defer tk.Stop()
st := store.New()
cl := newCluster("abc")
cl.SetStore(&storeRecorder{})
cl.SetStore(st)
srv := &EtcdServer{
node: n,
store: store.New(),
send: send,
store: st,
sender: &fakeSender{ss},
storage: &storageRecorder{},
Ticker: tk.C,
Cluster: cl,
@ -521,8 +592,8 @@ func testServer(t *testing.T, ns uint64) {
g, w := resp.Event.Node, &store.NodeExtern{
Key: "/foo",
ModifiedIndex: uint64(i),
CreatedIndex: uint64(i),
ModifiedIndex: uint64(i) + ns,
CreatedIndex: uint64(i) + ns,
Value: stringp("bar"),
}
@ -561,11 +632,11 @@ func TestDoProposal(t *testing.T) {
// this makes <-tk always successful, which accelerates internal clock
close(tk)
cl := newCluster("abc")
cl.SetStore(&storeRecorder{})
cl.SetStore(store.New())
srv := &EtcdServer{
node: n,
store: st,
send: func(_ []raftpb.Message) {},
sender: &nopSender{},
storage: &storageRecorder{},
Ticker: tk,
Cluster: cl,
@ -614,8 +685,8 @@ func TestDoProposalCancelled(t *testing.T) {
if len(gaction) != 0 {
t.Errorf("len(action) = %v, want 0", len(gaction))
}
if err != context.Canceled {
t.Fatalf("err = %v, want %v", err, context.Canceled)
if err != ErrCanceled {
t.Fatalf("err = %v, want %v", err, ErrCanceled)
}
w := []action{action{name: "Register1"}, action{name: "Trigger1"}}
if !reflect.DeepEqual(wait.action, w) {
@ -623,6 +694,18 @@ func TestDoProposalCancelled(t *testing.T) {
}
}
func TestDoProposalTimeout(t *testing.T) {
ctx, _ := context.WithTimeout(context.Background(), 0)
srv := &EtcdServer{
node: &nodeRecorder{},
w: &waitRecorder{},
}
_, err := srv.Do(ctx, pb.Request{Method: "PUT", ID: 1})
if err != ErrTimeout {
t.Fatalf("err = %v, want %v", err, ErrTimeout)
}
}
func TestDoProposalStopped(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@ -632,13 +715,16 @@ func TestDoProposalStopped(t *testing.T) {
tk := make(chan time.Time)
// this makes <-tk always successful, which accelarates internal clock
close(tk)
cl := newCluster("abc")
cl.SetStore(store.New())
srv := &EtcdServer{
// TODO: use fake node for better testability
node: n,
store: st,
send: func(_ []raftpb.Message) {},
sender: &nopSender{},
storage: &storageRecorder{},
Ticker: tk,
Cluster: cl,
}
srv.start()
@ -666,15 +752,20 @@ func TestSync(t *testing.T) {
srv := &EtcdServer{
node: n,
}
start := time.Now()
srv.sync(defaultSyncTimeout)
done := make(chan struct{})
go func() {
srv.sync(10 * time.Second)
close(done)
}()
// check that sync is non-blocking
if d := time.Since(start); d > time.Millisecond {
t.Errorf("CallSyncTime = %v, want < %v", d, time.Millisecond)
select {
case <-done:
case <-time.After(time.Second):
t.Fatalf("sync should be non-blocking but did not return after 1s!")
}
pkg.ForceGosched()
testutil.ForceGosched()
data := n.data()
if len(data) != 1 {
t.Fatalf("len(proposeData) = %d, want 1", len(data))
@ -695,17 +786,22 @@ func TestSyncTimeout(t *testing.T) {
srv := &EtcdServer{
node: n,
}
start := time.Now()
srv.sync(0)
done := make(chan struct{})
go func() {
srv.sync(0)
close(done)
}()
// check that sync is non-blocking
if d := time.Since(start); d > time.Millisecond {
t.Errorf("CallSyncTime = %v, want < %v", d, time.Millisecond)
select {
case <-done:
case <-time.After(time.Second):
t.Fatalf("sync should be non-blocking but did not return after 1s!")
}
// give time for goroutine in sync to cancel
// TODO: use fake clock
pkg.ForceGosched()
testutil.ForceGosched()
w := []action{action{name: "Propose blocked"}}
if g := n.Action(); !reflect.DeepEqual(g, w) {
t.Errorf("action = %v, want %v", g, w)
@ -736,7 +832,7 @@ func TestSyncTrigger(t *testing.T) {
srv := &EtcdServer{
node: n,
store: &storeRecorder{},
send: func(_ []raftpb.Message) {},
sender: &nopSender{},
storage: &storageRecorder{},
SyncTicker: st,
}
@ -801,17 +897,20 @@ func TestTriggerSnap(t *testing.T) {
ctx := context.Background()
n := raft.StartNode(0xBAD0, mustMakePeerSlice(t, 0xBAD0), 10, 1)
<-n.Ready()
n.Advance()
n.ApplyConfChange(raftpb.ConfChange{Type: raftpb.ConfChangeAddNode, NodeID: 0xBAD0})
n.Campaign(ctx)
st := &storeRecorder{}
p := &storageRecorder{}
cl := newCluster("abc")
cl.SetStore(store.New())
s := &EtcdServer{
store: st,
send: func(_ []raftpb.Message) {},
sender: &nopSender{},
storage: p,
node: n,
snapCount: 10,
Cluster: &Cluster{},
Cluster: cl,
}
s.start()
@ -839,17 +938,20 @@ func TestRecvSnapshot(t *testing.T) {
n := newReadyNode()
st := &storeRecorder{}
p := &storageRecorder{}
cl := newCluster("abc")
cl.SetStore(store.New())
s := &EtcdServer{
store: st,
send: func(_ []raftpb.Message) {},
sender: &nopSender{},
storage: p,
node: n,
Cluster: cl,
}
s.start()
n.readyc <- raft.Ready{Snapshot: raftpb.Snapshot{Index: 1}}
// make goroutines move forward to receive snapshot
pkg.ForceGosched()
testutil.ForceGosched()
s.Stop()
wactions := []action{action{name: "Recovery"}}
@ -863,26 +965,30 @@ func TestRecvSnapshot(t *testing.T) {
}
// TestRecvSlowSnapshot tests that slow snapshot will not be applied
// to store.
// to store. The case could happen when server compacts the log and
// raft returns the compacted snapshot.
func TestRecvSlowSnapshot(t *testing.T) {
n := newReadyNode()
st := &storeRecorder{}
cl := newCluster("abc")
cl.SetStore(store.New())
s := &EtcdServer{
store: st,
send: func(_ []raftpb.Message) {},
sender: &nopSender{},
storage: &storageRecorder{},
node: n,
Cluster: cl,
}
s.start()
n.readyc <- raft.Ready{Snapshot: raftpb.Snapshot{Index: 1}}
// make goroutines move forward to receive snapshot
pkg.ForceGosched()
testutil.ForceGosched()
action := st.Action()
n.readyc <- raft.Ready{Snapshot: raftpb.Snapshot{Index: 1}}
// make goroutines move forward to receive snapshot
pkg.ForceGosched()
testutil.ForceGosched()
s.Stop()
if g := st.Action(); !reflect.DeepEqual(g, action) {
@ -890,6 +996,45 @@ func TestRecvSlowSnapshot(t *testing.T) {
}
}
// TestApplySnapshotAndCommittedEntries tests that server applies snapshot
// first and then committed entries.
func TestApplySnapshotAndCommittedEntries(t *testing.T) {
n := newReadyNode()
st := &storeRecorder{}
cl := newCluster("abc")
cl.SetStore(store.New())
s := &EtcdServer{
store: st,
sender: &nopSender{},
storage: &storageRecorder{},
node: n,
Cluster: cl,
}
s.start()
req := &pb.Request{Method: "QGET"}
n.readyc <- raft.Ready{
Snapshot: raftpb.Snapshot{Index: 1},
CommittedEntries: []raftpb.Entry{
{Index: 2, Data: pbutil.MustMarshal(req)},
},
}
// make goroutines move forward to receive snapshot
testutil.ForceGosched()
s.Stop()
actions := st.Action()
if len(actions) != 2 {
t.Fatalf("len(action) = %d, want 2", len(actions))
}
if actions[0].name != "Recovery" {
t.Errorf("actions[0] = %s, want %s", actions[0].name, "Recovery")
}
if actions[1].name != "Get" {
t.Errorf("actions[1] = %s, want %s", actions[1].name, "Get")
}
}
// TestAddMember tests AddMember can propose and perform node addition.
func TestAddMember(t *testing.T) {
n := newNodeConfChangeCommitterRecorder()
@ -900,11 +1045,11 @@ func TestAddMember(t *testing.T) {
},
}
cl := newTestCluster(nil)
cl.SetStore(&storeRecorder{})
cl.SetStore(store.New())
s := &EtcdServer{
node: n,
store: &storeRecorder{},
send: func(_ []raftpb.Message) {},
sender: &nopSender{},
storage: &storageRecorder{},
Cluster: cl,
}
@ -935,12 +1080,13 @@ func TestRemoveMember(t *testing.T) {
Nodes: []uint64{1234, 2345, 3456},
},
}
cl := newTestCluster([]Member{{ID: 1234}})
cl.SetStore(&storeRecorder{})
cl := newTestCluster(nil)
cl.SetStore(store.New())
cl.AddMember(&Member{ID: 1234})
s := &EtcdServer{
node: n,
store: &storeRecorder{},
send: func(_ []raftpb.Message) {},
sender: &nopSender{},
storage: &storageRecorder{},
Cluster: cl,
}
@ -961,6 +1107,43 @@ func TestRemoveMember(t *testing.T) {
}
}
// TestUpdateMember tests RemoveMember can propose and perform node update.
func TestUpdateMember(t *testing.T) {
n := newNodeConfChangeCommitterRecorder()
n.readyc <- raft.Ready{
SoftState: &raft.SoftState{
RaftState: raft.StateLeader,
Nodes: []uint64{1234, 2345, 3456},
},
}
cl := newTestCluster(nil)
cl.SetStore(store.New())
cl.AddMember(&Member{ID: 1234})
s := &EtcdServer{
node: n,
store: &storeRecorder{},
sender: &nopSender{},
storage: &storageRecorder{},
Cluster: cl,
}
s.start()
wm := Member{ID: 1234, RaftAttributes: RaftAttributes{PeerURLs: []string{"http://127.0.0.1:1"}}}
err := s.UpdateMember(context.TODO(), wm)
gaction := n.Action()
s.Stop()
if err != nil {
t.Fatalf("UpdateMember error: %v", err)
}
wactions := []action{action{name: "ProposeConfChange:ConfChangeUpdateNode"}, action{name: "ApplyConfChange:ConfChangeUpdateNode"}}
if !reflect.DeepEqual(gaction, wactions) {
t.Errorf("action = %v, want %v", gaction, wactions)
}
if !reflect.DeepEqual(cl.Member(1234), &wm) {
t.Errorf("member = %v, want %v", cl.Member(1234), &wm)
}
}
// TODO: test server could stop itself when being removed
// TODO: test wait trigger correctness in multi-server case
@ -974,6 +1157,7 @@ func TestPublish(t *testing.T) {
srv := &EtcdServer{
id: 1,
attributes: Attributes{Name: "node1", ClientURLs: []string{"http://a", "http://b"}},
Cluster: &Cluster{},
node: n,
w: w,
}
@ -1007,12 +1191,13 @@ func TestPublish(t *testing.T) {
func TestPublishStopped(t *testing.T) {
srv := &EtcdServer{
node: &nodeRecorder{},
sender: &nopSender{},
Cluster: &Cluster{},
w: &waitRecorder{},
done: make(chan struct{}),
stopped: make(chan struct{}),
stop: make(chan struct{}),
}
close(srv.stopped)
srv.Stop()
close(srv.done)
srv.publish(time.Hour)
}
@ -1024,7 +1209,7 @@ func TestPublishRetry(t *testing.T) {
w: &waitRecorder{},
done: make(chan struct{}),
}
time.AfterFunc(500*time.Microsecond, srv.Stop)
time.AfterFunc(500*time.Microsecond, func() { close(srv.done) })
srv.publish(10 * time.Nanosecond)
action := n.Action()
@ -1034,6 +1219,71 @@ func TestPublishRetry(t *testing.T) {
}
}
func TestStopNotify(t *testing.T) {
s := &EtcdServer{
stop: make(chan struct{}),
done: make(chan struct{}),
}
go func() {
<-s.stop
close(s.done)
}()
notifier := s.StopNotify()
select {
case <-notifier:
t.Fatalf("received unexpected stop notification")
default:
}
s.Stop()
select {
case <-notifier:
default:
t.Fatalf("cannot receive stop notification")
}
}
func TestGetOtherPeerURLs(t *testing.T) {
tests := []struct {
membs []*Member
self string
wurls []string
}{
{
[]*Member{
newTestMember(1, []string{"http://10.0.0.1"}, "a", nil),
},
"a",
[]string{},
},
{
[]*Member{
newTestMember(1, []string{"http://10.0.0.1"}, "a", nil),
newTestMember(2, []string{"http://10.0.0.2"}, "b", nil),
newTestMember(3, []string{"http://10.0.0.3"}, "c", nil),
},
"a",
[]string{"http://10.0.0.2", "http://10.0.0.3"},
},
{
[]*Member{
newTestMember(1, []string{"http://10.0.0.1"}, "a", nil),
newTestMember(3, []string{"http://10.0.0.3"}, "c", nil),
newTestMember(2, []string{"http://10.0.0.2"}, "b", nil),
},
"a",
[]string{"http://10.0.0.2", "http://10.0.0.3"},
},
}
for i, tt := range tests {
cl := NewClusterFromMembers("", types.ID(0), tt.membs)
urls := getOtherPeerURLs(cl, tt.self)
if !reflect.DeepEqual(urls, tt.wurls) {
t.Errorf("#%d: urls = %+v, want %+v", i, urls, tt.wurls)
}
}
}
func TestGetBool(t *testing.T) {
tests := []struct {
b *bool
@ -1207,18 +1457,19 @@ type storageRecorder struct {
recorder
}
func (p *storageRecorder) Save(st raftpb.HardState, ents []raftpb.Entry) {
func (p *storageRecorder) Save(st raftpb.HardState, ents []raftpb.Entry) error {
p.record(action{name: "Save"})
return nil
}
func (p *storageRecorder) Cut() error {
p.record(action{name: "Cut"})
return nil
}
func (p *storageRecorder) SaveSnap(st raftpb.Snapshot) {
if raft.IsEmptySnap(st) {
return
func (p *storageRecorder) SaveSnap(st raftpb.Snapshot) error {
if !raft.IsEmptySnap(st) {
p.record(action{name: "SaveSnap"})
}
p.record(action{name: "SaveSnap"})
return nil
}
type readyNode struct {
@ -1237,6 +1488,7 @@ func (n *readyNode) ProposeConfChange(ctx context.Context, conf raftpb.ConfChang
}
func (n *readyNode) Step(ctx context.Context, msg raftpb.Message) error { return nil }
func (n *readyNode) Ready() <-chan raft.Ready { return n.readyc }
func (n *readyNode) Advance() {}
func (n *readyNode) ApplyConfChange(conf raftpb.ConfChange) {}
func (n *readyNode) Stop() {}
func (n *readyNode) Compact(index uint64, nodes []uint64, d []byte) {}
@ -1245,9 +1497,8 @@ type nodeRecorder struct {
recorder
}
func (n *nodeRecorder) Tick() {
n.record(action{name: "Tick"})
}
func (n *nodeRecorder) Tick() { n.record(action{name: "Tick"}) }
func (n *nodeRecorder) Campaign(ctx context.Context) error {
n.record(action{name: "Campaign"})
return nil
@ -1265,6 +1516,7 @@ func (n *nodeRecorder) Step(ctx context.Context, msg raftpb.Message) error {
return nil
}
func (n *nodeRecorder) Ready() <-chan raft.Ready { return nil }
func (n *nodeRecorder) Advance() {}
func (n *nodeRecorder) ApplyConfChange(conf raftpb.ConfChange) {
n.record(action{name: "ApplyConfChange", params: []interface{}{conf}})
}
@ -1339,35 +1591,19 @@ func (w *waitWithResponse) Register(id uint64) <-chan interface{} {
}
func (w *waitWithResponse) Trigger(id uint64, x interface{}) {}
type clusterStoreRecorder struct {
recorder
}
type nopSender struct{}
func (cs *clusterStoreRecorder) Add(m Member) {
cs.record(action{name: "Add", params: []interface{}{m}})
}
func (cs *clusterStoreRecorder) Get() Cluster {
cs.record(action{name: "Get"})
return Cluster{}
}
func (cs *clusterStoreRecorder) Remove(id uint64) {
cs.record(action{name: "Remove", params: []interface{}{id}})
}
func (cs *clusterStoreRecorder) IsRemoved(id uint64) bool { return false }
type removedClusterStore struct {
removed map[uint64]bool
}
func (cs *removedClusterStore) Add(m Member) {}
func (cs *removedClusterStore) Get() Cluster { return Cluster{} }
func (cs *removedClusterStore) Remove(id uint64) {}
func (cs *removedClusterStore) IsRemoved(id uint64) bool { return cs.removed[id] }
func (s *nopSender) Send(m []raftpb.Message) {}
func (s *nopSender) Add(m *Member) {}
func (s *nopSender) Remove(id types.ID) {}
func (s *nopSender) Update(m *Member) {}
func (s *nopSender) Stop() {}
func (s *nopSender) ShouldStopNotify() <-chan struct{} { return nil }
func mustMakePeerSlice(t *testing.T, ids ...uint64) []raft.Peer {
peers := make([]raft.Peer, len(ids))
for i, id := range ids {
m := Member{ID: id}
m := Member{ID: types.ID(id)}
b, err := json.Marshal(m)
if err != nil {
t.Fatal(err)

View File

@ -49,7 +49,7 @@ func (ls *LeaderStats) JSON() []byte {
b, err := json.Marshal(stats)
// TODO(jonboulle): appropriate error handling?
if err != nil {
log.Printf("error marshalling leader stats: %v", err)
log.Printf("stats: error marshalling leader stats: %v", err)
}
return b
}

View File

@ -64,7 +64,7 @@ func (ss *ServerStats) JSON() []byte {
b, err := json.Marshal(stats)
// TODO(jonboulle): appropriate error handling?
if err != nil {
log.Printf("error marshalling server stats: %v", err)
log.Printf("stats: error marshalling server stats: %v", err)
}
return b
}

View File

@ -1,4 +1,6 @@
# Use goreman to run `go get github.com/mattn/goreman`
etcd1: ../../bin/etcd -name node1 -listen-client-urls http://127.0.0.1:4001 -advertise-client-urls http://127.0.0.1:4001 -listen-peer-urls http://127.0.0.1:7001 -initial-advertise-peer-urls http://127.0.0.1:7001
etcd2: ../../bin/etcd -name node2 -listen-client-urls http://127.0.0.1:4002 -advertise-client-urls http://127.0.0.1:4002 -listen-peer-urls http://127.0.0.1:7002 -initial-advertise-peer-urls http://127.0.0.1:7002
etcd3: ../../bin/etcd -name node3 -listen-client-urls http://127.0.0.1:4003 -advertise-client-urls http://127.0.0.1:4003 -listen-peer-urls http://127.0.0.1:7003 -initial-advertise-peer-urls http://127.0.0.1:7003
# One of the four etcd members falls back to a proxy
etcd1: ../../bin/etcd -name infra1 -listen-client-urls http://127.0.0.1:4001 -advertise-client-urls http://127.0.0.1:4001 -listen-peer-urls http://127.0.0.1:7001 -initial-advertise-peer-urls http://127.0.0.1:7001
etcd2: ../../bin/etcd -name infra2 -listen-client-urls http://127.0.0.1:4002 -advertise-client-urls http://127.0.0.1:4002 -listen-peer-urls http://127.0.0.1:7002 -initial-advertise-peer-urls http://127.0.0.1:7002
etcd3: ../../bin/etcd -name infra3 -listen-client-urls http://127.0.0.1:4003 -advertise-client-urls http://127.0.0.1:4003 -listen-peer-urls http://127.0.0.1:7003 -initial-advertise-peer-urls http://127.0.0.1:7003
etcd4: ../../bin/etcd -name infra4 -listen-client-urls http://127.0.0.1:4004 -advertise-client-urls http://127.0.0.1:4004 -listen-peer-urls http://127.0.0.1:7004 -initial-advertise-peer-urls http://127.0.0.1:7004

View File

@ -1,6 +1,7 @@
#!/bin/sh
disc=$(curl https://discovery.etcd.io/new?size=3)
rm -rf infra*.etcd
disc=$(curl -s https://discovery.etcd.io/new?size=3)
echo ETCD_DISCOVERY=${disc} > .env
echo "setup discovery start your cluster"
cat .env

View File

@ -1,3 +1,19 @@
/*
Copyright 2014 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 integration
import (
@ -7,21 +23,26 @@ import (
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"reflect"
"sort"
"strings"
"testing"
"time"
"github.com/coreos/etcd/client"
"github.com/coreos/etcd/etcdserver"
"github.com/coreos/etcd/etcdserver/etcdhttp"
"github.com/coreos/etcd/pkg/transport"
"github.com/coreos/etcd/etcdserver/etcdhttp/httptypes"
"github.com/coreos/etcd/pkg/types"
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
)
const (
tickDuration = 5 * time.Millisecond
clusterName = "etcd"
tickDuration = 10 * time.Millisecond
clusterName = "etcd"
requestTimeout = 2 * time.Second
)
func init() {
@ -34,97 +55,217 @@ func TestClusterOf3(t *testing.T) { testCluster(t, 3) }
func testCluster(t *testing.T, size int) {
defer afterTest(t)
c := &cluster{Size: size}
c := NewCluster(t, size)
c.Launch(t)
defer c.Terminate(t)
clusterMustProgress(t, c)
}
func TestClusterOf1UsingDiscovery(t *testing.T) { testClusterUsingDiscovery(t, 1) }
func TestClusterOf3UsingDiscovery(t *testing.T) { testClusterUsingDiscovery(t, 3) }
func testClusterUsingDiscovery(t *testing.T, size int) {
defer afterTest(t)
dc := NewCluster(t, 1)
dc.Launch(t)
defer dc.Terminate(t)
// init discovery token space
dcc := mustNewHTTPClient(t, dc.URLs())
dkapi := client.NewKeysAPI(dcc)
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
if _, err := dkapi.Create(ctx, "/_config/size", fmt.Sprintf("%d", size), -1); err != nil {
t.Fatal(err)
}
cancel()
c := NewClusterByDiscovery(t, size, dc.URL(0)+"/v2/keys")
c.Launch(t)
defer c.Terminate(t)
clusterMustProgress(t, c)
}
func TestDoubleClusterSizeOf1(t *testing.T) { testDoubleClusterSize(t, 1) }
func TestDoubleClusterSizeOf3(t *testing.T) { testDoubleClusterSize(t, 3) }
func testDoubleClusterSize(t *testing.T, size int) {
defer afterTest(t)
c := NewCluster(t, size)
c.Launch(t)
defer c.Terminate(t)
for i := 0; i < size; i++ {
for _, u := range c.Members[i].ClientURLs {
var err error
for j := 0; j < 3; j++ {
if err = setKey(u, "/foo", "bar"); err == nil {
break
}
}
if err != nil {
t.Errorf("setKey on %v error: %v", u.String(), err)
}
c.AddMember(t)
}
clusterMustProgress(t, c)
}
func TestLaunchDuplicateMemberShouldFail(t *testing.T) {
size := 3
c := NewCluster(t, size)
m := c.Members[0].Clone()
var err error
m.DataDir, err = ioutil.TempDir(os.TempDir(), "etcd")
if err != nil {
t.Fatal(err)
}
c.Launch(t)
defer c.Terminate(t)
if err := m.Launch(); err == nil {
t.Errorf("unexpect successful launch")
}
}
// clusterMustProgress ensures that cluster can make progress. It creates
// a key first, and check the new key could be got from all client urls of
// the cluster.
func clusterMustProgress(t *testing.T, cl *cluster) {
cc := mustNewHTTPClient(t, []string{cl.URL(0)})
kapi := client.NewKeysAPI(cc)
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
resp, err := kapi.Create(ctx, "/foo", "bar", -1)
if err != nil {
t.Fatalf("create on %s error: %v", cl.URL(0), err)
}
cancel()
for i, u := range cl.URLs() {
cc := mustNewHTTPClient(t, []string{u})
kapi := client.NewKeysAPI(cc)
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
if _, err := kapi.Watch("foo", resp.Node.ModifiedIndex).Next(ctx); err != nil {
t.Fatalf("#%d: watch on %s error: %v", i, u, err)
}
cancel()
}
c.Terminate(t)
}
// TODO: use etcd client
func setKey(u url.URL, key string, value string) error {
u.Path = "/v2/keys" + key
v := url.Values{"value": []string{value}}
req, err := http.NewRequest("PUT", u.String(), strings.NewReader(v.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
ioutil.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("statusCode = %d, want %d or %d", resp.StatusCode, http.StatusOK, http.StatusCreated)
}
return nil
}
type cluster struct {
Size int
Members []member
}
// TODO: support TLS
func (c *cluster) Launch(t *testing.T) {
if c.Size <= 0 {
t.Fatalf("cluster size <= 0")
}
type cluster struct {
Members []*member
}
lns := make([]net.Listener, c.Size)
addrs := make([]string, c.Size)
for i := 0; i < c.Size; i++ {
l := newLocalListener(t)
// each member claims only one peer listener
lns[i] = l
addrs[i] = fmt.Sprintf("%v=%v", c.name(i), "http://"+l.Addr().String())
// NewCluster returns an unlaunched cluster of the given size which has been
// set to use static bootstrap.
func NewCluster(t *testing.T, size int) *cluster {
c := &cluster{}
ms := make([]*member, size)
for i := 0; i < size; i++ {
ms[i] = mustNewMember(t, c.name(i))
}
c.Members = ms
addrs := make([]string, 0)
for _, m := range ms {
for _, l := range m.PeerListeners {
addrs = append(addrs, fmt.Sprintf("%s=%s", m.Name, "http://"+l.Addr().String()))
}
}
clusterStr := strings.Join(addrs, ",")
var err error
for i := 0; i < c.Size; i++ {
m := member{}
m.PeerListeners = []net.Listener{lns[i]}
cln := newLocalListener(t)
m.ClientListeners = []net.Listener{cln}
m.Name = c.name(i)
m.ClientURLs, err = types.NewURLs([]string{"http://" + cln.Addr().String()})
if err != nil {
t.Fatal(err)
}
m.DataDir, err = ioutil.TempDir(os.TempDir(), "etcd")
if err != nil {
t.Fatal(err)
}
for _, m := range ms {
m.Cluster, err = etcdserver.NewClusterFromString(clusterName, clusterStr)
if err != nil {
t.Fatal(err)
}
m.ClusterState = etcdserver.ClusterStateValueNew
m.Transport, err = transport.NewTransport(transport.TLSInfo{})
if err != nil {
t.Fatal(err)
}
// TODO: need the support of graceful stop in Sender to remove this
m.Transport.DisableKeepAlives = true
m.Transport.Dial = (&net.Dialer{Timeout: 100 * time.Millisecond}).Dial
m.Launch(t)
c.Members = append(c.Members, m)
}
return c
}
// NewClusterUsingDiscovery returns an unlaunched cluster of the given size
// which has been set to use the given url as discovery service to bootstrap.
func NewClusterByDiscovery(t *testing.T, size int, url string) *cluster {
c := &cluster{}
ms := make([]*member, size)
for i := 0; i < size; i++ {
ms[i] = mustNewMember(t, c.name(i))
ms[i].DiscoveryURL = url
}
c.Members = ms
return c
}
func (c *cluster) Launch(t *testing.T) {
errc := make(chan error)
for _, m := range c.Members {
// Members are launched in separate goroutines because if they boot
// using discovery url, they have to wait for others to register to continue.
go func(m *member) {
errc <- m.Launch()
}(m)
}
for _ = range c.Members {
if err := <-errc; err != nil {
t.Fatalf("error setting up member: %v", err)
}
}
// wait cluster to be stable to receive future client requests
c.waitMembersMatch(t, c.HTTPMembers())
}
func (c *cluster) URL(i int) string {
return c.Members[i].ClientURLs[0].String()
}
func (c *cluster) URLs() []string {
urls := make([]string, 0)
for _, m := range c.Members {
for _, u := range m.ClientURLs {
urls = append(urls, u.String())
}
}
return urls
}
func (c *cluster) HTTPMembers() []httptypes.Member {
ms := make([]httptypes.Member, len(c.Members))
for i, m := range c.Members {
ms[i].Name = m.Name
for _, ln := range m.PeerListeners {
ms[i].PeerURLs = append(ms[i].PeerURLs, "http://"+ln.Addr().String())
}
for _, ln := range m.ClientListeners {
ms[i].ClientURLs = append(ms[i].ClientURLs, "http://"+ln.Addr().String())
}
}
return ms
}
func (c *cluster) AddMember(t *testing.T) {
clusterStr := c.Members[0].Cluster.String()
idx := len(c.Members)
m := mustNewMember(t, c.name(idx))
// send add request to the cluster
cc := mustNewHTTPClient(t, []string{c.URL(0)})
ma := client.NewMembersAPI(cc)
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
peerURL := "http://" + m.PeerListeners[0].Addr().String()
if _, err := ma.Add(ctx, peerURL); err != nil {
t.Fatalf("add member on %s error: %v", c.URL(0), err)
}
cancel()
// wait for the add node entry applied in the cluster
members := append(c.HTTPMembers(), httptypes.Member{PeerURLs: []string{peerURL}, ClientURLs: []string{}})
c.waitMembersMatch(t, members)
for _, ln := range m.PeerListeners {
clusterStr += fmt.Sprintf(",%s=http://%s", m.Name, ln.Addr().String())
}
var err error
m.Cluster, err = etcdserver.NewClusterFromString(clusterName, clusterStr)
if err != nil {
t.Fatal(err)
}
m.NewCluster = false
if err := m.Launch(); err != nil {
t.Fatal(err)
}
c.Members = append(c.Members, m)
// wait cluster to be stable to receive future client requests
c.waitMembersMatch(t, c.HTTPMembers())
}
func (c *cluster) Terminate(t *testing.T) {
@ -133,10 +274,38 @@ func (c *cluster) Terminate(t *testing.T) {
}
}
func (c *cluster) waitMembersMatch(t *testing.T, membs []httptypes.Member) {
for _, u := range c.URLs() {
cc := mustNewHTTPClient(t, []string{u})
ma := client.NewMembersAPI(cc)
for {
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
ms, err := ma.List(ctx)
cancel()
if err == nil && isMembersEqual(ms, membs) {
break
}
time.Sleep(tickDuration)
}
}
return
}
func (c *cluster) name(i int) string {
return fmt.Sprint("node", i)
}
// isMembersEqual checks whether two members equal except ID field.
// The given wmembs should always set ID field to empty string.
func isMembersEqual(membs []httptypes.Member, wmembs []httptypes.Member) bool {
sort.Sort(SortableMemberSliceByPeerURLs(membs))
sort.Sort(SortableMemberSliceByPeerURLs(wmembs))
for i := range membs {
membs[i].ID = ""
}
return reflect.DeepEqual(membs, wmembs)
}
func newLocalListener(t *testing.T) net.Listener {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
@ -153,12 +322,78 @@ type member struct {
hss []*httptest.Server
}
func mustNewMember(t *testing.T, name string) *member {
var err error
m := &member{}
pln := newLocalListener(t)
m.PeerListeners = []net.Listener{pln}
m.PeerURLs, err = types.NewURLs([]string{"http://" + pln.Addr().String()})
if err != nil {
t.Fatal(err)
}
cln := newLocalListener(t)
m.ClientListeners = []net.Listener{cln}
m.ClientURLs, err = types.NewURLs([]string{"http://" + cln.Addr().String()})
if err != nil {
t.Fatal(err)
}
m.Name = name
m.DataDir, err = ioutil.TempDir(os.TempDir(), "etcd")
if err != nil {
t.Fatal(err)
}
clusterStr := fmt.Sprintf("%s=http://%s", name, pln.Addr().String())
m.Cluster, err = etcdserver.NewClusterFromString(clusterName, clusterStr)
if err != nil {
t.Fatal(err)
}
m.NewCluster = true
m.Transport = newTransport()
return m
}
// Clone returns a member with the same server configuration. The returned
// member will not set PeerListeners and ClientListeners.
func (m *member) Clone() *member {
mm := &member{}
mm.ServerConfig = m.ServerConfig
var err error
clientURLStrs := m.ClientURLs.StringSlice()
mm.ClientURLs, err = types.NewURLs(clientURLStrs)
if err != nil {
// this should never fail
panic(err)
}
peerURLStrs := m.PeerURLs.StringSlice()
mm.PeerURLs, err = types.NewURLs(peerURLStrs)
if err != nil {
// this should never fail
panic(err)
}
clusterStr := m.Cluster.String()
mm.Cluster, err = etcdserver.NewClusterFromString(clusterName, clusterStr)
if err != nil {
// this should never fail
panic(err)
}
mm.Transport = newTransport()
return mm
}
// Launch starts a member based on ServerConfig, PeerListeners
// and ClientListeners.
func (m *member) Launch(t *testing.T) {
m.s = etcdserver.NewServer(&m.ServerConfig)
func (m *member) Launch() error {
var err error
if m.s, err = etcdserver.NewServer(&m.ServerConfig); err != nil {
return fmt.Errorf("failed to initialize the etcd server: %v", err)
}
m.s.Ticker = time.Tick(tickDuration)
m.s.SyncTicker = nil
m.s.SyncTicker = time.Tick(500 * time.Millisecond)
m.s.Start()
for _, ln := range m.PeerListeners {
@ -177,6 +412,7 @@ func (m *member) Launch(t *testing.T) {
hs.Start()
m.hss = append(m.hss, hs)
}
return nil
}
// Stop stops the member, but the data dir of the member is preserved.
@ -184,18 +420,43 @@ func (m *member) Stop(t *testing.T) {
panic("unimplemented")
}
// Start starts the member using preserved data dir.
// Start starts the member using the preserved data dir.
func (m *member) Start(t *testing.T) {
panic("unimplemented")
}
// Terminate stops the member and remove the data dir.
// Terminate stops the member and removes the data dir.
func (m *member) Terminate(t *testing.T) {
m.s.Stop()
for _, hs := range m.hss {
hs.CloseClientConnections()
hs.Close()
}
if err := os.RemoveAll(m.ServerConfig.DataDir); err != nil {
t.Fatal(err)
}
}
func mustNewHTTPClient(t *testing.T, eps []string) client.HTTPClient {
cc, err := client.NewHTTPClient(newTransport(), eps)
if err != nil {
t.Fatal(err)
}
return cc
}
func newTransport() *http.Transport {
tr := &http.Transport{}
// TODO: need the support of graceful stop in Sender to remove this
tr.DisableKeepAlives = true
tr.Dial = (&net.Dialer{Timeout: 100 * time.Millisecond}).Dial
return tr
}
type SortableMemberSliceByPeerURLs []httptypes.Member
func (p SortableMemberSliceByPeerURLs) Len() int { return len(p) }
func (p SortableMemberSliceByPeerURLs) Less(i, j int) bool {
return p[i].PeerURLs[0] < p[j].PeerURLs[0]
}
func (p SortableMemberSliceByPeerURLs) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

View File

@ -1,25 +1,27 @@
// Copyright 2014 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.
/*
Copyright 2014 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.
*/
/*
functional tests are built upon embeded etcd, and focus on etcd functional
correctness.
Package integration implements tests built upon embedded etcd, and focus on
etcd correctness.
Its goal:
1. it tests the whole code base except the command line parse.
2. it is able to check internal data, including raft, store and etc.
3. it is based on goroutine, which is faster than process.
4. it mainly tests user behavior and user-facing API.
Features/goals of the integration tests:
1. test the whole code base except command-line parsing.
2. check internal data, including raft, store and etc.
3. based on goroutines, which is faster than process.
4. mainly tests user behavior and user-facing API.
*/
package integration

File diff suppressed because it is too large Load Diff

298
main.go
View File

@ -14,297 +14,21 @@
limitations under the License.
*/
// Package main is a simple wrapper of the real etcd entrypoint package
// (located at github.com/coreos/etcd/etcdmain) to ensure that etcd is still
// "go getable"; e.g. `go get github.com/coreos/etcd` works as expected and
// builds a binary in $GOBIN/etcd
//
// This package should NOT be extended or modified in any way; to modify the
// etcd binary, work in the `github.com/coreos/etcd/etcdmain` package.
//
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"strings"
"github.com/coreos/etcd/etcdserver"
"github.com/coreos/etcd/etcdserver/etcdhttp"
"github.com/coreos/etcd/pkg"
flagtypes "github.com/coreos/etcd/pkg/flags"
"github.com/coreos/etcd/pkg/transport"
"github.com/coreos/etcd/pkg/types"
"github.com/coreos/etcd/proxy"
"github.com/coreos/etcd/version"
"github.com/coreos/etcd/etcdmain"
)
const (
// the owner can make/remove files inside the directory
privateDirMode = 0700
)
var (
fs = flag.NewFlagSet("etcd", flag.ContinueOnError)
name = fs.String("name", "default", "Unique human-readable name for this node")
dir = fs.String("data-dir", "", "Path to the data directory")
durl = fs.String("discovery", "", "Discovery service used to bootstrap the cluster")
snapCount = fs.Uint64("snapshot-count", etcdserver.DefaultSnapCount, "Number of committed transactions to trigger a snapshot")
printVersion = fs.Bool("version", false, "Print the version and exit")
initialCluster = fs.String("initial-cluster", "default=http://localhost:2380,default=http://localhost:7001", "Initial cluster configuration for bootstrapping")
initialClusterName = fs.String("initial-cluster-name", "etcd", "Initial name for the etcd cluster during bootstrap")
clusterState = new(etcdserver.ClusterState)
cors = &pkg.CORSInfo{}
proxyFlag = new(flagtypes.Proxy)
clientTLSInfo = transport.TLSInfo{}
peerTLSInfo = transport.TLSInfo{}
ignored = []string{
"cluster-active-size",
"cluster-remove-delay",
"cluster-sync-interval",
"config",
"force",
"max-result-buffer",
"max-retry-attempts",
"peer-heartbeat-interval",
"peer-election-timeout",
"retry-interval",
"snapshot",
"v",
"vv",
}
)
func init() {
fs.Var(clusterState, "initial-cluster-state", "Initial cluster configuration for bootstrapping")
clusterState.Set(etcdserver.ClusterStateValueNew)
fs.Var(flagtypes.NewURLsValue("http://localhost:2380,http://localhost:7001"), "initial-advertise-peer-urls", "List of this member's peer URLs to advertise to the rest of the cluster")
fs.Var(flagtypes.NewURLsValue("http://localhost:2379,http://localhost:4001"), "advertise-client-urls", "List of this member's client URLs to advertise to the rest of the cluster")
fs.Var(flagtypes.NewURLsValue("http://localhost:2380,http://localhost:7001"), "listen-peer-urls", "List of URLs to listen on for peer traffic")
fs.Var(flagtypes.NewURLsValue("http://localhost:2379,http://localhost:4001"), "listen-client-urls", "List of URLs to listen on for client traffic")
fs.Var(cors, "cors", "Comma-separated white list of origins for CORS (cross-origin resource sharing).")
fs.Var(proxyFlag, "proxy", fmt.Sprintf("Valid values include %s", strings.Join(flagtypes.ProxyValues, ", ")))
proxyFlag.Set(flagtypes.ProxyValueOff)
fs.StringVar(&clientTLSInfo.CAFile, "ca-file", "", "Path to the client server TLS CA file.")
fs.StringVar(&clientTLSInfo.CertFile, "cert-file", "", "Path to the client server TLS cert file.")
fs.StringVar(&clientTLSInfo.KeyFile, "key-file", "", "Path to the client server TLS key file.")
fs.StringVar(&peerTLSInfo.CAFile, "peer-ca-file", "", "Path to the peer server TLS CA file.")
fs.StringVar(&peerTLSInfo.CertFile, "peer-cert-file", "", "Path to the peer server TLS cert file.")
fs.StringVar(&peerTLSInfo.KeyFile, "peer-key-file", "", "Path to the peer server TLS key file.")
// backwards-compatibility with v0.4.6
fs.Var(&flagtypes.IPAddressPort{}, "addr", "DEPRECATED: Use -advertise-client-urls instead.")
fs.Var(&flagtypes.IPAddressPort{}, "bind-addr", "DEPRECATED: Use -listen-client-urls instead.")
fs.Var(&flagtypes.IPAddressPort{}, "peer-addr", "DEPRECATED: Use -initial-advertise-peer-urls instead.")
fs.Var(&flagtypes.IPAddressPort{}, "peer-bind-addr", "DEPRECATED: Use -listen-peer-urls instead.")
for _, f := range ignored {
fs.Var(&pkg.IgnoredFlag{Name: f}, f, "")
}
fs.Var(&pkg.DeprecatedFlag{Name: "peers"}, "peers", "DEPRECATED: Use -initial-cluster instead")
fs.Var(&pkg.DeprecatedFlag{Name: "peers-file"}, "peers-file", "DEPRECATED: Use -initial-cluster instead")
}
func main() {
fs.Usage = pkg.UsageWithIgnoredFlagsFunc(fs, ignored)
err := fs.Parse(os.Args[1:])
switch err {
case nil:
case flag.ErrHelp:
os.Exit(0)
default:
os.Exit(2)
}
if *printVersion {
fmt.Println("etcd version", version.Version)
os.Exit(0)
}
pkg.SetFlagsFromEnv(fs)
if string(*proxyFlag) == flagtypes.ProxyValueOff {
startEtcd()
} else {
startProxy()
}
// Block indefinitely
<-make(chan struct{})
}
// startEtcd launches the etcd server and HTTP handlers for client/server communication.
func startEtcd() {
cls, err := setupCluster()
if err != nil {
log.Fatalf("etcd: error setting up initial cluster: %v", err)
}
if *dir == "" {
*dir = fmt.Sprintf("%v.etcd", *name)
log.Printf("etcd: no data-dir provided, using default data-dir ./%s", *dir)
}
if err := os.MkdirAll(*dir, privateDirMode); err != nil {
log.Fatalf("etcd: cannot create data directory: %v", err)
}
pt, err := transport.NewTransport(peerTLSInfo)
if err != nil {
log.Fatal(err)
}
acurls, err := pkg.URLsFromFlags(fs, "advertise-client-urls", "addr", clientTLSInfo)
if err != nil {
log.Fatal(err.Error())
}
cfg := &etcdserver.ServerConfig{
Name: *name,
ClientURLs: acurls,
DataDir: *dir,
SnapCount: *snapCount,
Cluster: cls,
DiscoveryURL: *durl,
ClusterState: *clusterState,
Transport: pt,
}
s := etcdserver.NewServer(cfg)
s.Start()
ch := &pkg.CORSHandler{
Handler: etcdhttp.NewClientHandler(s),
Info: cors,
}
ph := etcdhttp.NewPeerHandler(s)
lpurls, err := pkg.URLsFromFlags(fs, "listen-peer-urls", "peer-bind-addr", peerTLSInfo)
if err != nil {
log.Fatal(err.Error())
}
for _, u := range lpurls {
l, err := transport.NewListener(u.Host, peerTLSInfo)
if err != nil {
log.Fatal(err)
}
// Start the peer server in a goroutine
urlStr := u.String()
go func() {
log.Print("Listening for peers on ", urlStr)
log.Fatal(http.Serve(l, ph))
}()
}
lcurls, err := pkg.URLsFromFlags(fs, "listen-client-urls", "bind-addr", clientTLSInfo)
if err != nil {
log.Fatal(err.Error())
}
// Start a client server goroutine for each listen address
for _, u := range lcurls {
l, err := transport.NewListener(u.Host, clientTLSInfo)
if err != nil {
log.Fatal(err)
}
urlStr := u.String()
go func() {
log.Print("Listening for client requests on ", urlStr)
log.Fatal(http.Serve(l, ch))
}()
}
}
// startProxy launches an HTTP proxy for client communication which proxies to other etcd nodes.
func startProxy() {
cls, err := setupCluster()
if err != nil {
log.Fatalf("etcd: error setting up initial cluster: %v", err)
}
pt, err := transport.NewTransport(clientTLSInfo)
if err != nil {
log.Fatal(err)
}
// TODO(jonboulle): update peerURLs dynamically (i.e. when updating
// clientURLs) instead of just using the initial fixed list here
peerURLs := cls.PeerURLs()
uf := func() []string {
cls, err := etcdserver.GetClusterFromPeers(peerURLs)
if err != nil {
log.Printf("etcd: %v", err)
return []string{}
}
return cls.ClientURLs()
}
ph := proxy.NewHandler(pt, uf)
ph = &pkg.CORSHandler{
Handler: ph,
Info: cors,
}
if string(*proxyFlag) == flagtypes.ProxyValueReadonly {
ph = proxy.NewReadonlyHandler(ph)
}
lcurls, err := pkg.URLsFromFlags(fs, "listen-client-urls", "bind-addr", clientTLSInfo)
if err != nil {
log.Fatal(err.Error())
}
// Start a proxy server goroutine for each listen address
for _, u := range lcurls {
l, err := transport.NewListener(u.Host, clientTLSInfo)
if err != nil {
log.Fatal(err)
}
host := u.Host
go func() {
log.Print("Listening for client requests on ", host)
log.Fatal(http.Serve(l, ph))
}()
}
}
// setupCluster sets up the cluster definition for bootstrap or discovery.
func setupCluster() (*etcdserver.Cluster, error) {
set := make(map[string]bool)
fs.Visit(func(f *flag.Flag) {
set[f.Name] = true
})
if set["discovery"] && set["initial-cluster"] {
return nil, fmt.Errorf("both discovery and bootstrap-config are set")
}
apurls, err := pkg.URLsFromFlags(fs, "initial-advertise-peer-urls", "addr", peerTLSInfo)
if err != nil {
return nil, err
}
var cls *etcdserver.Cluster
switch {
case set["discovery"]:
clusterStr := genClusterString(*name, apurls)
cls, err = etcdserver.NewClusterFromString(*durl, clusterStr)
case set["initial-cluster"]:
fallthrough
default:
// We're statically configured, and cluster has appropriately been set.
// Try to configure by indexing the static cluster by name.
cls, err = etcdserver.NewClusterFromString(*initialClusterName, *initialCluster)
}
return cls, err
}
func genClusterString(name string, urls types.URLs) string {
addrs := make([]string, 0)
for _, u := range urls {
addrs = append(addrs, fmt.Sprintf("%v=%v", name, u.String()))
}
return strings.Join(addrs, ",")
etcdmain.Main()
}

View File

@ -0,0 +1,97 @@
package main
import (
"errors"
"flag"
"fmt"
"log"
"path"
etcdserverpb "github.com/coreos/etcd/etcdserver/etcdserverpb"
"github.com/coreos/etcd/migrate"
"github.com/coreos/etcd/pkg/types"
raftpb "github.com/coreos/etcd/raft/raftpb"
"github.com/coreos/etcd/wal"
)
func walDir5(dataDir string) string {
return path.Join(dataDir, "wal")
}
func logFile4(dataDir string) string {
return path.Join(dataDir, "log")
}
func main() {
version := flag.Int("version", 5, "4 or 5")
from := flag.String("data-dir", "", "")
flag.Parse()
if *from == "" {
log.Fatal("Must provide -data-dir flag")
}
var ents []raftpb.Entry
var err error
switch *version {
case 4:
ents, err = dump4(*from)
case 5:
ents, err = dump5(*from)
default:
err = errors.New("value of -version flag must be 4 or 5")
}
if err != nil {
log.Fatalf("Failed decoding log: %v", err)
}
for _, e := range ents {
msg := fmt.Sprintf("%2d %5d: ", e.Term, e.Index)
switch e.Type {
case raftpb.EntryNormal:
msg = fmt.Sprintf("%s norm", msg)
var r etcdserverpb.Request
if err := r.Unmarshal(e.Data); err != nil {
msg = fmt.Sprintf("%s ???", msg)
} else {
msg = fmt.Sprintf("%s %s %s %s", msg, r.Method, r.Path, r.Val)
}
case raftpb.EntryConfChange:
msg = fmt.Sprintf("%s conf", msg)
var r raftpb.ConfChange
if err := r.Unmarshal(e.Data); err != nil {
msg = fmt.Sprintf("%s ???", msg)
} else {
msg = fmt.Sprintf("%s %s %s %s", msg, r.Type, types.ID(r.NodeID), r.Context)
}
}
fmt.Println(msg)
}
}
func dump4(dataDir string) ([]raftpb.Entry, error) {
lf4 := logFile4(dataDir)
ents, err := migrate.DecodeLog4FromFile(lf4)
if err != nil {
return nil, err
}
return migrate.Entries4To5(ents)
}
func dump5(dataDir string) ([]raftpb.Entry, error) {
wd5 := walDir5(dataDir)
if !wal.Exist(wd5) {
return nil, fmt.Errorf("No wal exists at %s", wd5)
}
w, err := wal.OpenAtIndex(wd5, 0)
if err != nil {
return nil, err
}
defer w.Close()
_, _, ents, err := w.ReadAll()
return ents, err
}

View File

@ -0,0 +1,23 @@
package main
import (
"flag"
"log"
"github.com/coreos/etcd/migrate"
)
func main() {
from := flag.String("data-dir", "", "etcd v0.4 data-dir")
name := flag.String("name", "", "etcd node name")
flag.Parse()
if *from == "" {
log.Fatal("Must provide -data-dir flag")
}
err := migrate.Migrate4To5(*from, *name)
if err != nil {
log.Fatalf("Failed migrating data-dir: %v", err)
}
}

39
migrate/config.go Normal file
View File

@ -0,0 +1,39 @@
package migrate
import (
"encoding/json"
"io/ioutil"
"github.com/coreos/etcd/raft/raftpb"
)
type Config4 struct {
CommitIndex uint64 `json:"commitIndex"`
Peers []struct {
Name string `json:"name"`
ConnectionString string `json:"connectionString"`
} `json:"peers"`
}
func (c *Config4) HardState5() raftpb.HardState {
return raftpb.HardState{
Commit: c.CommitIndex,
Term: 0,
Vote: 0,
}
}
func DecodeConfig4FromFile(cfgPath string) (*Config4, error) {
b, err := ioutil.ReadFile(cfgPath)
if err != nil {
return nil, err
}
conf := &Config4{}
if err = json.Unmarshal(b, conf); err != nil {
return nil, err
}
return conf, nil
}

168
migrate/etcd4.go Normal file
View File

@ -0,0 +1,168 @@
package migrate
import (
"fmt"
"log"
"os"
"path"
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
"github.com/coreos/etcd/pkg/pbutil"
raftpb "github.com/coreos/etcd/raft/raftpb"
"github.com/coreos/etcd/snap"
"github.com/coreos/etcd/wal"
)
func snapDir4(dataDir string) string {
return path.Join(dataDir, "snapshot")
}
func logFile4(dataDir string) string {
return path.Join(dataDir, "log")
}
func cfgFile4(dataDir string) string {
return path.Join(dataDir, "conf")
}
func snapDir5(dataDir string) string {
return path.Join(dataDir, "snap")
}
func walDir5(dataDir string) string {
return path.Join(dataDir, "wal")
}
func Migrate4To5(dataDir string, name string) error {
// prep new directories
sd5 := snapDir5(dataDir)
if err := os.MkdirAll(sd5, 0700); err != nil {
return fmt.Errorf("failed creating snapshot directory %s: %v", sd5, err)
}
// read v0.4 data
snap4, err := DecodeLatestSnapshot4FromDir(snapDir4(dataDir))
if err != nil {
return err
}
cfg4, err := DecodeConfig4FromFile(cfgFile4(dataDir))
if err != nil {
return err
}
ents4, err := DecodeLog4FromFile(logFile4(dataDir))
if err != nil {
return err
}
nodeIDs := ents4.NodeIDs()
nodeID := GuessNodeID(nodeIDs, snap4, cfg4, name)
if nodeID == 0 {
return fmt.Errorf("Couldn't figure out the node ID from the log or flags, cannot convert")
}
metadata := pbutil.MustMarshal(&pb.Metadata{NodeID: nodeID, ClusterID: 0x04add5})
wd5 := walDir5(dataDir)
w, err := wal.Create(wd5, metadata)
if err != nil {
return fmt.Errorf("failed initializing wal at %s: %v", wd5, err)
}
defer w.Close()
// transform v0.4 data
var snap5 *raftpb.Snapshot
if snap4 == nil {
log.Printf("No snapshot found")
} else {
log.Printf("Found snapshot: lastIndex=%d", snap4.LastIndex)
snap5 = snap4.Snapshot5()
}
st5 := cfg4.HardState5()
// If we've got the most recent snapshot, we can use it's committed index. Still likely less than the current actual index, but worth it for the replay.
if snap5 != nil {
st5.Commit = snap5.Index
}
ents5, err := Entries4To5(ents4)
if err != nil {
return err
}
ents5Len := len(ents5)
log.Printf("Found %d log entries: firstIndex=%d lastIndex=%d", ents5Len, ents5[0].Index, ents5[ents5Len-1].Index)
// explicitly prepend an empty entry as the WAL code expects it
ents5 = append(make([]raftpb.Entry, 1), ents5...)
if err = w.Save(st5, ents5); err != nil {
return err
}
log.Printf("Log migration successful")
// migrate snapshot (if necessary) and logs
if snap5 != nil {
ss := snap.New(sd5)
if err := ss.SaveSnap(*snap5); err != nil {
return err
}
log.Printf("Snapshot migration successful")
}
return nil
}
func GuessNodeID(nodes map[string]uint64, snap4 *Snapshot4, cfg *Config4, name string) uint64 {
var snapNodes map[string]uint64
if snap4 != nil {
snapNodes = snap4.GetNodesFromStore()
}
// First, use the flag, if set.
if name != "" {
log.Printf("Using suggested name %s", name)
if val, ok := nodes[name]; ok {
log.Printf("Found ID %d", val)
return val
}
if snapNodes != nil {
if val, ok := snapNodes[name]; ok {
log.Printf("Found ID %d", val)
return val
}
}
log.Printf("Name not found, autodetecting...")
}
// Next, look at the snapshot peers, if that exists.
if snap4 != nil {
//snapNodes := make(map[string]uint64)
//for _, p := range snap4.Peers {
//m := generateNodeMember(p.Name, p.ConnectionString, "")
//snapNodes[p.Name] = uint64(m.ID)
//}
for _, p := range cfg.Peers {
log.Printf(p.Name)
delete(snapNodes, p.Name)
}
if len(snapNodes) == 1 {
for name, id := range nodes {
log.Printf("Autodetected from snapshot: name %s", name)
return id
}
}
}
// Then, try and deduce from the log.
for _, p := range cfg.Peers {
delete(nodes, p.Name)
}
if len(nodes) == 1 {
for name, id := range nodes {
log.Printf("Autodetected name %s", name)
return id
}
}
return 0
}

View File

@ -0,0 +1,552 @@
// Code generated by protoc-gen-gogo.
// source: log_entry.proto
// DO NOT EDIT!
package protobuf
import proto "github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto"
import json "encoding/json"
import math "math"
// discarding unused import gogoproto "code.google.com/p/gogoprotobuf/gogoproto/gogo.pb"
import io "io"
import code_google_com_p_gogoprotobuf_proto "github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto"
import fmt "fmt"
import strings "strings"
import reflect "reflect"
import fmt1 "fmt"
import strings1 "strings"
import code_google_com_p_gogoprotobuf_proto1 "github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto"
import sort "sort"
import strconv "strconv"
import reflect1 "reflect"
import fmt2 "fmt"
import bytes "bytes"
// Reference proto, json, and math imports to suppress error if they are not otherwise used.
var _ = proto.Marshal
var _ = &json.SyntaxError{}
var _ = math.Inf
type LogEntry struct {
Index *uint64 `protobuf:"varint,1,req" json:"Index,omitempty"`
Term *uint64 `protobuf:"varint,2,req" json:"Term,omitempty"`
CommandName *string `protobuf:"bytes,3,req" json:"CommandName,omitempty"`
Command []byte `protobuf:"bytes,4,opt" json:"Command,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *LogEntry) Reset() { *m = LogEntry{} }
func (*LogEntry) ProtoMessage() {}
func (m *LogEntry) GetIndex() uint64 {
if m != nil && m.Index != nil {
return *m.Index
}
return 0
}
func (m *LogEntry) GetTerm() uint64 {
if m != nil && m.Term != nil {
return *m.Term
}
return 0
}
func (m *LogEntry) GetCommandName() string {
if m != nil && m.CommandName != nil {
return *m.CommandName
}
return ""
}
func (m *LogEntry) GetCommand() []byte {
if m != nil {
return m.Command
}
return nil
}
func init() {
}
func (m *LogEntry) Unmarshal(data []byte) error {
l := len(data)
index := 0
for index < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if index >= l {
return io.ErrUnexpectedEOF
}
b := data[index]
index++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
switch fieldNum {
case 1:
if wireType != 0 {
return code_google_com_p_gogoprotobuf_proto.ErrWrongType
}
var v uint64
for shift := uint(0); ; shift += 7 {
if index >= l {
return io.ErrUnexpectedEOF
}
b := data[index]
index++
v |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
m.Index = &v
case 2:
if wireType != 0 {
return code_google_com_p_gogoprotobuf_proto.ErrWrongType
}
var v uint64
for shift := uint(0); ; shift += 7 {
if index >= l {
return io.ErrUnexpectedEOF
}
b := data[index]
index++
v |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
m.Term = &v
case 3:
if wireType != 2 {
return code_google_com_p_gogoprotobuf_proto.ErrWrongType
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if index >= l {
return io.ErrUnexpectedEOF
}
b := data[index]
index++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
postIndex := index + int(stringLen)
if postIndex > l {
return io.ErrUnexpectedEOF
}
s := string(data[index:postIndex])
m.CommandName = &s
index = postIndex
case 4:
if wireType != 2 {
return code_google_com_p_gogoprotobuf_proto.ErrWrongType
}
var byteLen int
for shift := uint(0); ; shift += 7 {
if index >= l {
return io.ErrUnexpectedEOF
}
b := data[index]
index++
byteLen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
postIndex := index + byteLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Command = append(m.Command, data[index:postIndex]...)
index = postIndex
default:
var sizeOfWire int
for {
sizeOfWire++
wire >>= 7
if wire == 0 {
break
}
}
index -= sizeOfWire
skippy, err := code_google_com_p_gogoprotobuf_proto.Skip(data[index:])
if err != nil {
return err
}
if (index + skippy) > l {
return io.ErrUnexpectedEOF
}
m.XXX_unrecognized = append(m.XXX_unrecognized, data[index:index+skippy]...)
index += skippy
}
}
return nil
}
func (this *LogEntry) String() string {
if this == nil {
return "nil"
}
s := strings.Join([]string{`&LogEntry{`,
`Index:` + valueToStringLogEntry(this.Index) + `,`,
`Term:` + valueToStringLogEntry(this.Term) + `,`,
`CommandName:` + valueToStringLogEntry(this.CommandName) + `,`,
`Command:` + valueToStringLogEntry(this.Command) + `,`,
`XXX_unrecognized:` + fmt.Sprintf("%v", this.XXX_unrecognized) + `,`,
`}`,
}, "")
return s
}
func valueToStringLogEntry(v interface{}) string {
rv := reflect.ValueOf(v)
if rv.IsNil() {
return "nil"
}
pv := reflect.Indirect(rv).Interface()
return fmt.Sprintf("*%v", pv)
}
func (m *LogEntry) Size() (n int) {
var l int
_ = l
if m.Index != nil {
n += 1 + sovLogEntry(uint64(*m.Index))
}
if m.Term != nil {
n += 1 + sovLogEntry(uint64(*m.Term))
}
if m.CommandName != nil {
l = len(*m.CommandName)
n += 1 + l + sovLogEntry(uint64(l))
}
if m.Command != nil {
l = len(m.Command)
n += 1 + l + sovLogEntry(uint64(l))
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
return n
}
func sovLogEntry(x uint64) (n int) {
for {
n++
x >>= 7
if x == 0 {
break
}
}
return n
}
func sozLogEntry(x uint64) (n int) {
return sovLogEntry(uint64((x << 1) ^ uint64((int64(x) >> 63))))
}
func NewPopulatedLogEntry(r randyLogEntry, easy bool) *LogEntry {
this := &LogEntry{}
v1 := uint64(r.Uint32())
this.Index = &v1
v2 := uint64(r.Uint32())
this.Term = &v2
v3 := randStringLogEntry(r)
this.CommandName = &v3
if r.Intn(10) != 0 {
v4 := r.Intn(100)
this.Command = make([]byte, v4)
for i := 0; i < v4; i++ {
this.Command[i] = byte(r.Intn(256))
}
}
if !easy && r.Intn(10) != 0 {
this.XXX_unrecognized = randUnrecognizedLogEntry(r, 5)
}
return this
}
type randyLogEntry interface {
Float32() float32
Float64() float64
Int63() int64
Int31() int32
Uint32() uint32
Intn(n int) int
}
func randUTF8RuneLogEntry(r randyLogEntry) rune {
res := rune(r.Uint32() % 1112064)
if 55296 <= res {
res += 2047
}
return res
}
func randStringLogEntry(r randyLogEntry) string {
v5 := r.Intn(100)
tmps := make([]rune, v5)
for i := 0; i < v5; i++ {
tmps[i] = randUTF8RuneLogEntry(r)
}
return string(tmps)
}
func randUnrecognizedLogEntry(r randyLogEntry, maxFieldNumber int) (data []byte) {
l := r.Intn(5)
for i := 0; i < l; i++ {
wire := r.Intn(4)
if wire == 3 {
wire = 5
}
fieldNumber := maxFieldNumber + r.Intn(100)
data = randFieldLogEntry(data, r, fieldNumber, wire)
}
return data
}
func randFieldLogEntry(data []byte, r randyLogEntry, fieldNumber int, wire int) []byte {
key := uint32(fieldNumber)<<3 | uint32(wire)
switch wire {
case 0:
data = encodeVarintPopulateLogEntry(data, uint64(key))
data = encodeVarintPopulateLogEntry(data, uint64(r.Int63()))
case 1:
data = encodeVarintPopulateLogEntry(data, uint64(key))
data = append(data, byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)))
case 2:
data = encodeVarintPopulateLogEntry(data, uint64(key))
ll := r.Intn(100)
data = encodeVarintPopulateLogEntry(data, uint64(ll))
for j := 0; j < ll; j++ {
data = append(data, byte(r.Intn(256)))
}
default:
data = encodeVarintPopulateLogEntry(data, uint64(key))
data = append(data, byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)))
}
return data
}
func encodeVarintPopulateLogEntry(data []byte, v uint64) []byte {
for v >= 1<<7 {
data = append(data, uint8(uint64(v)&0x7f|0x80))
v >>= 7
}
data = append(data, uint8(v))
return data
}
func (m *LogEntry) Marshal() (data []byte, err error) {
size := m.Size()
data = make([]byte, size)
n, err := m.MarshalTo(data)
if err != nil {
return nil, err
}
return data[:n], nil
}
func (m *LogEntry) MarshalTo(data []byte) (n int, err error) {
var i int
_ = i
var l int
_ = l
if m.Index != nil {
data[i] = 0x8
i++
i = encodeVarintLogEntry(data, i, uint64(*m.Index))
}
if m.Term != nil {
data[i] = 0x10
i++
i = encodeVarintLogEntry(data, i, uint64(*m.Term))
}
if m.CommandName != nil {
data[i] = 0x1a
i++
i = encodeVarintLogEntry(data, i, uint64(len(*m.CommandName)))
i += copy(data[i:], *m.CommandName)
}
if m.Command != nil {
data[i] = 0x22
i++
i = encodeVarintLogEntry(data, i, uint64(len(m.Command)))
i += copy(data[i:], m.Command)
}
if m.XXX_unrecognized != nil {
i += copy(data[i:], m.XXX_unrecognized)
}
return i, nil
}
func encodeFixed64LogEntry(data []byte, offset int, v uint64) int {
data[offset] = uint8(v)
data[offset+1] = uint8(v >> 8)
data[offset+2] = uint8(v >> 16)
data[offset+3] = uint8(v >> 24)
data[offset+4] = uint8(v >> 32)
data[offset+5] = uint8(v >> 40)
data[offset+6] = uint8(v >> 48)
data[offset+7] = uint8(v >> 56)
return offset + 8
}
func encodeFixed32LogEntry(data []byte, offset int, v uint32) int {
data[offset] = uint8(v)
data[offset+1] = uint8(v >> 8)
data[offset+2] = uint8(v >> 16)
data[offset+3] = uint8(v >> 24)
return offset + 4
}
func encodeVarintLogEntry(data []byte, offset int, v uint64) int {
for v >= 1<<7 {
data[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
data[offset] = uint8(v)
return offset + 1
}
func (this *LogEntry) GoString() string {
if this == nil {
return "nil"
}
s := strings1.Join([]string{`&protobuf.LogEntry{` + `Index:` + valueToGoStringLogEntry(this.Index, "uint64"), `Term:` + valueToGoStringLogEntry(this.Term, "uint64"), `CommandName:` + valueToGoStringLogEntry(this.CommandName, "string"), `Command:` + valueToGoStringLogEntry(this.Command, "byte"), `XXX_unrecognized:` + fmt1.Sprintf("%#v", this.XXX_unrecognized) + `}`}, ", ")
return s
}
func valueToGoStringLogEntry(v interface{}, typ string) string {
rv := reflect1.ValueOf(v)
if rv.IsNil() {
return "nil"
}
pv := reflect1.Indirect(rv).Interface()
return fmt1.Sprintf("func(v %v) *%v { return &v } ( %#v )", typ, typ, pv)
}
func extensionToGoStringLogEntry(e map[int32]code_google_com_p_gogoprotobuf_proto1.Extension) string {
if e == nil {
return "nil"
}
s := "map[int32]proto.Extension{"
keys := make([]int, 0, len(e))
for k := range e {
keys = append(keys, int(k))
}
sort.Ints(keys)
ss := []string{}
for _, k := range keys {
ss = append(ss, strconv.Itoa(k)+": "+e[int32(k)].GoString())
}
s += strings1.Join(ss, ",") + "}"
return s
}
func (this *LogEntry) VerboseEqual(that interface{}) error {
if that == nil {
if this == nil {
return nil
}
return fmt2.Errorf("that == nil && this != nil")
}
that1, ok := that.(*LogEntry)
if !ok {
return fmt2.Errorf("that is not of type *LogEntry")
}
if that1 == nil {
if this == nil {
return nil
}
return fmt2.Errorf("that is type *LogEntry but is nil && this != nil")
} else if this == nil {
return fmt2.Errorf("that is type *LogEntrybut is not nil && this == nil")
}
if this.Index != nil && that1.Index != nil {
if *this.Index != *that1.Index {
return fmt2.Errorf("Index this(%v) Not Equal that(%v)", *this.Index, *that1.Index)
}
} else if this.Index != nil {
return fmt2.Errorf("this.Index == nil && that.Index != nil")
} else if that1.Index != nil {
return fmt2.Errorf("Index this(%v) Not Equal that(%v)", this.Index, that1.Index)
}
if this.Term != nil && that1.Term != nil {
if *this.Term != *that1.Term {
return fmt2.Errorf("Term this(%v) Not Equal that(%v)", *this.Term, *that1.Term)
}
} else if this.Term != nil {
return fmt2.Errorf("this.Term == nil && that.Term != nil")
} else if that1.Term != nil {
return fmt2.Errorf("Term this(%v) Not Equal that(%v)", this.Term, that1.Term)
}
if this.CommandName != nil && that1.CommandName != nil {
if *this.CommandName != *that1.CommandName {
return fmt2.Errorf("CommandName this(%v) Not Equal that(%v)", *this.CommandName, *that1.CommandName)
}
} else if this.CommandName != nil {
return fmt2.Errorf("this.CommandName == nil && that.CommandName != nil")
} else if that1.CommandName != nil {
return fmt2.Errorf("CommandName this(%v) Not Equal that(%v)", this.CommandName, that1.CommandName)
}
if !bytes.Equal(this.Command, that1.Command) {
return fmt2.Errorf("Command this(%v) Not Equal that(%v)", this.Command, that1.Command)
}
if !bytes.Equal(this.XXX_unrecognized, that1.XXX_unrecognized) {
return fmt2.Errorf("XXX_unrecognized this(%v) Not Equal that(%v)", this.XXX_unrecognized, that1.XXX_unrecognized)
}
return nil
}
func (this *LogEntry) Equal(that interface{}) bool {
if that == nil {
if this == nil {
return true
}
return false
}
that1, ok := that.(*LogEntry)
if !ok {
return false
}
if that1 == nil {
if this == nil {
return true
}
return false
} else if this == nil {
return false
}
if this.Index != nil && that1.Index != nil {
if *this.Index != *that1.Index {
return false
}
} else if this.Index != nil {
return false
} else if that1.Index != nil {
return false
}
if this.Term != nil && that1.Term != nil {
if *this.Term != *that1.Term {
return false
}
} else if this.Term != nil {
return false
} else if that1.Term != nil {
return false
}
if this.CommandName != nil && that1.CommandName != nil {
if *this.CommandName != *that1.CommandName {
return false
}
} else if this.CommandName != nil {
return false
} else if that1.CommandName != nil {
return false
}
if !bytes.Equal(this.Command, that1.Command) {
return false
}
if !bytes.Equal(this.XXX_unrecognized, that1.XXX_unrecognized) {
return false
}
return true
}

View File

@ -0,0 +1,22 @@
package protobuf;
import "code.google.com/p/gogoprotobuf/gogoproto/gogo.proto";
option (gogoproto.gostring_all) = true;
option (gogoproto.equal_all) = true;
option (gogoproto.verbose_equal_all) = true;
option (gogoproto.goproto_stringer_all) = false;
option (gogoproto.stringer_all) = true;
option (gogoproto.populate_all) = true;
option (gogoproto.testgen_all) = true;
option (gogoproto.benchgen_all) = true;
option (gogoproto.marshaler_all) = true;
option (gogoproto.sizer_all) = true;
option (gogoproto.unmarshaler_all) = true;
message LogEntry {
required uint64 Index=1;
required uint64 Term=2;
required string CommandName=3;
optional bytes Command=4; // for nop-command
}

Some files were not shown because too many files have changed in this diff Show More