Compare commits
1023 Commits
v0.5.0-alp
...
v2.0.2
Author | SHA1 | Date | |
---|---|---|---|
409daceb73 | |||
c6cc276ef0 | |||
cd50f0e058 | |||
fade9b6065 | |||
590205b8c0 | |||
163f0f09f6 | |||
20497f1f85 | |||
4a0887ef7a | |||
161b1d2e2e | |||
71bed48916 | |||
fe1d9565c2 | |||
a81e147d8f | |||
24b953a55d | |||
54bef0d2cd | |||
d0677a24dd | |||
bdc8cc1f54 | |||
b036c384a5 | |||
df2a689d1c | |||
f97a263a95 | |||
96ea0ff45c | |||
58112c4d2d | |||
d74e74d320 | |||
9834875d35 | |||
9460b6efda | |||
57dd8c18cc | |||
9ec8ea47c8 | |||
6e1aecfc6f | |||
96fde55a0f | |||
84dac75ed5 | |||
1481ef9a5e | |||
fa66055f66 | |||
085b608de9 | |||
3c9c4c4afa | |||
279b216f9a | |||
8788c74b48 | |||
8d663078bf | |||
0242faa838 | |||
9c850b7182 | |||
db88d9764c | |||
7bbdad9068 | |||
af00536d71 | |||
c990099008 | |||
65cd0051fe | |||
c94db98177 | |||
d423946fa4 | |||
e2feafc741 | |||
c8b5d47f24 | |||
d71be31e68 | |||
9776e6d082 | |||
766e0ad901 | |||
a387e2a989 | |||
26dc5904a5 | |||
136e0b6e26 | |||
599e821309 | |||
1ce7f6e0d0 | |||
860a8c8717 | |||
a4c4027dc7 | |||
3ac0298bd0 | |||
f13c7872d5 | |||
38038e476a | |||
871e92ef73 | |||
58cb9a3b76 | |||
a0f8aa1add | |||
5c6ce0c18d | |||
378fa46b7d | |||
83edf0d862 | |||
d0205519a8 | |||
fca9805f84 | |||
f109020b94 | |||
81d7eaf17f | |||
2d081bd3b9 | |||
f2f2adc663 | |||
92b329fdb9 | |||
00eaf165a8 | |||
b147a6328d | |||
afb14a3e7a | |||
ce1d7a9fa9 | |||
470be16c04 | |||
fbabcedcc9 | |||
d16c5e1e81 | |||
d65af21b73 | |||
bdcae31638 | |||
ae9f54c132 | |||
a3d0097908 | |||
37e8d608b3 | |||
c66176b538 | |||
b6936a0079 | |||
9961d5ca2b | |||
dc7374c488 | |||
87a8ebd222 | |||
27e5b9a394 | |||
f5afe3cc34 | |||
3ee7a265f6 | |||
d1f9f2f1b7 | |||
894f1aadce | |||
fce80136e3 | |||
ebf9daff74 | |||
ec5a6e8beb | |||
0945e487e7 | |||
a65556abe2 | |||
e966e565c4 | |||
7840d49ae0 | |||
d0af96d558 | |||
fd0c0c9263 | |||
4960324876 | |||
b606078e93 | |||
127fe322a4 | |||
b377110c11 | |||
7167cd6ccd | |||
bff2ccaa22 | |||
553379e82b | |||
67d141a0af | |||
0b2fde38d0 | |||
0c1329ace2 | |||
43f1ccc88c | |||
33d2400063 | |||
4c33d12bf8 | |||
0f2582e0be | |||
a03c906e9d | |||
c530e6fc55 | |||
c5adff4988 | |||
91bd02dce1 | |||
62b0fe50eb | |||
9eaa79a12a | |||
55c1635cee | |||
7f91a35313 | |||
517eb340dd | |||
915c22292f | |||
276c9540b4 | |||
825107629a | |||
8c932ff719 | |||
f0c9a54edb | |||
08b34a3f5b | |||
78f70137ea | |||
4c55e8a7c0 | |||
4427b889df | |||
37f8e2d5e0 | |||
f8ce5996b0 | |||
9c7f66c5d9 | |||
033e7d1db9 | |||
a6661201c5 | |||
f1ed69e883 | |||
200d4d6f41 | |||
f1dcefa834 | |||
b2acb12c8e | |||
93e4880ae6 | |||
d5f6b97b20 | |||
6b304ce605 | |||
2120af8cfc | |||
f16ff64949 | |||
c658e9a3e9 | |||
a77bf97c14 | |||
7d33a2686c | |||
e7d539e4ce | |||
8c3a6508e9 | |||
59214978a2 | |||
c2fa486920 | |||
4409e88358 | |||
cd9d5573d4 | |||
891cb62b81 | |||
99821579bf | |||
e73d442e32 | |||
88704c70e7 | |||
c104ca89c2 | |||
7c7d78a11f | |||
003b97a60f | |||
b34936b097 | |||
0eaaad0e48 | |||
3ec91ead88 | |||
a97f331a0e | |||
4735324403 | |||
37dde76cd5 | |||
c36aa3be6e | |||
973f79e1c9 | |||
4b6fa2d24f | |||
84ceefbffc | |||
1a6161d08a | |||
6bd8c435f9 | |||
b28bad3b4a | |||
d380be8fa1 | |||
e8698b0e42 | |||
295fa1ca99 | |||
276a4abac0 | |||
e9235002f7 | |||
ae7153bf38 | |||
68fdd70580 | |||
190fd446f9 | |||
886a6a6194 | |||
9b4e72dd3a | |||
c4e4a9711f | |||
cb1903ddcb | |||
5568d590ef | |||
b0a4637ebd | |||
78a7e0e551 | |||
e41d1e2064 | |||
28feb073b5 | |||
e109836fef | |||
5e4cc73991 | |||
245e23ca47 | |||
6bda827b67 | |||
f1c6771726 | |||
1146cb9461 | |||
467ce1e730 | |||
507021d884 | |||
8e8552b2ad | |||
2b94119fb1 | |||
41a30ff21b | |||
dde0e6d05a | |||
4df434334f | |||
1d02493a53 | |||
d117a12d02 | |||
23406dc2ee | |||
e5daa2c6ca | |||
5de4a464f7 | |||
2e1c36cdd9 | |||
238b17fee0 | |||
9972e62d94 | |||
232927d9dc | |||
4510993b67 | |||
8e6297780b | |||
3e268467c8 | |||
733b655bfa | |||
1b9ccfc66f | |||
a318112c7a | |||
89d95539cf | |||
07a69430c1 | |||
a83aba12f0 | |||
c212a511fe | |||
c68f5c2059 | |||
51005d32c7 | |||
b9544d32b6 | |||
586a5e463e | |||
42ae6e5f5b | |||
c8994cff37 | |||
0015372939 | |||
2e776117f8 | |||
dc6aef0d02 | |||
9010e8a2c4 | |||
7e67fd13f6 | |||
e01ae2c083 | |||
50395a53fb | |||
60d6c34c28 | |||
2d8f5e1250 | |||
e3b2f08bd0 | |||
aec2eef498 | |||
dfb66ab8ce | |||
f1368a00fb | |||
3577ed69a2 | |||
e688471c28 | |||
5d99024fea | |||
05e591f805 | |||
9bdc343b7c | |||
270e67db84 | |||
50c179ec1c | |||
f08d1090d0 | |||
9532810f76 | |||
92f013393c | |||
096cbbcbf6 | |||
fcbe7fdc83 | |||
d225690b08 | |||
80c174255a | |||
bca1e5aea6 | |||
9132098960 | |||
930156c18a | |||
f98d0ef817 | |||
6b237416e1 | |||
1d1a4754a7 | |||
6460e49a33 | |||
78bb207bac | |||
84f62f21ee | |||
945c5dd558 | |||
a15f39e6a2 | |||
02085153c9 | |||
7f1c630a0b | |||
47113d776e | |||
0afbca4090 | |||
cbdb0266e9 | |||
1ebad5e42c | |||
7a2fa39e52 | |||
6d288fa9e9 | |||
88a9eedf06 | |||
0c55cfb21e | |||
1aa8f1eee6 | |||
6b8667152b | |||
8ac184ad52 | |||
d4a145ab0d | |||
66d9f28926 | |||
cb5bff5b05 | |||
4938e6bff5 | |||
3319f716d9 | |||
15be030aaa | |||
4dd00be365 | |||
b44d7f84c4 | |||
d719bc0e29 | |||
bc6f062008 | |||
2a83e350b1 | |||
6e727625b9 | |||
0632dc2023 | |||
51ffc88096 | |||
5bb43b5276 | |||
41f6137261 | |||
27d47977d9 | |||
ac6cd03365 | |||
921ce4c25b | |||
35b907ac58 | |||
95a661251d | |||
fe53ffd74d | |||
5a867611ca | |||
1f8eef3b3b | |||
2292da15d6 | |||
04003a01ba | |||
4974bb0349 | |||
9de4e36b6a | |||
dc863459f8 | |||
17401994c0 | |||
8b0c7bf652 | |||
7273a861a6 | |||
803c38f448 | |||
2c21ac656b | |||
c3d2f5eea0 | |||
8088440e1d | |||
241a474935 | |||
d2c7a7e5cb | |||
2193b70fb3 | |||
bbfed7e6ef | |||
3f8a85ed7e | |||
a92bd1d165 | |||
f79b9042ab | |||
3748088b96 | |||
6ccaadc95d | |||
05c921229e | |||
c712dd682a | |||
a14d13f724 | |||
7c8b9c0203 | |||
152676f43a | |||
dc6ba914c8 | |||
5bb8eeb5cf | |||
4463f5c4b3 | |||
cea29fe158 | |||
5f11d5a0d0 | |||
e1ee335c3a | |||
08f839e32c | |||
0630f42e7a | |||
1535596252 | |||
3dcd66459d | |||
e056e96ad5 | |||
69444b6bba | |||
60d25635c4 | |||
78b51d3f2f | |||
9c84443f42 | |||
6dc3af5da4 | |||
7a5bf53222 | |||
7ce0fc782e | |||
ef0a66bb0a | |||
52fc768c28 | |||
00b4e919d0 | |||
fc96a9e4a7 | |||
f43bc809b9 | |||
7d866dbc44 | |||
b9d228b0fa | |||
08e9c25ea5 | |||
7ec2e382bd | |||
705ec45083 | |||
289b070aa5 | |||
08f74cf68f | |||
2b9f388a91 | |||
2dbdf87f86 | |||
d87ee9819b | |||
ee7f23d0d5 | |||
841368c8e3 | |||
3abe71dff5 | |||
f143948fdd | |||
0fa754d90e | |||
39786c4bea | |||
fb4781920c | |||
1a5afaec7a | |||
4f2d35679e | |||
8fc17147ef | |||
1f98d15535 | |||
f78bf987c8 | |||
896bac1f76 | |||
f8752f9879 | |||
021fc140b8 | |||
1ec98cb795 | |||
2311463935 | |||
f1890ea48b | |||
6295dfba5a | |||
4e6cbc937e | |||
1d859790e5 | |||
a5923e5b00 | |||
9f84be81a2 | |||
977c74069c | |||
a0d72fb00c | |||
2dfcf053d4 | |||
5a99e969b7 | |||
7f733ad68b | |||
0a40e18f68 | |||
8b8ebb96c4 | |||
88767d913d | |||
7588e4ddfb | |||
d375b67a50 | |||
221abdcb3b | |||
fa35363f74 | |||
fc70aa27d2 | |||
04d9f848a7 | |||
fdad6630ea | |||
35a772753c | |||
aea87bc88d | |||
ce6f606766 | |||
a000e97eea | |||
2d76e5e273 | |||
f0b9ad3863 | |||
0a14927823 | |||
910198d117 | |||
722247a752 | |||
c27c288bef | |||
04522baeee | |||
af4272848d | |||
43bb6cf038 | |||
8ece28d4f7 | |||
5369fb1c4f | |||
044e35b814 | |||
e9b06416de | |||
9dc5b5a7e8 | |||
e3dbfefbe0 | |||
0ea8c0929e | |||
502396edd5 | |||
6b73a72d42 | |||
53bf7e4b5e | |||
f538cba272 | |||
ea94d19147 | |||
b90693ccae | |||
dcf34c0ab4 | |||
ceb077424d | |||
d07434f99e | |||
cb6983cbb1 | |||
c586d5012c | |||
d86603840d | |||
e40a53b534 | |||
3f64c677e1 | |||
b8ab2b0b5c | |||
3cc4cdd363 | |||
c620238257 | |||
f265afa8ac | |||
bee3103931 | |||
fa195dae39 | |||
fe4abc40ce | |||
3794f6ab88 | |||
22e56ae9c6 | |||
b7cd72b593 | |||
1f0d43250f | |||
97025bf5f1 | |||
ae1f3d5640 | |||
4724cbbe2c | |||
935f7128a9 | |||
b555843e0f | |||
d5d034ecd2 | |||
f054dd9d6f | |||
773f112a5d | |||
ec777ebd28 | |||
3a83ab1b71 | |||
d381889a84 | |||
d9b21c79d4 | |||
2e9f6f70d6 | |||
2c2e032155 | |||
b1d7597a9e | |||
f21cc09d83 | |||
b26856b603 | |||
5f16fab541 | |||
cf7690cb51 | |||
421fe128c3 | |||
0416503124 | |||
c26542b7f2 | |||
2d4ca7e448 | |||
836ccabad2 | |||
5b2ec31a15 | |||
7171410422 | |||
f2863e5279 | |||
123b3dd64c | |||
89cba625d6 | |||
e89cc25c50 | |||
8aba4caa72 | |||
3867c72c8a | |||
a729c829a5 | |||
fa247d09cc | |||
7e242acf04 | |||
96de9776b7 | |||
d4dcd39b83 | |||
07e876592b | |||
e7f5b14f1b | |||
4777cba995 | |||
2593914973 | |||
ce0b0ef418 | |||
a852936a59 | |||
4094812b39 | |||
13f3158728 | |||
e4c0f5c1a8 | |||
a5efbf826d | |||
0472ddf05f | |||
29d7a2a558 | |||
4804c45e14 | |||
22dd3b039c | |||
7317834417 | |||
20e2c8f431 | |||
e981dda287 | |||
9c8f5c9535 | |||
325e768c7b | |||
13814c9d7d | |||
7e06d85651 | |||
ba45637ba3 | |||
099f4f10ea | |||
dfbe16445d | |||
8ead428e76 | |||
f73d059d80 | |||
25313b1210 | |||
d52c66ad42 | |||
a5ec7040e0 | |||
62ed1de10d | |||
71f3b80fbe | |||
8c338ffcc7 | |||
b4773a15b2 | |||
6cb7f2d9e9 | |||
576aba700e | |||
8065206839 | |||
cd878ea9a9 | |||
091cc237e3 | |||
069578c29c | |||
31b9f712ba | |||
706b6f96b3 | |||
e83e2bff92 | |||
2b519c90b9 | |||
f10f7802be | |||
f63d51e40f | |||
b24d546bd0 | |||
ea4d645a83 | |||
3d91faf85a | |||
6bfa5d409e | |||
793cb095b0 | |||
c03da80330 | |||
6409a8bf0d | |||
15aed05071 | |||
771ff4589d | |||
a16dd7ea67 | |||
b9bf957c6d | |||
abb72f60bc | |||
182c30a41a | |||
1b43f60e0e | |||
6d046d94d6 | |||
933a9f3e3f | |||
a1f648e5db | |||
1d1c2ff834 | |||
29982dc935 | |||
a7bc03b42b | |||
88e2fab572 | |||
197e6b1b20 | |||
3de2ab2c04 | |||
ca32a5fe9b | |||
356146b5a0 | |||
151f043414 | |||
af0f34c595 | |||
a47690dd30 | |||
4ebd3a0b10 | |||
72d2597f3d | |||
149389cbfa | |||
719a634fdc | |||
e344774c10 | |||
34a468de36 | |||
ddd9cb7345 | |||
2c0d323318 | |||
2caf4f5f22 | |||
a426b310fc | |||
06a5892a18 | |||
a36d07047a | |||
8074a5b5a4 | |||
37ab463e86 | |||
7703d4942c | |||
be60c88603 | |||
256e51874e | |||
63ed202db6 | |||
48f75ca645 | |||
058356d9bd | |||
98ebfa3468 | |||
70bd26a652 | |||
16f9fd63ab | |||
23b32a6cbe | |||
7305451d43 | |||
38768e5396 | |||
7e01c02abb | |||
b3841afcc3 | |||
e07e2ac124 | |||
3209fd544b | |||
79014556e9 | |||
2f5b748a90 | |||
1c7b9317a9 | |||
551a56fb98 | |||
b7ca56e3c8 | |||
3cadaca1a3 | |||
411063e14f | |||
99c2e905e2 | |||
788d1e59a2 | |||
70b501d17c | |||
6692a8060e | |||
51de095d2c | |||
f02eae934b | |||
57b076f710 | |||
2b7af3d101 | |||
aa61009560 | |||
fa292391d8 | |||
f34fe6e4ae | |||
cb74b6812b | |||
312db7f0f3 | |||
7a1d147795 | |||
19ccdbee18 | |||
7beac083ff | |||
d3db010190 | |||
92d4112feb | |||
649176934a | |||
7c47decd19 | |||
bc5acd3c42 | |||
3c0fbe285c | |||
d214e87aee | |||
d244e3bf6e | |||
fe0bc4ff36 | |||
746c66b466 | |||
43d6f9f964 | |||
35cf7b5a31 | |||
7929e46dd8 | |||
8a626257c7 | |||
416b799ecf | |||
00ce0702b9 | |||
7358ef21a2 | |||
e03cf6d488 | |||
670d98ec72 | |||
0f070f3d2d | |||
b2d686495c | |||
66252c7d62 | |||
488f508505 | |||
ab2a40ea37 | |||
732cfa1ad6 | |||
e23f9e76d1 | |||
d01d6119e5 | |||
39e6631447 | |||
7614aa53bf | |||
006da2f8a0 | |||
d5ceb26408 | |||
8de98d4903 | |||
9bd1786fe4 | |||
9df0e7715d | |||
01cbcce8ba | |||
74d8c7f457 | |||
7e6e305c4f | |||
a13d5a70ff | |||
dd57c1f189 | |||
4b43824be9 | |||
1a5333e51d | |||
07ca99f4d6 | |||
aa2721e31d | |||
c46e30412e | |||
dbb6a75e3f | |||
c67b937d62 | |||
9974bf0291 | |||
8aa89dba3d | |||
8ee1bf31d6 | |||
e466126510 | |||
e17bcd8932 | |||
85d0e2f130 | |||
1e0f87df8c | |||
1d01c8aa2d | |||
2c06a1d815 | |||
0d200baf72 | |||
7fcaca6d18 | |||
8670f4012b | |||
239c8dd479 | |||
54e1237271 | |||
1b038da18a | |||
bd9e93eeea | |||
185d37c333 | |||
e3cb3d640b | |||
9455119968 | |||
d67eea4a7d | |||
61ce494386 | |||
65ad1f6ffd | |||
10ebf1a335 | |||
2876c652ab | |||
d69e4dbe6d | |||
453133977d | |||
4b7af29c37 | |||
cfb96de413 | |||
1e797c1e38 | |||
3e55834c38 | |||
ad58122e3c | |||
400e573013 | |||
62a8df304a | |||
e8afdcfe0a | |||
08f156a1de | |||
3dd4c458ca | |||
94190286ff | |||
0a46c70f5d | |||
bc0e72acb9 | |||
f3cef87c69 | |||
6c8e294d20 | |||
bdbafe2cf3 | |||
bb640e326d | |||
c72221a691 | |||
c6cbea451a | |||
35e6df6d0a | |||
da1ff2d2bb | |||
68e79868cc | |||
91bfead9e9 | |||
9ddd8ee539 | |||
03c8881e35 | |||
0d680d0e6b | |||
30690d15d9 | |||
66c30f28d6 | |||
edcdffe11e | |||
264a63be80 | |||
063c5c77a0 | |||
c0fb1c8a00 | |||
5139257b8d | |||
ce82a3e7ad | |||
53fbf0f333 | |||
2d5ccf12ef | |||
d1e7fee3ca | |||
59a0c64e9f | |||
78ea3335bf | |||
d0dd205b0e | |||
aca195f3ad | |||
9d53b94546 | |||
da5538b8c7 | |||
9a728a127a | |||
b29240baf0 | |||
355ee4f393 | |||
b50f331558 | |||
12aaf046d7 | |||
f08df9e0f3 | |||
00df13138e | |||
d2e36a9535 | |||
893fb3b890 | |||
03bacc1984 | |||
4c1fd07311 | |||
e07ef6991c | |||
46ee58c6f0 | |||
f94ff96c69 | |||
bd4cfa2a07 | |||
1a72143ecb | |||
6cac631a0d | |||
04d416291a | |||
1635844ebd | |||
b93d87f17f | |||
a2c568a144 | |||
f24e214ee5 | |||
5dc5f8145c | |||
3fcc011717 | |||
84fbf7aab5 | |||
300c5a2001 | |||
e04e4632b3 | |||
0541f0afa0 | |||
64d9bcabf1 | |||
144db790ae | |||
c26de66262 | |||
ee8fbee7ab | |||
800747e1cf | |||
a817ca705b | |||
fd512ffe6d | |||
7c4b84a6cd | |||
ac5a282003 | |||
8bf71d796e | |||
4e251f8624 | |||
192f200d9e | |||
5ea1f2d96f | |||
c36abeabd1 | |||
b6887e4a83 | |||
77433ff6da | |||
dfaa7290c4 | |||
f6a7f96967 | |||
7d0ffb3f12 | |||
45e96be605 | |||
1f7198855b | |||
6f7fd89ba2 | |||
12dba7d413 | |||
e66bda957b | |||
6a1fe00615 | |||
11f392bdc8 | |||
b5d480f17a | |||
978d0f1ca1 | |||
fb344bc33f | |||
813ff6ba48 | |||
ac907d746b | |||
30dfdb0ea9 | |||
0e8ffe9128 | |||
39eddd8565 | |||
8c4494a39d | |||
9716def94b | |||
d89bf9f215 | |||
32824e053c | |||
8319d4dcbe | |||
d6f40acc86 | |||
b29c512f50 | |||
92096dfdc3 | |||
a551b75d96 | |||
0d18a0f381 | |||
23b5bc0dfe | |||
1e1535e6f9 | |||
4adbd821a3 | |||
04994048bb | |||
ba915ad5a8 | |||
84ecb89774 | |||
0c2b45ddc6 | |||
eb66d2b0eb | |||
2a407dadc0 | |||
634011eb8b | |||
2cedf127d4 | |||
68ab7e69e1 | |||
54b07d7974 | |||
147fd614ce | |||
ec7793557a | |||
b271e88c20 | |||
bc9de47a9a | |||
fc21f299b1 | |||
5cef3d888a | |||
d834324e97 | |||
76a3de9a33 | |||
d1ae276434 | |||
1197c1f965 | |||
3f358b6d5d | |||
45c36a0808 | |||
0772987128 | |||
fe0325fce7 | |||
f1f796f2fc | |||
0aa8258d29 | |||
fb93e3fa00 | |||
d494014782 | |||
48644f465d | |||
78cbb1512c | |||
7dba92dd53 | |||
3f3fc05c8f | |||
5967794009 | |||
b6f0c789b8 | |||
b87243d827 | |||
67a0de4bbc | |||
e4931e0c47 | |||
077e144e8a | |||
4b2d6fc70b | |||
f64963de88 | |||
246ba4301d | |||
24edf57e12 | |||
b1c3c4a202 | |||
50ffd87831 | |||
424377f859 | |||
6fa8f77638 | |||
25b6590547 | |||
ac77971f99 | |||
645cfb8355 | |||
e1e454f138 | |||
a0002d0598 | |||
99aa2caa3d | |||
8799679083 | |||
2dcd8213e4 | |||
5396037450 | |||
1e299d8232 | |||
8870b739b3 | |||
5a964f49a5 | |||
aca58ec605 | |||
41757e7f78 | |||
f333c7ff87 | |||
071ebb9feb | |||
aa72cda7b2 | |||
4b9c3a9102 | |||
0b493ac8d4 | |||
c73d41d98b | |||
3d2f65fc0d | |||
6b283f6ea1 | |||
4367c9a1db | |||
c9894687fc | |||
a56fa60fb4 | |||
014ef0f52d | |||
2fc47034ee | |||
46cbfbc630 | |||
d3fd10798b | |||
a6ba4d357c | |||
e707af7c3a | |||
ca06fd0060 | |||
958ade86a5 | |||
85a4477f71 | |||
38ec84693f | |||
78865aa7f7 | |||
0d541e6338 | |||
5f6e536be8 | |||
4f85a68c25 | |||
c3aae88b0c | |||
32a82bb423 | |||
285cd404e3 | |||
a607e097c6 | |||
55b4ff0cdf | |||
82094f05e0 | |||
c4f273478d | |||
14e1442d2d | |||
810a5146dd | |||
5055863e09 | |||
bf47fe7cac | |||
9d19429993 | |||
142dfc7d88 | |||
0a9c6164af | |||
376268391b | |||
d6f37ec9ad | |||
ca1b30db10 | |||
9454d30854 | |||
f75e56932a | |||
5604b4c57c | |||
8f1885a398 | |||
ccded6644a | |||
321d65c4ac | |||
c5e6053fcd | |||
eb0d80767e | |||
6fa031fa69 | |||
21987c8701 | |||
457b30e585 | |||
2138163c61 | |||
211c5e3e29 | |||
c3b0de943c | |||
1e05cd75c7 | |||
2d942e970b | |||
087e0e8b62 | |||
b65dd84e1a | |||
66572561bf | |||
902f06c5c4 | |||
b53a98eb38 | |||
a1f5df22ad | |||
04f6208ace | |||
3cb885c6b2 | |||
f4ea274555 | |||
4b555dba99 | |||
9c8f9b3560 | |||
4ed60471fe | |||
7d28d80e5a | |||
45d7ef99c4 | |||
0d8345e0c1 | |||
2760739ceb | |||
5d755bd54a | |||
bd2b18b6de | |||
68bca981de | |||
6fdbb086f4 | |||
99b1af40c6 | |||
99bb479a60 | |||
98406af448 | |||
6c9169b4f4 | |||
3fc6f9c24f | |||
0d7c43d885 | |||
c5140d5c18 | |||
fdb82718e0 | |||
791b2fd503 | |||
3c3cae57c6 | |||
bdd2a0a018 | |||
c6104c1e2a | |||
b85496922f | |||
89eac70d09 | |||
58b171b3e5 | |||
bb84aaebaf | |||
ab00d23cd3 | |||
5de9d38cc6 | |||
d36f09d643 | |||
f71c247d87 | |||
71acd0c3d0 | |||
288624550e | |||
e4d0c25365 | |||
c628d7f412 | |||
5cb13fd071 | |||
9e001dee29 | |||
4d40816a90 | |||
0f7add9722 | |||
9f29545f66 | |||
45b7c9a4ac | |||
34dabe281b | |||
5fbef59dbc | |||
915f8f4822 | |||
cedcc0d8df | |||
ac49e1d50f | |||
866ec5948c | |||
aa5711bd0f | |||
f7434b55e5 | |||
2235b47030 | |||
5ead800ff5 | |||
e4b12a8e28 | |||
9aefb91531 | |||
5ed5d44652 | |||
cc0ef16346 | |||
a272f5d7e3 | |||
63cf0b9d90 | |||
ab69c2adbd | |||
075ab6415f | |||
dd09042632 | |||
165ac654e8 | |||
dbdeceda7b | |||
ff1f5a9d57 | |||
d1ec13210f | |||
2ba02c04be | |||
6dd4944e62 | |||
5da481213e | |||
433b4138c5 | |||
729770f32a | |||
3ec4da6ac6 | |||
9df06bfa94 | |||
20df86e3c3 | |||
6433be5738 | |||
3068340a83 | |||
da6827f09e | |||
75104c10d4 | |||
58af26736c | |||
17c6f21d68 | |||
f0760d6246 | |||
913d102a81 | |||
824049897d | |||
b47631b38f | |||
22b86684f0 | |||
5ed5d018be | |||
f6e8b677cf | |||
0ef270c25c | |||
1130273178 | |||
3eb126af4d | |||
c282664c23 | |||
d52d836761 | |||
5bdf6a4110 | |||
421d5fbe72 |
33
.header
33
.header
@ -1,20 +1,13 @@
|
||||
/*
|
||||
Copyright 2013 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 x
|
||||
|
||||
import (
|
||||
)
|
||||
// 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.
|
||||
|
@ -1,10 +1,11 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.3
|
||||
- 1.4
|
||||
|
||||
install:
|
||||
- go get code.google.com/p/go.tools/cmd/cover
|
||||
- go get code.google.com/p/go.tools/cmd/vet
|
||||
- go get golang.org/x/tools/cmd/cover
|
||||
- go get golang.org/x/tools/cmd/vet
|
||||
|
||||
script:
|
||||
- ./test
|
||||
- INTEGRATION=y ./test
|
||||
|
87
CHANGELOG
87
CHANGELOG
@ -1,87 +0,0 @@
|
||||
v0.4.6
|
||||
* Fix long-term timer leak (#900, #875, #868, #904)
|
||||
* Fix `Running` field in standby_info file (#881)
|
||||
* Add `quorum=true` query parameter for GET requests (#866, #883)
|
||||
* Add `Access-Control-Allow-Headers` header for CORS requests (#886)
|
||||
* Various documentation improvements (#907, #882)
|
||||
|
||||
v0.4.5
|
||||
* Flush headers immediatly on `wait=true` requests (#877)
|
||||
* Add `ETCD_HTTP_READ_TIMEOUT` and `ETCD_HTTP_WRITE_TIMEOUT` (#880)
|
||||
* Add `ETCDCTL_PEERS` configuration to etcdctl (#95)
|
||||
* etcdctl takes stdin for mk (#91)
|
||||
|
||||
v0.4.4
|
||||
* Fix `--no-sync` flag in etcdctl (#83)
|
||||
* Improved logging for machine removal (#844)
|
||||
* Various documentation improvements (#858, #851, #847)
|
||||
|
||||
v0.4.3
|
||||
* Avoid panic() on truncated or unexpected log data (#834, #833)
|
||||
* Fix missing stats field (#807)
|
||||
* Lengthen default peer removal delay to 30mins (#835)
|
||||
* Reduce logging on heartbeat timeouts (#836)
|
||||
|
||||
v0.4.2
|
||||
* Improvements to the clustering documents
|
||||
* Set content-type properly on errors (#469)
|
||||
* Standbys re-join if they should be part of the cluster (#810, #815, #818)
|
||||
|
||||
v0.4.1
|
||||
* Re-introduce DELETE on the machines endpoint
|
||||
* Document the machines endpoint
|
||||
|
||||
v0.4.0
|
||||
* Introduced standby mode
|
||||
* Added HEAD requests
|
||||
* Set logs NOCOW flag when BTRFS is detected to avoid fsync overhead
|
||||
* Fix all known data races, and pass Go race detector (TODO: re-run race detector)
|
||||
* Fixed timeouts when using HTTPS
|
||||
* Improved snapshot stability
|
||||
* Migration of machine names to new IPs
|
||||
* Updated peer discovery ordering
|
||||
|
||||
v0.3.0
|
||||
* Add Compare-and-Delete support.
|
||||
* Added prevNode to response objects.
|
||||
* Added Discovery API.
|
||||
* Add tracing and debug endpoints (Documentation/debugging.md).
|
||||
* Improved logging of cluster events.
|
||||
* go get github.com/coreos/etcd works.
|
||||
* info file is no longer used.
|
||||
* Snapshots are on by default.
|
||||
* Statistics APIs documented.
|
||||
|
||||
v0.2.0
|
||||
* Support directory creation and removal.
|
||||
* Add Compare-and-Swap (CAS) support.
|
||||
* Support recursive GETs.
|
||||
* Support fully consistent GETs.
|
||||
* Allow clients to watch specific paths.
|
||||
* Allow clients to watch for key expiration.
|
||||
* Unique key generation.
|
||||
* Support hidden paths.
|
||||
* Refactor low-level data store.
|
||||
* Modularize store, server and API code.
|
||||
* Integrate Gorilla Web Toolkit.
|
||||
* Add tiered configuration (command line args, env variables, config file).
|
||||
* Add peer protocol versioning.
|
||||
* Add rolling upgrade support for future versions.
|
||||
* Sync key expiration across cluster.
|
||||
* Significantly improve test coverage.
|
||||
* Improve migration testing.
|
||||
* Configurable snapshot count.
|
||||
* Reduce TCP connection count.
|
||||
* Fix TCP connection leak.
|
||||
* Bug Fixes: https://github.com/coreos/etcd/issues?milestone=1&state=closed
|
||||
|
||||
Contributors:
|
||||
* Xiang Li (@xiangli-cmu)
|
||||
* Ben Johnson (@benbjohnson)
|
||||
* Brandon Philips (@philips)
|
||||
* Yifan (@yifan-gu)
|
||||
* Rob Szumski
|
||||
* Hongchao Deng (@fengjingchao)
|
||||
* Kelsey Hightower (@kelseyhightower)
|
||||
* Adrián (@adrianlzt)
|
||||
* Antonio Terreno (@aterreno)
|
@ -1,33 +0,0 @@
|
||||
## 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 member’s 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.
|
File diff suppressed because it is too large
Load Diff
@ -1,204 +0,0 @@
|
||||
# Clustering Guide
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
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_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
|
||||
```
|
||||
|
||||
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-token etcd-cluster-1\
|
||||
-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-token etcd-cluster-1\
|
||||
-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-token etcd-cluster-1\
|
||||
-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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### 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 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: 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.
|
||||
|
||||
```
|
||||
$ 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
|
||||
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.
|
||||
|
||||
```
|
||||
$ 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: 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”.
|
||||
|
||||
### 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=3
|
||||
```
|
||||
|
||||
By setting the size key to the URL, you create a discovery URL with expected-cluster-size of 3.
|
||||
|
||||
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:2379 -discovery https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
|
||||
$ etcd -name infra1 -initial-advertise-peer-urls http://10.0.1.11:2379 -discovery https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
|
||||
$ etcd -name infra2 -initial-advertise-peer-urls http://10.0.1.12:2379 -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.
|
||||
|
||||
```
|
||||
ETCD_DISCOVERY=https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
```
|
||||
|
||||
```
|
||||
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### Error and Warning Cases
|
||||
|
||||
#### Discovery Server Errors
|
||||
|
||||
```
|
||||
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2379 -discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
etcd: error: the cluster doesn’t have a size configuration value in https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de/_config
|
||||
exit 1
|
||||
```
|
||||
|
||||
#### User Errors
|
||||
|
||||
```
|
||||
$ 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
|
||||
exit 1
|
||||
```
|
||||
|
||||
#### Warnings
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
# 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.
|
||||
|
||||
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 |
|
||||
|-----------------------|-----------------------|---------------------------------------------------------------------------------------|
|
||||
|-peer-addr |-initial-advertise-peer-urls |If specified, peer-addr will be used as the only peer URL. Error if both flags specified.|
|
||||
|-addr |-advertise-client-urls |If specified, addr will be used as the only client URL. Error if both flags specified.|
|
||||
|-peer-bind-addr |-listen-peer-urls |If specified, peer-bind-addr will be used as the only peer bind URL. Error if both flags specified.|
|
||||
|-bind-addr |-listen-client-urls |If specified, bind-addr will be used as the only client bind URL. Error if both flags specified.|
|
||||
|-peers |none |Deprecated. The -initial-cluster flag provides a similar concept with different semantics. Please read this guide on cluster startup.|
|
||||
|-peers-file |none |Deprecated. The -initial-cluster flag provides a similar concept with different semantics. Please read this guide on cluster startup.|
|
@ -1,70 +0,0 @@
|
||||
## Members API
|
||||
|
||||
### GET /v2/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/members
|
||||
Response formats: JSON
|
||||
Example Response:
|
||||
```
|
||||
```json
|
||||
{
|
||||
"members": [
|
||||
{
|
||||
"id":"272e204152",
|
||||
"name":"infra1",
|
||||
"peerURLs":[
|
||||
"http://10.0.0.10:2379"
|
||||
],
|
||||
"clientURLs":[
|
||||
"http://10.0.0.10:2380"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id":"2225373f43",
|
||||
"name":"infra2",
|
||||
"peerURLs":[
|
||||
"http://127.0.0.11:2379"
|
||||
],
|
||||
"clientURLs":[
|
||||
"http://127.0.0.11:2380"
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### POST /v2/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/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/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/members/272e204152
|
||||
Example Response: Empty
|
||||
```
|
@ -1,139 +0,0 @@
|
||||
## 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
|
||||
EOF
|
||||
```
|
||||
|
||||
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.
|
||||
First we have to tell the cluster that it should expect a new member to join, then we will need to start the member with the correct configuration.
|
||||
|
||||
Using `etcdctl`, let’s tell the cluster about the new member:
|
||||
|
||||
```
|
||||
$ etcdctl member add infra3 http://10.0.1.13:2379
|
||||
added member 9bf1b35fc7761a23 to cluster
|
||||
ETCD_NAME="infra3"
|
||||
ETCD_INITIAL_CLUSTER="infra0=http://10.0.1.10:2379,infra1=http://10.0.1.11:2379,infra2=http://10.0.1.12:2379,infra3=http://10.0.1.13:2379"
|
||||
ETCD_INITIAL_CLUSTER_STATE=existing
|
||||
```
|
||||
|
||||
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:2379,infra1=http://10.0.1.11:2379,infra2=http://10.0.1.12:2379,infra3=http://10.0.1.13:2379"
|
||||
$ 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:2379,infra1=http://10.0.1.11:2379,infra2=http://10.0.1.12:2379 \
|
||||
-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:2379) to the one that we used to join the cluster (10.0.1.13:2379).
|
||||
|
||||
```
|
||||
$ etcd -name infra4 \
|
||||
-initial-cluster infra0=http://10.0.1.10:2379,infra1=http://10.0.1.11:2379,infra2=http://10.0.1.12:2379,infra4=http://10.0.1.14:2379 \
|
||||
-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
|
||||
```
|
47
Documentation/0_4_migration_tool.md
Normal file
47
Documentation/0_4_migration_tool.md
Normal file
@ -0,0 +1,47 @@
|
||||
## etcd 0.4.x -> 2.0.0 Data Migration Tool
|
||||
|
||||
### Upgrading from 0.4.x
|
||||
|
||||
Between 0.4.x and 2.0, the on-disk data formats have changed. In order to allow users to convert to 2.0, a migration tool is provided.
|
||||
|
||||
etcd will detect 0.4.x data dir and update the data automatically (while leaving a backup, in case of emergency).
|
||||
|
||||
### Data Migration Tips
|
||||
|
||||
* Keep the environment variables and etcd instance flags the same, 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 2.0 compatible. It does not remove the 0.4.x data, and is safe to convert multiple times; the 2.0 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 2.0-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 2.0 data.
|
209
Documentation/admin_guide.md
Normal file
209
Documentation/admin_guide.md
Normal file
@ -0,0 +1,209 @@
|
||||
## 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 member’s 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/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 Management
|
||||
|
||||
#### 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.
|
||||
|
||||
#### Optimal Cluster Size
|
||||
|
||||
The recommended etcd cluster size is 3, 5 or 7, which is decided by the fault tolerance requirement. A 7-member cluster can provide enough fault tolerance in most cases. While larger cluster provides better fault tolerance the write performance reduces since data needs to be replicated to more machines.
|
||||
|
||||
#### Fault Tolerance Table
|
||||
|
||||
It is recommended to have an odd number of members in a cluster. Having an odd cluster size doesn't change the number needed for majority, but you gain a higher tolerance for failure by adding the extra member. You can see this in practice when comparing even and odd sized clusters:
|
||||
|
||||
| Cluster Size | Majority | Failure Tolerance |
|
||||
|--------------|------------|-------------------|
|
||||
| 1 | 1 | 0 |
|
||||
| 3 | 2 | 1 |
|
||||
| 4 | 3 | 1 |
|
||||
| 5 | 3 | **2** |
|
||||
| 6 | 4 | 2 |
|
||||
| 7 | 4 | **3** |
|
||||
| 8 | 5 | 3 |
|
||||
| 9 | 5 | **4** |
|
||||
|
||||
As you can see, adding another member to bring the size of cluster up to an odd size is always worth it. During a network partition, an odd number of members also guarantees that there will almost always be a majority of the cluster that can continue to operate and be the source of truth when the partition ends.
|
||||
|
||||
#### Changing Cluster Size
|
||||
|
||||
After your cluster is up and running, adding or removing members is done via [runtime reconfiguration](runtime-configuration.md), which allows the cluster to be modified without downtime. The `etcdctl` tool has a `member list`, `member add` and `member remove` commands to complete this process.
|
||||
|
||||
### Member Migration
|
||||
|
||||
When there is a scheduled machine maintenance or retirement, you might want to migrate an etcd member to another machine without losing the data and changing the member ID.
|
||||
|
||||
The data directory contains all the data to recover a member to its point-in-time state. To migrate a member:
|
||||
|
||||
* Stop the member process
|
||||
* Copy the data directory of the now-idle member to the new machine
|
||||
* Update the peer URLs for that member to reflect the new machine according to the [member api] [change peer url]
|
||||
* Start etcd on the new machine, using the same configuration and the copy of the data directory
|
||||
|
||||
This example will walk you through the process of migrating the infra1 member to a new machine:
|
||||
|
||||
|Name|Peer URL|
|
||||
|------|--------------|
|
||||
|infra0|10.0.1.10:2380|
|
||||
|infra1|10.0.1.11:2380|
|
||||
|infra2|10.0.1.12:2380|
|
||||
|
||||
```
|
||||
$ export ETCDCTL_PEERS=http://10.0.1.10:2379,http://10.0.1.11:2379,http://10.0.1.12:2379
|
||||
```
|
||||
|
||||
```
|
||||
$ etcdctl member list
|
||||
84194f7c5edd8b37: name=infra0 peerURLs=http://10.0.1.10:2380 clientURLs=http://127.0.0.1:2379,http://10.0.1.10:2379
|
||||
b4db3bf5e495e255: name=infra1 peerURLs=http://10.0.1.11:2380 clientURLs=http://127.0.0.1:2379,http://10.0.1.11:2379
|
||||
bc1083c870280d44: name=infra2 peerURLs=http://10.0.1.12:2380 clientURLs=http://127.0.0.1:2379,http://10.0.1.12:2379
|
||||
```
|
||||
|
||||
#### Stop the member etcd process
|
||||
|
||||
```
|
||||
$ ssh core@10.0.1.11
|
||||
```
|
||||
|
||||
```
|
||||
$ sudo systemctl stop etcd
|
||||
```
|
||||
|
||||
#### Copy the data directory of the now-idle member to the new machine
|
||||
|
||||
```
|
||||
$ tar -cvzf node1.etcd.tar.gz /var/lib/etcd/node1.etcd
|
||||
```
|
||||
|
||||
```
|
||||
$ scp node1.etcd.tar.gz core@10.0.1.13:~/
|
||||
```
|
||||
|
||||
#### Update the peer URLs for that member to reflect the new machine
|
||||
|
||||
```
|
||||
$ curl http://10.0.1.10:2379/v2/members/b4db3bf5e495e255 -XPUT \
|
||||
-H "Content-Type: application/json" -d '{"peerURLs":["http://10.0.1.13:2380"]}'
|
||||
```
|
||||
|
||||
#### Start etcd on the new machine, using the same configuration and the copy of the data directory
|
||||
|
||||
```
|
||||
$ ssh core@10.0.1.13
|
||||
```
|
||||
|
||||
```
|
||||
$ tar -xzvf node1.etcd.tar.gz -C /var/lib/etcd
|
||||
```
|
||||
|
||||
```
|
||||
etcd -name node1 \
|
||||
-listen-peer-urls http://10.0.1.13:2380 \
|
||||
-listen-client-urls http://10.0.1.13:2379,http://127.0.0.1:2379 \
|
||||
-advertise-client-urls http://10.0.1.13:2379,http://127.0.0.1:2379
|
||||
```
|
||||
|
||||
[change peer url]: https://github.com/coreos/etcd/blob/master/Documentation/other_apis.md#change-the-peer-urls-of-a-member
|
||||
|
||||
### 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
|
||||
|
||||
**NB:** Windows users must stop etcd before running the backup command.
|
||||
|
||||
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.
|
||||
|
||||
### Client Request Timeout
|
||||
|
||||
etcd sets different timeouts for various types of client requests. The timeout value is not tunable now, which will be improved soon(https://github.com/coreos/etcd/issues/2038).
|
||||
|
||||
#### Get requests
|
||||
|
||||
Timeout is not set for get requests, because etcd serves the result locally in a non-blocking way.
|
||||
|
||||
**Note**: QuorumGet request is a different type, which is mentioned in the following sections.
|
||||
|
||||
#### Watch requests
|
||||
|
||||
Timeout is not set for watch requests. etcd will not stop a watch request until client cancels it, or the connection is broken.
|
||||
|
||||
#### Delete, Put, Post, QuorumGet requests
|
||||
|
||||
The default timeout is 5 seconds. It should be large enough to allow all key modifications if the majority of cluster is functioning.
|
||||
|
||||
If the request times out, it indicates two possibilities:
|
||||
|
||||
1. the server the request sent to was not functioning at that time.
|
||||
2. the majority of the cluster is not functioning.
|
||||
|
||||
If timeout happens several times continuously, administrators should check status of cluster and resolve it as soon as possible.
|
120
Documentation/allow_legacy_mode.md
Normal file
120
Documentation/allow_legacy_mode.md
Normal file
@ -0,0 +1,120 @@
|
||||
## Allow-legacy mode
|
||||
|
||||
Allow-legacy is a special mode in etcd that contains logic to enable a running etcd cluster to smoothly transition between major versions of etcd. For example, the internal API versions between etcd 0.4 (internal v1) and etcd 2.0 (internal v2) aren't compatible and the cluster needs to be updated all at once to make the switch. To minimize downtime, allow-legacy coordinates with all of the members of the cluster to shutdown, migration of data and restart onto the new version.
|
||||
|
||||
Allow-legacy helps users upgrade v0.4 etcd clusters easily, and allows your etcd cluster to have a minimal amount of downtime -- less than 1 minute for clusters storing less than 50 MB.
|
||||
|
||||
It supports upgrading from internal v1 to internal v2 now.
|
||||
|
||||
### Setup
|
||||
|
||||
This mode is enabled if `ETCD_ALLOW_LEGACY_MODE` is set to true, or etcd is running in CoreOS system.
|
||||
|
||||
It treats `ETCD_BINARY_DIR` as the directory for etcd binaries, which is organized in this way:
|
||||
|
||||
```
|
||||
ETCD_BINARY_DIR
|
||||
|
|
||||
-- 1
|
||||
|
|
||||
-- 2
|
||||
```
|
||||
|
||||
`1` is etcd with internal v1 protocol. You should use etcd v0.4.7 here. `2` is etcd with internal v2 protocol, which is etcd v2.x.
|
||||
|
||||
The default value for `ETCD_BINARY_DIR` is `/usr/libexec/etcd/internal_versions/`.
|
||||
|
||||
### Upgrading a Cluster
|
||||
|
||||
When starting etcd with a v1 data directory and v1 flags, etcd executes the v0.4.7 binary and runs exactly the same as before. To start the migration, follow the steps below:
|
||||
|
||||

|
||||
|
||||
#### 1. Check the Cluster Health
|
||||
|
||||
Before upgrading, you should check the health of the cluster to double check that everything working perfectly. Check the health by running:
|
||||
|
||||
```
|
||||
$ etcdctl cluster-health
|
||||
cluster is healthy
|
||||
member 6e3bd23ae5f1eae0 is healthy
|
||||
member 924e2e83e93f2560 is healthy
|
||||
member a8266ecf031671f3 is healthy
|
||||
```
|
||||
|
||||
If the cluster and all members are healthy, you can start the upgrading process. If not, check the unhealthy machines and repair them using [admin guide](./admin_guide.md).
|
||||
|
||||
#### 2. Trigger the Upgrade
|
||||
|
||||
When you're ready, use the `etcdctl upgrade` command to start the upgrade the etcd cluster to 2.0:
|
||||
|
||||
```
|
||||
# Defaults work on a CoreOS machine running etcd
|
||||
$ etcdctl upgrade
|
||||
```
|
||||
|
||||
```
|
||||
# Advanced example specifying a peer url
|
||||
$ etcdctl upgrade --old-version=1 --new-version=2 --peer-url=$PEER_URL
|
||||
```
|
||||
|
||||
`PEER_URL` can be any accessible peer url of the cluster.
|
||||
|
||||
Once triggered, all peer-mode members will print out:
|
||||
|
||||
```
|
||||
detected next internal version 2, exit after 10 seconds.
|
||||
```
|
||||
|
||||
#### Parallel Coordinated Upgrade
|
||||
|
||||
As part of the upgrade, etcd does internal coordination within the cluster for a brief period and then exits. Clusters storing 50 MB should be unavailable for less than 1 minute.
|
||||
|
||||
#### Restart etcd Processes
|
||||
|
||||
After the etcd processes exit, they need to be restarted. You can do this manually or configure your unit system to do this automatically. On CoreOS, etcd is already configured to start automatically with systemd.
|
||||
|
||||
When restarted, the data directory of each member is upgraded, and afterwards etcd v2.0 will be running and servicing requests. The upgrade is now complete!
|
||||
|
||||
Standby-mode members are a special case — they will be upgraded into proxy mode (a new feature in etcd 2.0) upon restarting. When the upgrade is triggered, any standbys will exit with the message:
|
||||
|
||||
```
|
||||
Detect the cluster has been upgraded to internal API v2. Exit now.
|
||||
```
|
||||
|
||||
Once restarted, standbys run in v2.0 proxy mode, which proxy user requests to the etcd cluster.
|
||||
|
||||
#### 3. Check the Cluster Health
|
||||
|
||||
After the upgrade process, you can run the health check again to verify the upgrade. If the cluster is unhealthy or there is an unhealthy member, please refer to start [failure recovery](#failure-recovery).
|
||||
|
||||
### Downgrade
|
||||
|
||||
If the upgrading fails due to disk/network issues, you still can restart the upgrading process manually. However, once you upgrade etcd to internal v2 protocol, you CANNOT downgrade it back to internal v1 protocol. If you want to downgrade etcd in the future, please backup your v1 data dir beforehand.
|
||||
|
||||
### Upgrade Process on CoreOS
|
||||
|
||||
When running on a CoreOS system, allow-legacy mode is enabled by default and an automatic update will set up everything needed to execute the upgrade. The `etcd.service` on CoreOS is already configured to restart automatically. All you need to do is run `etcdctl upgrade` when you're ready, as described
|
||||
|
||||
### Internal Details
|
||||
|
||||
etcd v0.4.7 registers versions of available etcd binaries in its local machine into the key space at bootstrap stage. When the upgrade command is executed, etcdctl checks whether each member has internal-version-v2 etcd binary around. If that is true, each member is asked to record the fact that it needs to be upgraded the next time it reboots, and exits after 10 seconds.
|
||||
|
||||
Once restarted, etcd v2.0 sees the upgrade flag recorded. It upgrades the data directory, and executes etcd v2.0.
|
||||
|
||||
### Failure Recovery
|
||||
|
||||
If `etcdctl cluster-health` says that the cluster is unhealthy, the upgrade process fails, which may happen if the network is broken, or the disk cannot work.
|
||||
|
||||
The way to recover it is to manually upgrade the whole cluster to v2.0:
|
||||
|
||||
- Log into machines that ran v0.4 peer-mode etcd
|
||||
- Stop all etcd services
|
||||
- Remove the `member` directory under the etcd data-dir
|
||||
- Start etcd service using [2.0 flags](configuration.md). An example for this is:
|
||||
```
|
||||
$ etcd --data-dir=$DATA_DIR --listen-peer-urls http://$LISTEN_PEER_ADDR \
|
||||
--advertise-client-urls http://$ADVERTISE_CLIENT_ADDR \
|
||||
--listen-client-urls http://$LISTEN_CLIENT_ADDR
|
||||
```
|
||||
- When this is done, v2.0 etcd cluster should work now.
|
File diff suppressed because it is too large
Load Diff
74
Documentation/backward_compatibility.md
Normal file
74
Documentation/backward_compatibility.md
Normal file
@ -0,0 +1,74 @@
|
||||
### Backward Compatibility
|
||||
|
||||
The main goal of etcd 2.0 release is to improve cluster safety around bootstrapping and dynamic reconfiguration. To do this, we deprecated the old error-prone APIs and provide a new set of APIs.
|
||||
|
||||
The other main focus of this release was a more reliable Raft implementation, but as this change is internal it should not have any notable effects to users.
|
||||
|
||||
#### Command Line Flags Changes
|
||||
|
||||
The major flag changes are to mostly related to bootstrapping. The `initial-*` flags provide an improved way to specify the required criteria to start the cluster. The advertised URLs now support a list of values instead of a single value, which allows etcd users to gracefully migrate to the new set of IANA-assigned ports (2379/client and 2380/peers) while maintaining backward compatibility with the old ports.
|
||||
|
||||
- `-addr` is replaced by `-advertise-client-urls`.
|
||||
- `-bind-addr` is replaced by `-listen-client-urls`.
|
||||
- `-peer-addr` is replaced by `-initial-advertise-peer-urls`.
|
||||
- `-peer-bind-addr` is replaced by `-listen-peer-urls`.
|
||||
- `-peers` is replaced by `-initial-cluster`.
|
||||
- `-peers-file` is replaced by `-initial-cluster`.
|
||||
- `-peer-heartbeat-interval` is replaced by `-heartbeat-interval`.
|
||||
- `-peer-election-timeout` is replaced by `-election-timeout`.
|
||||
|
||||
The documentation of new command line flags can be found at
|
||||
https://github.com/coreos/etcd/blob/master/Documentation/configuration.md.
|
||||
|
||||
#### Data Dir
|
||||
- Default data dir location has changed from {$hostname}.etcd to {name}.etcd.
|
||||
|
||||
- The disk format within the data dir has changed. etcd 2.0 should be able to auto upgrade the old data format. Instructions on doing so manually are in the [migration tool doc][migrationtooldoc].
|
||||
|
||||
[migrationtooldoc]: https://github.com/coreos/etcd/blob/master/Documentation/0_4_migration_tool.md
|
||||
|
||||
#### Key-Value API
|
||||
|
||||
##### Read consistency flag
|
||||
|
||||
The consistent flag for read operations is removed in etcd 2.0.0. The normal read operations provides the same consistency guarantees with the 0.4.6 read operations with consistent flag set.
|
||||
|
||||
The read consistency guarantees are:
|
||||
|
||||
The consistent read guarantees the sequential consistency within one client that talks to one etcd server. Read/Write from one client to one etcd member should be observed in order. If one client write a value to a etcd server successfully, it should be able to get the value out of the server immediately.
|
||||
|
||||
Each etcd member will proxy the request to leader and only return the result to user after the result is applied on the local member. Thus after the write succeed, the user is guaranteed to see the value on the member it sent the request to.
|
||||
|
||||
Reads do not provide linearizability. If you want linearizabilable read, you need to set quorum option to true.
|
||||
|
||||
**Previous behavior**
|
||||
|
||||
We added an option for a consistent read in the old version of etcd since etcd 0.x redirects the write request to the leader. When the user get back the result from the leader, the member it sent the request to originally might not apply the write request yet. With the consistent flag set to true, the client will always send read request to the leader. So one client should be able to see its last write when consistent=true is enabled. There is no order guarantees among different clients.
|
||||
|
||||
|
||||
#### Standby
|
||||
|
||||
etcd 0.4’s standby mode has been deprecated. [Proxy mode][proxymode] is introduced to solve a subset of problems standby was solving.
|
||||
|
||||
Standby mode was intended for large clusters that had a subset of the members acting in the consensus process. Overall this process was too magical and allowed for operators to back themselves into a corner.
|
||||
|
||||
Proxy mode in 2.0 will provide similar functionality, and with improved control over which machines act as proxies due to the operator specifically configuring them. Proxies also support read only or read/write modes for increased security and durability.
|
||||
|
||||
[proxymode]: https://github.com/coreos/etcd/blob/master/Documentation/proxy.md
|
||||
|
||||
#### Discovery Service
|
||||
|
||||
A size key needs to be provided inside a [discovery token][discoverytoken].
|
||||
[discoverytoken]: https://github.com/coreos/etcd/blob/master/Documentation/clustering.md#custom-etcd-discovery-service
|
||||
|
||||
#### HTTP Admin API
|
||||
|
||||
`v2/admin` on peer url and `v2/keys/_etcd` are unified under the new [v2/member API][memberapi] to better explain which machines are part of an etcd cluster, and to simplify the keyspace for all your use cases.
|
||||
|
||||
[memberapi]: https://github.com/coreos/etcd/blob/master/Documentation/other_apis.md
|
||||
|
||||
#### HTTP Key Value API
|
||||
- The follower can now transparently proxy write equests to the leader. Clients will no longer see 307 redirections to the leader from etcd.
|
||||
|
||||
- Expiration time is in UTC instead of local time.
|
||||
|
@ -1,43 +0,0 @@
|
||||
# Client libraries support matrix for etcd
|
||||
|
||||
As etcd features support is really uneven between client libraries, a compatibility matrix can be important.
|
||||
|
||||
## v2 clients
|
||||
|
||||
The v2 API has a lot of features, we will categorize them in a few categories:
|
||||
- **Language**: The language in which the client library was written.
|
||||
- **HTTPS Auth**: Support for SSL-certificate based authentication
|
||||
- **Reconnect**: If the client is able to reconnect automatically to another server if one fails.
|
||||
- **Mod/Lock**: Support for the locking module
|
||||
- **Mod/Leader**: Support for the leader election module
|
||||
- **GET,PUT,POST,DEL Features**: Support for all the modifiers when calling the etcd server with said HTTP method.
|
||||
|
||||
### Supported features matrix
|
||||
|
||||
**Legend**
|
||||
**F**: Full support **G**: Good support **B**: Basic support
|
||||
**Y**: Feature supported **-**: Feature not supported
|
||||
|
||||
Sorted alphabetically on language/name
|
||||
|
||||
|Client |**Language**|**HTTPS Auth**|**Re-connect**|**GET**|**PUT**|**POST**|**DEL**|**Mod Lock**|**Mod Leader**|
|
||||
| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
|
||||
|[etcd-api](https://github.com/jdarcy/etcd-api) |C |-|Y|B|G|-|B|-|-|
|
||||
|[etcdcpp](https://github.com/edwardcapriolo/etcdcpp) |C++ |-|-|F|F|G|-|-|-|
|
||||
|[cetcd](https://github.com/dwwoelfel/cetcd) |Clojure|-|-|F|F|-|G|-|-|
|
||||
|[clj-etcd](https://github.com/rthomas/clj-etcd) |Clojure|-|-|G|G|-|B|-|-|
|
||||
|[etcd-clojure](https://github.com/aterreno/etcd-clojure) |Clojure|-|-|F|F|F|F|-|-|
|
||||
|[go-etcd](https://github.com/coreos/go-etcd) |go |Y|Y|F|F|F|F|-|-|
|
||||
|[boon etcd client](https://github.com/boonproject/boon/blob/master/etcd/README.md) |java |Y|Y|F|F|F|F|-|F|
|
||||
|[etcd4j](https://github.com/jurmous/etcd4j) |java |Y|Y|F|F|F|F|-|-|
|
||||
|[jetcd](https://github.com/diwakergupta/jetcd) |java |Y|-|B|B|-|B|-|-|
|
||||
|[jetcd](https://github.com/justinsb/jetcd) |java |-|-|B|B|-|B|-|-|
|
||||
|[Etcd.jl](https://github.com/forio/Etcd.jl) |Julia |-|-|F|F|F|F|Y|Y|
|
||||
|[etcetera](https://github.com/drusellers/etcetera) |.net |-|-|F|F|F|F|-|-|
|
||||
|[node-etcd](https://github.com/stianeikeland/node-etcd) |nodejs |Y|-|F|F|-|F|-|-|
|
||||
|[nodejs-etcd](https://github.com/lavagetto/nodejs-etcd) |nodejs |Y|-|F|F|F|F|-|-|
|
||||
|[p5-etcd](https://metacpan.org/release/Etcd) |perl |-|-|F|F|F|F|-|-|
|
||||
|[python-etcd](https://github.com/jplana/python-etcd) |python |Y|Y|F|F|F|F|Y|-|
|
||||
|[python-etcd-client](https://github.com/dsoprea/PythonEtcdClient)|python |Y|Y|F|F|F|F|Y|Y|
|
||||
|[txetcd](https://github.com/russellhaering/txetcd) |python |-|-|G|G|F|G|-|-|
|
||||
|[etcd-ruby](https://github.com/ranjib/etcd-ruby) |ruby |-|-|F|F|F|F|-|-|
|
@ -1,60 +0,0 @@
|
||||
# Cluster Discovery
|
||||
|
||||
## Overview
|
||||
|
||||
Starting an etcd cluster requires that each node knows another in the cluster. If you are trying to bring up a cluster all at once, say using a cloud formation, you also need to coordinate who will be the initial cluster leader. The discovery protocol helps you by providing an automated way to discover other existing peers in a cluster.
|
||||
|
||||
For more information on how etcd can locate the cluster, see the [finding the cluster][cluster-finding] documentation.
|
||||
|
||||
Please note - at least 3 nodes are required for [cluster availability][optimal-cluster-size].
|
||||
|
||||
[cluster-finding]: https://github.com/coreos/etcd/blob/master/Documentation/design/cluster-finding.md
|
||||
[optimal-cluster-size]: https://github.com/coreos/etcd/blob/master/Documentation/optimal-cluster-size.md
|
||||
|
||||
## Using discovery.etcd.io
|
||||
|
||||
### Create a Discovery URL
|
||||
|
||||
To use the discovery API, you must first create a unique discovery URL for your etcd cluster. Visit [https://discovery.etcd.io/new](https://discovery.etcd.io/new) to create a new discovery URL.
|
||||
|
||||
You can inspect the list of peers by viewing `https://discovery.etcd.io/<cluster id>`.
|
||||
|
||||
### Start etcd With the Discovery Flag
|
||||
|
||||
Specify the `-discovery` flag when you start each etcd instance. The list of existing peers in the cluster will be downloaded and configured. If the instance is the first peer, it will start as the leader of the cluster.
|
||||
|
||||
Here's a full example:
|
||||
|
||||
```
|
||||
URL=$(curl https://discovery.etcd.io/new)
|
||||
./etcd -name instance1 -peer-addr 10.1.2.3:7001 -addr 10.1.2.3:4001 -discovery $URL
|
||||
./etcd -name instance2 -peer-addr 10.1.2.4:7001 -addr 10.1.2.4:4001 -discovery $URL
|
||||
./etcd -name instance3 -peer-addr 10.1.2.5:7001 -addr 10.1.2.5:4001 -discovery $URL
|
||||
```
|
||||
|
||||
## Running Your Own Discovery Endpoint
|
||||
|
||||
The discovery API communicates with a separate etcd cluster to store and retrieve the list of peers. CoreOS provides [https://discovery.etcd.io](https://discovery.etcd.io) as a free service, but you can easily run your own etcd cluster for this purpose. Here's an example using an etcd cluster located at `10.10.10.10:4001`:
|
||||
|
||||
```
|
||||
URL="http://10.10.10.10:4001/v2/keys/testcluster"
|
||||
./etcd -name instance1 -peer-addr 10.1.2.3:7001 -addr 10.1.2.3:4001 -discovery $URL
|
||||
./etcd -name instance2 -peer-addr 10.1.2.4:7001 -addr 10.1.2.4:4001 -discovery $URL
|
||||
./etcd -name instance3 -peer-addr 10.1.2.5:7001 -addr 10.1.2.5:4001 -discovery $URL
|
||||
```
|
||||
|
||||
If you're interested in how to discovery API works behind the scenes, read about the [Discovery Protocol](https://github.com/coreos/etcd/blob/master/Documentation/discovery-protocol.md).
|
||||
|
||||
## Setting Peer Addresses Correctly
|
||||
|
||||
The Discovery API submits the `-peer-addr` of each etcd instance to the configured Discovery endpoint. It's important to select an address that *all* peers in the cluster can communicate with. For example, if you're located in two regions of a cloud provider, configuring a private `10.x` address will not work between the two regions, and communication will not be possible between all peers.
|
||||
|
||||
## Stale Peers
|
||||
|
||||
The discovery API will automatically clean up the address of a stale peer that is no longer part of the cluster. The TTL for this process is a week, which should be long enough to handle any extremely long outage you may encounter. There is no harm in having stale peers in the list until they are cleaned up, since an etcd instance only needs to connect to one valid peer in the cluster to join.
|
||||
|
||||
## Lifetime of a Discovery URL
|
||||
|
||||
A discovery URL identifies a single etcd cluster. Do not re-use discovery URLs for new clusters.
|
||||
|
||||
When a machine starts with a new discovery URL the discovery URL will be activated and record the machine's metadata. If you destroy the whole cluster and attempt to bring the cluster back up with the same discovery URL it will fail. This is intentional because all of the registered machines are gone including their logs so there is nothing to recover the killed cluster.
|
@ -1,175 +1,358 @@
|
||||
## Clustering
|
||||
# Clustering Guide
|
||||
|
||||
### Example cluster of three machines
|
||||
## Overview
|
||||
|
||||
Let's explore the use of etcd clustering.
|
||||
We use Raft as the underlying distributed protocol which provides consistency and persistence of the data across all of the etcd instances.
|
||||
Starting an etcd cluster statically requires that each member knows another in the cluster. In a number of cases, you might not know the IPs of your cluster members ahead of time. In these cases, you can bootstrap an etcd cluster with the help of a discovery service.
|
||||
|
||||
Let start by creating 3 new etcd instances.
|
||||
Once an etcd cluster is up and running, adding or removing members is done via [runtime reconfiguration](runtime-configuration.md).
|
||||
|
||||
We use `-peer-addr` to specify server port and `-addr` to specify client port and `-data-dir` to specify the directory to store the log and info of the machine in the cluster:
|
||||
This guide will cover the following mechanisms for bootstrapping an etcd cluster:
|
||||
|
||||
```sh
|
||||
./etcd -peer-addr 127.0.0.1:7001 -addr 127.0.0.1:4001 -data-dir machines/machine1 -name machine1
|
||||
```
|
||||
* [Static](#static)
|
||||
* [etcd Discovery](#etcd-discovery)
|
||||
* [DNS Discovery](#dns-discovery)
|
||||
|
||||
**Note:** If you want to run etcd on an external IP address and still have access locally, you'll need to add `-bind-addr 0.0.0.0` so that it will listen on both external and localhost addresses.
|
||||
A similar argument `-peer-bind-addr` is used to setup the listening address for the server port.
|
||||
Each of the bootstrapping mechanisms will be used to create a three machine etcd cluster with the following details:
|
||||
|
||||
Let's join two more machines to this cluster using the `-peers` argument. A single connection to any peer will allow a new machine to join, but multiple can be specified for greater resiliency.
|
||||
|Name|Address|Hostname|
|
||||
|------|---------|------------------|
|
||||
|infra0|10.0.1.10|infra0.example.com|
|
||||
|infra1|10.0.1.11|infra1.example.com|
|
||||
|infra2|10.0.1.12|infra2.example.com|
|
||||
|
||||
```sh
|
||||
./etcd -peer-addr 127.0.0.1:7002 -addr 127.0.0.1:4002 -peers 127.0.0.1:7001,127.0.0.1:7003 -data-dir machines/machine2 -name machine2
|
||||
./etcd -peer-addr 127.0.0.1:7003 -addr 127.0.0.1:4003 -peers 127.0.0.1:7001,127.0.0.1:7002 -data-dir machines/machine3 -name machine3
|
||||
```
|
||||
## Static
|
||||
|
||||
We can retrieve a list of machines in the cluster using the HTTP API:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/machines
|
||||
```
|
||||
|
||||
We should see there are three machines in the cluster
|
||||
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:
|
||||
|
||||
```
|
||||
http://127.0.0.1:4001, http://127.0.0.1:4002, http://127.0.0.1:4003
|
||||
```
|
||||
|
||||
The machine list is also available via the main key API:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/keys/_etcd/machines
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "get",
|
||||
"node": {
|
||||
"createdIndex": 1,
|
||||
"dir": true,
|
||||
"key": "/_etcd/machines",
|
||||
"modifiedIndex": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"createdIndex": 1,
|
||||
"key": "/_etcd/machines/machine1",
|
||||
"modifiedIndex": 1,
|
||||
"value": "raft=http://127.0.0.1:7001&etcd=http://127.0.0.1:4001"
|
||||
},
|
||||
{
|
||||
"createdIndex": 2,
|
||||
"key": "/_etcd/machines/machine2",
|
||||
"modifiedIndex": 2,
|
||||
"value": "raft=http://127.0.0.1:7002&etcd=http://127.0.0.1:4002"
|
||||
},
|
||||
{
|
||||
"createdIndex": 3,
|
||||
"key": "/_etcd/machines/machine3",
|
||||
"modifiedIndex": 3,
|
||||
"value": "raft=http://127.0.0.1:7003&etcd=http://127.0.0.1:4003"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We can also get the current leader in the cluster:
|
||||
|
||||
```
|
||||
curl -L http://127.0.0.1:4001/v2/leader
|
||||
```
|
||||
|
||||
The first server we set up should still be the leader unless it has died during these commands.
|
||||
|
||||
```
|
||||
http://127.0.0.1:7001
|
||||
```
|
||||
|
||||
Now we can do normal SET and GET operations on keys as we explored earlier.
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "set",
|
||||
"node": {
|
||||
"createdIndex": 4,
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 4,
|
||||
"value": "bar"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rejoining to the Cluster
|
||||
|
||||
If one machine disconnects from the cluster, it could rejoin the cluster automatically when the communication is recovered.
|
||||
|
||||
If one machine is killed, it could rejoin the cluster when started with old name. If the peer address is changed, etcd will treat the new peer address as the refreshed one, which benefits instance migration, or virtual machine boot with different IP. The peer-address-changing functionality is only supported when the majority of the cluster is alive, because this behavior needs the consensus of the etcd cluster.
|
||||
|
||||
**Note:** For now, it is user responsibility to ensure that the machine doesn't join the cluster that has the member with the same name. Or unexpected error will happen. It would be improved sooner or later.
|
||||
|
||||
### Killing Nodes in the Cluster
|
||||
|
||||
Now if we kill the leader of the cluster, we can get the value from one of the other two machines:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4002/v2/keys/foo
|
||||
```
|
||||
|
||||
We can also see that a new leader has been elected:
|
||||
|
||||
```
|
||||
curl -L http://127.0.0.1:4002/v2/leader
|
||||
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
|
||||
```
|
||||
|
||||
```
|
||||
http://127.0.0.1:7002
|
||||
-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
|
||||
```
|
||||
|
||||
or
|
||||
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:
|
||||
|
||||
```
|
||||
http://127.0.0.1:7003
|
||||
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \
|
||||
-listen-peer-urls http://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 http://10.0.1.11:2380 \
|
||||
-listen-peer-urls http://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 http://10.0.1.12:2380 \
|
||||
-listen-peer-urls http://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 (for example, adding or removing members to/from the cluster), see the [runtime configuration](runtime-configuration.md) guide.
|
||||
|
||||
### Testing Persistence
|
||||
### Error Cases
|
||||
|
||||
Next we'll kill all the machines to test persistence.
|
||||
Type `CTRL-C` on each terminal and then rerun the same command you used to start each machine.
|
||||
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.
|
||||
|
||||
Your request for the `foo` key will return the correct value:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4002/v2/keys/foo
|
||||
```
|
||||
$ etcd -name infra1 -initial-advertise-peer-urls http://10.0.1.11:2380 \
|
||||
-listen-peer-urls https://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
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "get",
|
||||
"node": {
|
||||
"createdIndex": 4,
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 4,
|
||||
"value": "bar"
|
||||
}
|
||||
}
|
||||
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:2380 \
|
||||
-listen-peer-urls http://10.0.1.10: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.
|
||||
|
||||
### Using HTTPS between servers
|
||||
```
|
||||
$ etcd -name infra3 -initial-advertise-peer-urls http://10.0.1.13:2380 \
|
||||
-listen-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
|
||||
```
|
||||
|
||||
In the previous example we showed how to use SSL client certs for client-to-server communication.
|
||||
Etcd can also do internal server-to-server communication using SSL client certs.
|
||||
To do this just change the `-*-file` flags to `-peer-*-file`.
|
||||
## Discovery
|
||||
|
||||
If you are using SSL for server-to-server communication, you must use it on all instances of etcd.
|
||||
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".
|
||||
|
||||
### Bootstrapping a new cluster by name
|
||||
There two methods that can be used for discovery:
|
||||
|
||||
An etcd server is uniquely defined by the peer addresses it listens to. Suppose, however, that you wish to start over, while maintaining the data from the previous cluster -- that is, to pretend that this machine has never joined a cluster before.
|
||||
* etcd discovery service
|
||||
* DNS SRV records
|
||||
|
||||
You can use `--initial-cluster-name` to generate a new unique ID for each node, as a shared token that every node understands. Nodes also take this into account for bootstrapping the new cluster ID, so it also provides a way for a machine to listen on the same interfaces, disconnect from one cluster, and join a different cluster.
|
||||
### etcd Discovery
|
||||
|
||||
#### 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/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=3
|
||||
```
|
||||
|
||||
By setting the size key to the URL, you create a discovery URL with an expected cluster size of 3.
|
||||
|
||||
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 \
|
||||
-listen-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 \
|
||||
-listen-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 \
|
||||
-listen-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 etcd 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.
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
```
|
||||
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
```
|
||||
|
||||
Now we start etcd with those relevant flags for each member:
|
||||
|
||||
```
|
||||
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \
|
||||
-listen-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 \
|
||||
-listen-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 \
|
||||
-listen-peer-urls http://10.0.1.12:2380 \
|
||||
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
#### Error and Warning Cases
|
||||
|
||||
##### Discovery Server Errors
|
||||
|
||||
|
||||
```
|
||||
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \
|
||||
-listen-peer-urls http://10.0.1.10:2380 \
|
||||
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
etcd: error: the cluster doesn’t 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:2380 \
|
||||
-listen-peer-urls http://10.0.1.10:2380 \
|
||||
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de \
|
||||
-discovery-fallback exit
|
||||
etcd: discovery: cluster is full
|
||||
exit 1
|
||||
```
|
||||
|
||||
##### Warnings
|
||||
|
||||
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:2380 \
|
||||
-listen-peer-urls http://10.0.1.10:2380 \
|
||||
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
etcdserver: discovery token ignored since a cluster has already been initialized. Valid log found at /var/lib/etcd
|
||||
```
|
||||
|
||||
### DNS Discovery
|
||||
|
||||
DNS [SRV records](http://www.ietf.org/rfc/rfc2052.txt) can be used as a discovery mechanism.
|
||||
The `-discovery-srv` flag can be used to set the DNS domain name where the discovery SRV records can be found.
|
||||
The following DNS SRV records are looked up in the listed order:
|
||||
|
||||
* _etcd-server-ssl._tcp.example.com
|
||||
* _etcd-server._tcp.example.com
|
||||
|
||||
If `_etcd-server-ssl._tcp.example.com` is found then etcd will attempt the bootstrapping process over SSL.
|
||||
|
||||
#### Create DNS SRV records
|
||||
|
||||
```
|
||||
$ dig +noall +answer SRV _etcd-server._tcp.example.com
|
||||
_etcd-server._tcp.example.com. 300 IN SRV 0 0 2380 infra0.example.com.
|
||||
_etcd-server._tcp.example.com. 300 IN SRV 0 0 2380 infra1.example.com.
|
||||
_etcd-server._tcp.example.com. 300 IN SRV 0 0 2380 infra2.example.com.
|
||||
```
|
||||
|
||||
```
|
||||
$ dig +noall +answer infra0.example.com infra1.example.com infra2.example.com
|
||||
infra0.example.com. 300 IN A 10.0.1.10
|
||||
infra1.example.com. 300 IN A 10.0.1.11
|
||||
infra2.example.com. 300 IN A 10.0.1.12
|
||||
```
|
||||
#### Bootstrap the etcd cluster using DNS
|
||||
|
||||
etcd cluster memebers can listen on domain names or IP address, the bootstrap process will resolve DNS A records.
|
||||
|
||||
```
|
||||
$ etcd -name infra0 \
|
||||
-discovery-srv example.com \
|
||||
-initial-advertise-peer-urls http://infra0.example.com:2380 \
|
||||
-initial-cluster-token etcd-cluster-1 \
|
||||
-initial-cluster-state new \
|
||||
-advertise-client-urls http://infra0.example.com:2379 \
|
||||
-listen-client-urls http://infra0.example.com:2379 \
|
||||
-listen-peer-urls http://infra0.example.com:2380
|
||||
```
|
||||
|
||||
```
|
||||
$ etcd -name infra1 \
|
||||
-discovery-srv example.com \
|
||||
-initial-advertise-peer-urls http://infra1.example.com:2380 \
|
||||
-initial-cluster-token etcd-cluster-1 \
|
||||
-initial-cluster-state new \
|
||||
-advertise-client-urls http://infra1.example.com:2379 \
|
||||
-listen-client-urls http://infra1.example.com:2379 \
|
||||
-listen-peer-urls http://infra1.example.com:2380
|
||||
```
|
||||
|
||||
```
|
||||
$ etcd -name infra2 \
|
||||
-discovery-srv example.com \
|
||||
-initial-advertise-peer-urls http://infra2.example.com:2380 \
|
||||
-initial-cluster-token etcd-cluster-1 \
|
||||
-initial-cluster-state new \
|
||||
-advertise-client-urls http://infra2.example.com:2379 \
|
||||
-listen-client-urls http://infra2.example.com:2379 \
|
||||
-listen-peer-urls http://infra2.example.com:2380
|
||||
```
|
||||
|
||||
You can also bootstrap the cluster using IP addresses instead of domain names:
|
||||
|
||||
```
|
||||
$ etcd -name infra0 \
|
||||
-discovery-srv example.com \
|
||||
-initial-advertise-peer-urls http://10.0.1.10:2380 \
|
||||
-initial-cluster-token etcd-cluster-1 \
|
||||
-initial-cluster-state new \
|
||||
-advertise-client-urls http://10.0.1.10:2379 \
|
||||
-listen-client-urls http://10.0.1.10:2379 \
|
||||
-listen-peer-urls http://10.0.1.10:2380
|
||||
```
|
||||
|
||||
```
|
||||
$ etcd -name infra1 \
|
||||
-discovery-srv example.com \
|
||||
-initial-advertise-peer-urls http://10.0.1.11:2380 \
|
||||
-initial-cluster-token etcd-cluster-1 \
|
||||
-initial-cluster-state new \
|
||||
-advertise-client-urls http://10.0.1.11:2379 \
|
||||
-listen-client-urls http://10.0.1.11:2379 \
|
||||
-listen-peer-urls http://10.0.1.11:2380
|
||||
```
|
||||
|
||||
```
|
||||
$ etcd -name infra2 \
|
||||
-discovery-srv example.com \
|
||||
-initial-advertise-peer-urls http://10.0.1.12:2380 \
|
||||
-initial-cluster-token etcd-cluster-1 \
|
||||
-initial-cluster-state new \
|
||||
-advertise-client-urls http://10.0.1.12:2379 \
|
||||
-listen-client-urls http://10.0.1.12:2379 \
|
||||
-listen-peer-urls http://10.0.1.12:2380
|
||||
```
|
||||
|
||||
#### etcd proxy configuration
|
||||
|
||||
DNS SRV records can also be used to configure the list of peers for an etcd server running in proxy mode:
|
||||
|
||||
```
|
||||
$ etcd --proxy on -discovery-srv example.com
|
||||
```
|
||||
|
||||
# 0.4 to 2.0+ Migration Guide
|
||||
|
||||
In etcd 2.0 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.
|
||||
|
||||
|Old Flag |New Flag |Migration Behavior |
|
||||
|-----------------------|-----------------------|---------------------------------------------------------------------------------------|
|
||||
|-peer-addr |-initial-advertise-peer-urls |If specified, peer-addr will be used as the only peer URL. Error if both flags specified.|
|
||||
|-addr |-advertise-client-urls |If specified, addr will be used as the only client URL. Error if both flags specified.|
|
||||
|-peer-bind-addr |-listen-peer-urls |If specified, peer-bind-addr will be used as the only peer bind URL. Error if both flags specified.|
|
||||
|-bind-addr |-listen-client-urls |If specified, bind-addr will be used as the only client bind URL. Error if both flags specified.|
|
||||
|-peers |none |Deprecated. The -initial-cluster flag provides a similar concept with different semantics. Please read this guide on cluster startup.|
|
||||
|-peers-file |none |Deprecated. The -initial-cluster flag provides a similar concept with different semantics. Please read this guide on cluster startup.|
|
||||
|
@ -1,135 +1,155 @@
|
||||
# Etcd Configuration
|
||||
## Configuration Flags
|
||||
|
||||
## Node Configuration
|
||||
etcd is configurable through command-line flags and environment variables. Options set on the command line take precedence over those from the environment.
|
||||
|
||||
Individual node configuration options can be set in three places:
|
||||
The format of environment variable for flag `-my-flag` is `ETCD_MY_FLAG`. It applies to all flags.
|
||||
|
||||
1. Command line flags
|
||||
2. Environment variables
|
||||
3. Configuration file
|
||||
To start etcd automatically using custom settings at startup in Linux, using a [systemd][systemd-intro] unit is highly recommended.
|
||||
|
||||
Options set on the command line take precedence over all other sources.
|
||||
Options set in environment variables take precedence over options set in
|
||||
configuration files.
|
||||
[systemd-intro]: http://freedesktop.org/wiki/Software/systemd/
|
||||
|
||||
## Cluster Configuration
|
||||
### Member Flags
|
||||
|
||||
Cluster-wide settings are configured via the `/config` admin endpoint and additionally in the configuration file. Values contained in the configuration file will seed the cluster setting with the provided value. After the cluster is running, only the admin endpoint is used.
|
||||
##### -name
|
||||
+ Human-readable name for this member.
|
||||
+ default: "default"
|
||||
|
||||
The full documentation is contained in the [API docs](https://github.com/coreos/etcd/blob/master/Documentation/api.md#cluster-config).
|
||||
##### -data-dir
|
||||
+ Path to the data directory.
|
||||
+ default: "${name}.etcd"
|
||||
|
||||
* `activeSize` - the maximum number of peers that can participate in the consensus protocol. Other peers will join as standbys.
|
||||
* `removeDelay` - the minimum time in seconds that a machine has been observed to be unresponsive before it is removed from the cluster.
|
||||
* `syncInterval` - the amount of time in seconds between cluster sync when it runs in standby mode.
|
||||
##### -snapshot-count
|
||||
+ Number of committed transactions to trigger a snapshot to disk.
|
||||
+ default: "10000"
|
||||
|
||||
## Command Line Flags
|
||||
##### -heartbeat-interval
|
||||
+ Time (in milliseconds) of a heartbeat interval.
|
||||
+ default: "100"
|
||||
|
||||
### Required
|
||||
##### -election-timeout
|
||||
+ Time (in milliseconds) for an election to timeout.
|
||||
+ default: "1000"
|
||||
|
||||
* `-name` - The node name. Defaults to a UUID.
|
||||
##### -listen-peer-urls
|
||||
+ List of URLs to listen on for peer traffic.
|
||||
+ default: "http://localhost:2380,http://localhost:7001"
|
||||
|
||||
### Optional
|
||||
##### -listen-client-urls
|
||||
+ List of URLs to listen on for client traffic.
|
||||
+ default: "http://localhost:2379,http://localhost:4001"
|
||||
|
||||
* `-addr` - The advertised public hostname:port for client communication. Defaults to `127.0.0.1:4001`.
|
||||
* `-discovery` - A URL to use for discovering the peer list. (i.e `"https://discovery.etcd.io/your-unique-key"`).
|
||||
* `-http-read-timeout` - The number of seconds before an HTTP read operation is timed out.
|
||||
* `-http-write-timeout` - The number of seconds before an HTTP write operation is timed out.
|
||||
* `-bind-addr` - The listening hostname for client communication. Defaults to advertised IP.
|
||||
* `-peers` - A comma separated list of peers in the cluster (i.e `"203.0.113.101:7001,203.0.113.102:7001"`).
|
||||
* `-peers-file` - The file path containing a comma separated list of peers in the cluster.
|
||||
* `-ca-file` - The path of the client CAFile. Enables client cert authentication when present.
|
||||
* `-cert-file` - The cert file of the client.
|
||||
* `-key-file` - The key file of the client.
|
||||
* `-config` - The path of the etcd configuration file. Defaults to `/etc/etcd/etcd.conf`.
|
||||
* `-cors` - A comma separated white list of origins for cross-origin resource sharing.
|
||||
* `-cpuprofile` - The path to a file to output CPU profile data. Enables CPU profiling when present.
|
||||
* `-data-dir` - The directory to store log and snapshot. Defaults to the current working directory.
|
||||
* `-max-result-buffer` - The max size of result buffer. Defaults to `1024`.
|
||||
* `-max-retry-attempts` - The max retry attempts when trying to join a cluster. Defaults to `3`.
|
||||
* `-peer-addr` - The advertised public hostname:port for server communication. Defaults to `127.0.0.1:7001`.
|
||||
* `-peer-bind-addr` - The listening hostname for server communication. Defaults to advertised IP.
|
||||
* `-peer-ca-file` - The path of the CAFile. Enables client/peer cert authentication when present.
|
||||
* `-peer-cert-file` - The cert file of the server.
|
||||
* `-peer-key-file` - The key file of the server.
|
||||
* `-peer-election-timeout` - The number of milliseconds to wait before the leader is declared unhealthy.
|
||||
* `-peer-heartbeat-interval` - The number of milliseconds in between heartbeat requests
|
||||
* `-snapshot=false` - Disable log snapshots. Defaults to `true`.
|
||||
* `-cluster-active-size` - The expected number of instances participating in the consensus protocol. Only applied if the etcd instance is the first peer in the cluster.
|
||||
* `-cluster-remove-delay` - The number of seconds before one node is removed from the cluster since it cannot be connected at all. Only applied if the etcd instance is the first peer in the cluster.
|
||||
* `-cluster-sync-interval` - The number of seconds between synchronization for standby-mode instance with the cluster. Only applied if the etcd instance is the first peer in the cluster.
|
||||
* `-v` - Enable verbose logging. Defaults to `false`.
|
||||
* `-vv` - Enable very verbose logging. Defaults to `false`.
|
||||
* `-version` - Print the version and exit.
|
||||
##### -max-snapshots
|
||||
+ Maximum number of snapshot files to retain (0 is unlimited)
|
||||
+ default: 5
|
||||
+ The default for users on Windows is unlimited, and manual purging down to 5 (or your preference for safety) is recommended.
|
||||
|
||||
## Configuration File
|
||||
##### -max-wals
|
||||
+ Maximum number of wal files to retain (0 is unlimited)
|
||||
+ default: 5
|
||||
+ The default for users on Windows is unlimited, and manual purging down to 5 (or your preference for safety) is recommended.
|
||||
|
||||
The etcd configuration file is written in [TOML](https://github.com/mojombo/toml)
|
||||
and read from `/etc/etcd/etcd.conf` by default.
|
||||
##### -cors
|
||||
+ Comma-separated white list of origins for CORS (cross-origin resource sharing).
|
||||
+ default: none
|
||||
|
||||
```TOML
|
||||
addr = "127.0.0.1:4001"
|
||||
bind_addr = "127.0.0.1:4001"
|
||||
ca_file = ""
|
||||
cert_file = ""
|
||||
cors = []
|
||||
cpu_profile_file = ""
|
||||
data_dir = "."
|
||||
discovery = "http://etcd.local:4001/v2/keys/_etcd/registry/examplecluster"
|
||||
http_read_timeout = 10.0
|
||||
http_write_timeout = 10.0
|
||||
key_file = ""
|
||||
peers = []
|
||||
peers_file = ""
|
||||
max_cluster_size = 9
|
||||
max_result_buffer = 1024
|
||||
max_retry_attempts = 3
|
||||
name = "default-name"
|
||||
snapshot = true
|
||||
verbose = false
|
||||
very_verbose = false
|
||||
### Clustering Flags
|
||||
|
||||
[peer]
|
||||
addr = "127.0.0.1:7001"
|
||||
bind_addr = "127.0.0.1:7001"
|
||||
ca_file = ""
|
||||
cert_file = ""
|
||||
key_file = ""
|
||||
`-initial` prefix flags are used in bootstrapping ([static bootstrap][build-cluster], [discovery-service bootstrap][discovery] or [runtime reconfiguration][reconfig]) a new member, and ignored when restarting an existing member.
|
||||
|
||||
[cluster]
|
||||
active_size = 9
|
||||
remove_delay = 1800.0
|
||||
sync_interval = 5.0
|
||||
```
|
||||
`-discovery` prefix flags need to be set when using [discovery service][discovery].
|
||||
|
||||
## Environment Variables
|
||||
##### -initial-advertise-peer-urls
|
||||
|
||||
* `ETCD_ADDR`
|
||||
* `ETCD_BIND_ADDR`
|
||||
* `ETCD_CA_FILE`
|
||||
* `ETCD_CERT_FILE`
|
||||
* `ETCD_CORS_ORIGINS`
|
||||
* `ETCD_CONFIG`
|
||||
* `ETCD_CPU_PROFILE_FILE`
|
||||
* `ETCD_DATA_DIR`
|
||||
* `ETCD_DISCOVERY`
|
||||
* `ETCD_HTTP_READ_TIMEOUT`
|
||||
* `ETCD_HTTP_WRITE_TIMEOUT`
|
||||
* `ETCD_KEY_FILE`
|
||||
* `ETCD_PEERS`
|
||||
* `ETCD_PEERS_FILE`
|
||||
* `ETCD_MAX_CLUSTER_SIZE`
|
||||
* `ETCD_MAX_RESULT_BUFFER`
|
||||
* `ETCD_MAX_RETRY_ATTEMPTS`
|
||||
* `ETCD_NAME`
|
||||
* `ETCD_SNAPSHOT`
|
||||
* `ETCD_VERBOSE`
|
||||
* `ETCD_VERY_VERBOSE`
|
||||
* `ETCD_PEER_ADDR`
|
||||
* `ETCD_PEER_BIND_ADDR`
|
||||
* `ETCD_PEER_CA_FILE`
|
||||
* `ETCD_PEER_CERT_FILE`
|
||||
* `ETCD_PEER_KEY_FILE`
|
||||
* `ETCD_PEER_ELECTION_TIMEOUT`
|
||||
* `ETCD_CLUSTER_ACTIVE_SIZE`
|
||||
* `ETCD_CLUSTER_REMOVE_DELAY`
|
||||
* `ETCD_CLUSTER_SYNC_INTERVAL`
|
||||
+ List of this member's peer URLs to advertise to the rest of the cluster. These addresses are used for communicating etcd data around the cluster. At least one must be routable to all cluster members.
|
||||
+ default: "http://localhost:2380,http://localhost:7001"
|
||||
|
||||
##### -initial-cluster
|
||||
+ Initial cluster configuration for bootstrapping.
|
||||
+ default: "default=http://localhost:2380,default=http://localhost:7001"
|
||||
|
||||
##### -initial-cluster-state
|
||||
+ Initial cluster state ("new" or "existing"). Set to `new` for all members present during initial static or DNS bootstrapping. If this option is set to `existing`, etcd will attempt to join the existing cluster. If the wrong value is set, etcd will attempt to start but fail safely.
|
||||
+ default: "new"
|
||||
|
||||
[static bootstrap]: clustering.md#static
|
||||
|
||||
##### -initial-cluster-token
|
||||
+ Initial cluster token for the etcd cluster during bootstrap.
|
||||
+ default: "etcd-cluster"
|
||||
|
||||
##### -advertise-client-urls
|
||||
+ List of this member's client URLs to advertise to the rest of the cluster.
|
||||
+ default: "http://localhost:2379,http://localhost:4001"
|
||||
|
||||
##### -discovery
|
||||
+ Discovery URL used to bootstrap the cluster.
|
||||
+ default: none
|
||||
|
||||
##### -discovery-srv
|
||||
+ DNS srv domain used to bootstrap the cluster.
|
||||
+ default: none
|
||||
|
||||
##### -discovery-fallback
|
||||
+ Expected behavior ("exit" or "proxy") when discovery services fails.
|
||||
+ default: "proxy"
|
||||
|
||||
##### -discovery-proxy
|
||||
+ HTTP proxy to use for traffic to discovery service.
|
||||
+ default: none
|
||||
|
||||
### Proxy Flags
|
||||
|
||||
`-proxy` prefix flags configures etcd to run in [proxy mode][proxy].
|
||||
|
||||
##### -proxy
|
||||
+ Proxy mode setting ("off", "readonly" or "on").
|
||||
+ default: "off"
|
||||
|
||||
### Security Flags
|
||||
|
||||
The security flags help to [build a secure etcd cluster][security].
|
||||
|
||||
##### -ca-file
|
||||
+ Path to the client server TLS CA file.
|
||||
+ default: none
|
||||
|
||||
##### -cert-file
|
||||
+ Path to the client server TLS cert file.
|
||||
+ default: none
|
||||
|
||||
##### -key-file
|
||||
+ Path to the client server TLS key file.
|
||||
+ default: none
|
||||
|
||||
##### -peer-ca-file
|
||||
+ Path to the peer server TLS CA file.
|
||||
+ default: none
|
||||
|
||||
##### -peer-cert-file
|
||||
+ Path to the peer server TLS cert file.
|
||||
+ default: none
|
||||
|
||||
##### -peer-key-file
|
||||
+ Path to the peer server TLS key file.
|
||||
+ default: none
|
||||
|
||||
### Unsafe Flags
|
||||
|
||||
Be CAUTIOUS to use unsafe flags because it will break the guarantee given by consensus protocol. For example, it may panic if other members in the cluster are still alive. Follow the instructions when using these falgs.
|
||||
|
||||
##### -force-new-cluster
|
||||
+ Force to create a new one-member cluster. It commits configuration changes in force to remove all existing members in the cluster and add itself. It needs to be set to [restore a backup][restore].
|
||||
+ default: false
|
||||
|
||||
### Miscellaneous Flags
|
||||
|
||||
##### -version
|
||||
+ Print the version and exit.
|
||||
+ default: false
|
||||
|
||||
[build-cluster]: https://github.com/coreos/etcd/blob/master/Documentation/clustering.md#static
|
||||
[reconfig]: https://github.com/coreos/etcd/blob/master/Documentation/runtime-configuration.md
|
||||
[discovery]: https://github.com/coreos/etcd/blob/master/Documentation/clustering.md#discovery
|
||||
[proxy]: https://github.com/coreos/etcd/blob/master/Documentation/proxy.md
|
||||
[security]: https://github.com/coreos/etcd/blob/master/Documentation/security.md
|
||||
[restore]: https://github.com/coreos/etcd/blob/master/Documentation/admin_guide.md#restoring-a-backup
|
||||
|
@ -1,69 +0,0 @@
|
||||
# Debugging etcd
|
||||
|
||||
Diagnosing issues in a distributed application is hard.
|
||||
etcd will help as much as it can - just enable these debug features using the CLI flag `-trace=*` or the config option `trace=*`.
|
||||
|
||||
## Logging
|
||||
|
||||
Log verbosity can be increased to the max using either the `-vvv` CLI flag or the `very_very_verbose=true` config option.
|
||||
|
||||
The only supported logging mode is to stdout.
|
||||
|
||||
## Metrics
|
||||
|
||||
etcd itself can generate a set of metrics.
|
||||
These metrics represent many different internal data points that can be helpful when debugging etcd servers.
|
||||
|
||||
#### Metrics reference
|
||||
|
||||
Each individual metric name is prefixed with `etcd.<NAME>`, where \<NAME\> is the configured name of the etcd server.
|
||||
|
||||
* `timer.appendentries.handle`: amount of time a peer takes to process an AppendEntriesRequest from the POV of the peer itself
|
||||
* `timer.peer.<PEER>.heartbeat`: amount of time a peer heartbeat operation takes from the POV of the leader that initiated that operation for peer \<PEER\>
|
||||
* `timer.command.<COMMAND>`: amount of time a given command took to be processed through the local server's raft state machine. This does not include time waiting on locks.
|
||||
|
||||
#### Fetching metrics over HTTP
|
||||
|
||||
Once tracing has been enabled on a given etcd server, all metric data is available at the server's `/debug/metrics` HTTP endpoint (i.e. `http://127.0.0.1:4001/debug/metrics`).
|
||||
Executing a GET HTTP command against the metrics endpoint will yield the current state of all metrics in the etcd server.
|
||||
|
||||
#### Sending metrics to Graphite
|
||||
|
||||
etcd supports [Graphite's Carbon plaintext protocol](https://graphite.readthedocs.org/en/latest/feeding-carbon.html#the-plaintext-protocol) - a TCP wire protocol designed for shipping metric data to an aggregator.
|
||||
To send metrics to a Graphite endpoint using this protocol, use of the `-graphite-host` CLI flag or the `graphite_host` config option (i.e. `graphite_host=172.17.0.19:2003`).
|
||||
|
||||
See an [example graphite deploy script](https://github.com/coreos/etcd/contrib/graphite).
|
||||
|
||||
#### Generating additional metrics with Collectd
|
||||
|
||||
[Collectd](http://collectd.org/documentation.shtml) gathers metrics from the host running etcd.
|
||||
While these aren't metrics generated by etcd itself, it can be invaluable to compare etcd's view of the world to that of a separate process running next to etcd.
|
||||
|
||||
See an [example collectd deploy script](https://github.com/coreos/etcd/contrib/collectd).
|
||||
|
||||
## Profiling
|
||||
|
||||
etcd exposes profiling information from the Go pprof package over HTTP.
|
||||
The basic browsable interface is served by etcd at the `/debug/pprof` HTTP endpoint (i.e. `http://127.0.0.1:4001/debug/pprof`).
|
||||
For more information on using profiling tools, see http://blog.golang.org/profiling-go-programs.
|
||||
|
||||
**NOTE**: In the following examples you need to ensure that the `./bin/etcd` is identical to the `./bin/etcd` that you are targeting (same git hash, arch, platform, etc).
|
||||
|
||||
#### Heap memory profile
|
||||
|
||||
```
|
||||
go tool pprof ./bin/etcd http://127.0.0.1:4001/debug/pprof/heap
|
||||
```
|
||||
|
||||
#### CPU profile
|
||||
|
||||
```
|
||||
go tool pprof ./bin/etcd http://127.0.0.1:4001/debug/pprof/profile
|
||||
```
|
||||
|
||||
#### Blocked goroutine profile
|
||||
|
||||
```
|
||||
go tool pprof ./bin/etcd http://127.0.0.1:4001/debug/pprof/block
|
||||
```
|
||||
|
@ -1,34 +0,0 @@
|
||||
## Cluster Finding Process
|
||||
|
||||
Peer discovery uses the following sources in this order: log data in `-data-dir`, `-discovery` and `-peers`.
|
||||
|
||||
If log data is provided, etcd will concatenate possible peers from three sources: the log data, the `-discovery` option, and `-peers`. Then it tries to join cluster through them one by one. If all connection attempts fail (which indicates that the majority of the cluster is currently down), it will restart itself based on the log data, which helps the cluster to recover from a full outage.
|
||||
|
||||
Without log data, the instance is assumed to be a brand new one. If possible targets are provided by `-discovery` and `-peers`, etcd will make a best effort attempt to join them, and if none is reachable it will exit. Otherwise, if no `-discovery` or `-peers` option is provided, a new cluster will always be started.
|
||||
|
||||
This ensures that users can always restart the node safely with the same command (without --force), and etcd will either reconnect to the old cluster if it is still running or recover its cluster from a outage.
|
||||
|
||||
## Logical Workflow
|
||||
|
||||
Start an etcd machine:
|
||||
|
||||
```
|
||||
If log data is given:
|
||||
Try to join via peers in previous cluster
|
||||
Try to join via peers found in discover URL
|
||||
Try to join via peers in peer list
|
||||
Restart the previous cluster which is down
|
||||
return
|
||||
|
||||
If discover URL is given:
|
||||
Fetch peers through discover URL
|
||||
If Success:
|
||||
Join via peers found
|
||||
return
|
||||
|
||||
If peer list is given:
|
||||
Join as follower via peers in peer list
|
||||
return
|
||||
|
||||
Start as the leader of a new cluster
|
||||
```
|
@ -1,232 +0,0 @@
|
||||
## Standbys
|
||||
|
||||
Adding peers in an etcd cluster adds network, CPU, and disk overhead to the leader since each one requires replication.
|
||||
Peers primarily provide resiliency in the event of a leader failure but the benefit of more failover nodes decreases as the cluster size increases.
|
||||
A lightweight alternative is the standby.
|
||||
|
||||
Standbys are a way for an etcd node to forward requests along to the cluster but the standbys are not part of the Raft cluster themselves.
|
||||
This provides an easier API for local applications while reducing the overhead required by a regular peer node.
|
||||
Standbys also act as standby nodes in the event that a peer node in the cluster has not recovered after a long duration.
|
||||
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
There are three configuration parameters used by standbys: active size, remove delay and standby sync interval.
|
||||
|
||||
The active size specifies a target size for the number of peers in the cluster.
|
||||
If there are not enough peers to meet the active size, standbys will send join requests until the peer count is equal to the active size.
|
||||
If there are more peers than the target active size then peers are removed by the leader and will become standbys.
|
||||
|
||||
The remove delay specifies how long the cluster should wait before removing a dead peer.
|
||||
By default this is 30 minutes.
|
||||
If a peer is inactive for 30 minutes then the peer is removed.
|
||||
|
||||
The standby sync interval specifies the synchronization interval of standbys with the cluster.
|
||||
By default this is 5 seconds.
|
||||
After each interval, standbys synchronize information with cluster.
|
||||
|
||||
|
||||
## Logical Workflow
|
||||
|
||||
### Start a etcd machine
|
||||
|
||||
#### Main logic
|
||||
|
||||
```
|
||||
If find existing standby cluster info:
|
||||
Goto standby loop
|
||||
|
||||
Find cluster as required
|
||||
If determine to start peer server:
|
||||
Goto peer loop
|
||||
Else:
|
||||
Goto standby loop
|
||||
|
||||
Peer loop:
|
||||
Start peer mode
|
||||
If running:
|
||||
Wait for stop
|
||||
Goto standby loop
|
||||
|
||||
Standby loop:
|
||||
Start standby mode
|
||||
If running:
|
||||
Wait for stop
|
||||
Goto peer loop
|
||||
```
|
||||
|
||||
|
||||
#### [Cluster finding logic][cluster-finding.md]
|
||||
|
||||
|
||||
#### Join request logic:
|
||||
|
||||
```
|
||||
Fetch machine info
|
||||
If cannot match version:
|
||||
return false
|
||||
If active size <= peer count:
|
||||
return false
|
||||
If it has existed in the cluster:
|
||||
return true
|
||||
If join request fails:
|
||||
return false
|
||||
return true
|
||||
```
|
||||
|
||||
**Note**
|
||||
1. [TODO] The running mode cannot be determined by log, because the log may be outdated. But the log could be used to estimate its state.
|
||||
2. Even if sync cluster fails, it will restart still for recovery from full outage.
|
||||
|
||||
|
||||
#### Peer mode start logic
|
||||
|
||||
```
|
||||
Start raft server
|
||||
Start other helper routines
|
||||
```
|
||||
|
||||
|
||||
#### Peer mode auto stop logic
|
||||
|
||||
```
|
||||
When removed from the cluster:
|
||||
Stop raft server
|
||||
Stop other helper routines
|
||||
```
|
||||
|
||||
|
||||
#### Standby mode run logic
|
||||
|
||||
```
|
||||
Loop:
|
||||
Sleep for some time
|
||||
|
||||
Sync cluster, and write cluster info into disk
|
||||
|
||||
Check active size and send join request if needed
|
||||
If succeed:
|
||||
Clear cluster info from disk
|
||||
Return
|
||||
```
|
||||
|
||||
|
||||
#### Serve Requests as Standby
|
||||
|
||||
Return '404 Page Not Found' always on peer address. This is because peer address is used for raft communication and cluster management, which should not be used in standby mode.
|
||||
|
||||
|
||||
Serve requests from client:
|
||||
|
||||
```
|
||||
Redirect all requests to client URL of leader
|
||||
```
|
||||
|
||||
**Note**
|
||||
1. The leader here implies the one in raft cluster when doing the latest successful synchronization.
|
||||
2. [IDEA] We could extend HTTP Redirect to multiple possible targets.
|
||||
|
||||
|
||||
### Join Request Handling
|
||||
|
||||
```
|
||||
If machine has existed in the cluster:
|
||||
Return
|
||||
If peer count < active size:
|
||||
Add peer
|
||||
Increase peer count
|
||||
```
|
||||
|
||||
|
||||
### Remove Request Handling
|
||||
|
||||
```
|
||||
If machine exists in the cluster:
|
||||
Remove peer
|
||||
Decrease peer count
|
||||
```
|
||||
|
||||
|
||||
## Cluster Monitor Logic
|
||||
|
||||
### Active Size Monitor:
|
||||
|
||||
This is only run by current cluster leader.
|
||||
|
||||
```
|
||||
Loop:
|
||||
Sleep for some time
|
||||
|
||||
If peer count > active size:
|
||||
Remove randomly selected peer
|
||||
```
|
||||
|
||||
|
||||
### Peer Activity Monitor
|
||||
|
||||
This is only run by current cluster leader.
|
||||
|
||||
```
|
||||
Loop:
|
||||
Sleep for some time
|
||||
|
||||
For each peer:
|
||||
If peer last activity time > remove delay:
|
||||
Remove the peer
|
||||
Goto Loop
|
||||
```
|
||||
|
||||
|
||||
## Cluster Cases
|
||||
|
||||
### Create Cluster with Thousands of Instances
|
||||
|
||||
First few machines run in peer mode.
|
||||
|
||||
All the others check the status of the cluster and run in standby mode.
|
||||
|
||||
|
||||
### Recover from full outage
|
||||
|
||||
Machines with log data restart with join failure.
|
||||
|
||||
Machines in peer mode recover heartbeat between each other.
|
||||
|
||||
Machines in standby mode always sync the cluster. If sync fails, it uses the first address from data log as redirect target.
|
||||
|
||||
|
||||
### Kill one peer machine
|
||||
|
||||
Leader of the cluster lose the connection with the peer.
|
||||
|
||||
When the time exceeds remove delay, it removes the peer from the cluster.
|
||||
|
||||
Machine in standby mode finds one available place of the cluster. It sends join request and joins the cluster.
|
||||
|
||||
**Note**
|
||||
1. [TODO] Machine which was divided from majority and was removed from the cluster will distribute running of the cluster if the new node uses the same name.
|
||||
|
||||
|
||||
### Kill one standby machine
|
||||
|
||||
No change for the cluster.
|
||||
|
||||
|
||||
## Cons
|
||||
|
||||
1. New instance cannot join immediately after one peer is kicked out of the cluster, because the leader doesn't know the info about the standby instances.
|
||||
|
||||
2. It may introduce join collision
|
||||
|
||||
3. Cluster needs a good interval setting to balance the join delay and join collision.
|
||||
|
||||
|
||||
## Future Attack Plans
|
||||
|
||||
1. Based on heartbeat miss and remove delay, standby could adjust its next check time.
|
||||
|
||||
2. Preregister the promotion target when heartbeat miss happens.
|
||||
|
||||
3. Get the estimated cluster size from the check happened in the sync interval, and adjust sync interval dynamically.
|
||||
|
||||
4. Accept join requests based on active size and alive peers.
|
@ -1,87 +0,0 @@
|
||||
# Discovery Protocol
|
||||
|
||||
Starting a new etcd cluster can be painful since each machine needs to know of at least one live machine in the cluster. If you are trying to bring up a new cluster all at once, say using an AWS cloud formation, you also need to coordinate who will be the initial cluster leader. The discovery protocol uses an existing running etcd cluster to start a second etcd cluster.
|
||||
|
||||
To use this feature you add the command line flag `-discovery` to your etcd args. In this example we will use `http://example.com/v2/keys/_etcd/registry` as the URL prefix.
|
||||
|
||||
## The Protocol
|
||||
|
||||
By convention the etcd discovery protocol uses the key prefix `_etcd/registry`. A full URL to the keyspace will be `http://example.com/v2/keys/_etcd/registry`.
|
||||
|
||||
### Creating a New Cluster
|
||||
|
||||
Generate a unique token that will identify the new cluster. This will be used as a key prefix in the following steps. An easy way to do this is to use uuidgen:
|
||||
|
||||
```
|
||||
UUID=$(uuidgen)
|
||||
```
|
||||
|
||||
### Bringing up Machines
|
||||
|
||||
Now that you have your cluster ID you can start bringing up machines. Every machine will follow this protocol internally in etcd if given a `-discovery`.
|
||||
|
||||
### Registering your Machine
|
||||
|
||||
The first thing etcd must do is register your machine. This is done by using the machine name (from the `-name` arg) and posting it with a long TTL to the given key.
|
||||
|
||||
```
|
||||
curl -X PUT "http://example.com/v2/keys/_etcd/registry/${UUID}/${etcd_machine_name}?ttl=604800" -d value=${peer_addr}
|
||||
```
|
||||
|
||||
### Discovering Peers
|
||||
|
||||
Now that this etcd machine is registered it must discover its peers.
|
||||
|
||||
But, the tricky bit of starting a new cluster is that one machine needs to assume the initial role of leader and will have no peers. To figure out if another machine has already started the cluster etcd needs to create the `_state` key and set its value to "started":
|
||||
|
||||
```
|
||||
curl -X PUT "http://example.com/v2/keys/_etcd/registry/${UUID}/_state?prevExist=false" -d value=started
|
||||
```
|
||||
|
||||
If this returns a `200 OK` response then this machine is the initial leader and should start with no peers configured. If, however, this returns a `412 Precondition Failed` then you need to find all of the registered peers:
|
||||
|
||||
```
|
||||
curl -X GET "http://example.com/v2/keys/_etcd/registry/${UUID}?recursive=true"
|
||||
```
|
||||
|
||||
```
|
||||
{
|
||||
"action": "get",
|
||||
"node": {
|
||||
"createdIndex": 11,
|
||||
"dir": true,
|
||||
"key": "/_etcd/registry/9D4258A5-A1D3-4074-8837-31C1E091131D",
|
||||
"modifiedIndex": 11,
|
||||
"nodes": [
|
||||
{
|
||||
"createdIndex": 16,
|
||||
"expiration": "2014-02-03T13:19:57.631253589-08:00",
|
||||
"key": "/_etcd/registry/9D4258A5-A1D3-4074-8837-31C1E091131D/peer1",
|
||||
"modifiedIndex": 16,
|
||||
"ttl": 604765,
|
||||
"value": "127.0.0.1:7001"
|
||||
},
|
||||
{
|
||||
"createdIndex": 17,
|
||||
"expiration": "2014-02-03T13:19:57.631253589-08:00",
|
||||
"key": "/_etcd/registry/9D4258A5-A1D3-4074-8837-31C1E091131D/peer2",
|
||||
"modifiedIndex": 17,
|
||||
"ttl": 604765,
|
||||
"value": "127.0.0.1:7002"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Using this information you can connect to the rest of the peers in the cluster.
|
||||
|
||||
### Heartbeating
|
||||
|
||||
At this point etcd will start heart beating to your registration URL. The
|
||||
protocol uses a heartbeat so permanently deleted nodes get slowly removed from
|
||||
the discovery information cluster.
|
||||
|
||||
The heartbeat interval is about once per day and the TTL is one week. This
|
||||
should give a sufficiently wide window to protect against a discovery service
|
||||
taking a temporary outage yet provide adequate cleanup.
|
@ -1,60 +1,42 @@
|
||||
Error Code
|
||||
======
|
||||
|
||||
This document describes the error code in **Etcd** project.
|
||||
This document describes the error code used in key space '/v2/keys'. Feel free to import 'github.com/coreos/etcd/error' to use.
|
||||
|
||||
It's categorized into four groups:
|
||||
|
||||
- Command Related Error
|
||||
|
||||
| name | code | strerror |
|
||||
|----------------------|------|-----------------------|
|
||||
| EcodeKeyNotFound | 100 | "Key not found" |
|
||||
| EcodeTestFailed | 101 | "Compare failed" |
|
||||
| EcodeNotFile | 102 | "Not a file" |
|
||||
| EcodeNotDir | 104 | "Not a directory" |
|
||||
| EcodeNodeExist | 105 | "Key already exists" |
|
||||
| EcodeRootROnly | 107 | "Root is read only" |
|
||||
| EcodeDirNotEmpty | 108 | "Directory not empty" |
|
||||
|
||||
- Post Form Related Error
|
||||
|
||||
| name | code | strerror |
|
||||
|--------------------------|------|------------------------------------------------|
|
||||
| EcodePrevValueRequired | 201 | "PrevValue is Required in POST form" |
|
||||
| EcodeTTLNaN | 202 | "The given TTL in POST form is not a number" |
|
||||
| EcodeIndexNaN | 203 | "The given index in POST form is not a number" |
|
||||
| EcodeInvalidField | 209 | "Invalid field" |
|
||||
| EcodeInvalidForm | 210 | "Invalid POST form" |
|
||||
|
||||
- Raft Related Error
|
||||
|
||||
| name | code | strerror |
|
||||
|-------------------|------|--------------------------|
|
||||
| EcodeRaftInternal | 300 | "Raft Internal Error" |
|
||||
| EcodeLeaderElect | 301 | "During Leader Election" |
|
||||
|
||||
- Etcd Related Error
|
||||
|
||||
Error code corresponding strerror
|
||||
------
|
||||
|
||||
const (
|
||||
EcodeKeyNotFound = 100
|
||||
EcodeTestFailed = 101
|
||||
EcodeNotFile = 102
|
||||
EcodeNoMorePeer = 103
|
||||
EcodeNotDir = 104
|
||||
EcodeNodeExist = 105
|
||||
EcodeKeyIsPreserved = 106
|
||||
EcodeRootROnly = 107
|
||||
|
||||
EcodeValueRequired = 200
|
||||
EcodePrevValueRequired = 201
|
||||
EcodeTTLNaN = 202
|
||||
EcodeIndexNaN = 203
|
||||
|
||||
EcodeRaftInternal = 300
|
||||
EcodeLeaderElect = 301
|
||||
|
||||
EcodeWatcherCleared = 400
|
||||
EcodeEventIndexCleared = 401
|
||||
)
|
||||
|
||||
// command related errors
|
||||
errors[100] = "Key Not Found"
|
||||
errors[101] = "Test Failed" //test and set
|
||||
errors[102] = "Not A File"
|
||||
errors[103] = "Reached the max number of peers in the cluster"
|
||||
errors[104] = "Not A Directory"
|
||||
errors[105] = "Already exists" // create
|
||||
errors[106] = "The prefix of given key is a keyword in etcd"
|
||||
errors[107] = "Root is read only"
|
||||
|
||||
// Post form related errors
|
||||
errors[200] = "Value is Required in POST form"
|
||||
errors[201] = "PrevValue is Required in POST form"
|
||||
errors[202] = "The given TTL in POST form is not a number"
|
||||
errors[203] = "The given index in POST form is not a number"
|
||||
|
||||
// raft related errors
|
||||
errors[300] = "Raft Internal Error"
|
||||
errors[301] = "During Leader Election"
|
||||
|
||||
// etcd related errors
|
||||
errors[400] = "watcher is cleared due to etcd recovery"
|
||||
errors[401] = "The event in requested index is outdated and cleared"
|
||||
| name | code | strerror |
|
||||
|-------------------------|------|--------------------------------------------------------|
|
||||
| EcodeWatcherCleared | 400 | "watcher is cleared due to etcd recovery" |
|
||||
| EcodeEventIndexCleared | 401 | "The event in requested index is outdated and cleared" |
|
||||
|
@ -1,101 +0,0 @@
|
||||
#Etcd File System
|
||||
|
||||
## Structure
|
||||
[TODO]
|
||||

|
||||
|
||||
## Node
|
||||
In **etcd**, the **node** is the base from which the filesystem is constructed.
|
||||
**etcd**'s file system is Unix-like with two kinds of nodes: file and directories.
|
||||
|
||||
- A **file node** has data associated with it.
|
||||
- A **directory node** has child nodes associated with it.
|
||||
|
||||
All nodes, regardless of type, have the following attributes and operations:
|
||||
|
||||
### Attributes:
|
||||
- **Expiration Time** [optional]
|
||||
|
||||
The node will be deleted when it expires.
|
||||
|
||||
- **ACL**
|
||||
|
||||
The path to the node's access control list.
|
||||
|
||||
### Operation:
|
||||
- **Get** (path, recursive, sorted)
|
||||
|
||||
Get the content of the node
|
||||
- If the node is a file, the data of the file will be returned.
|
||||
- If the node is a directory, the child nodes of the directory will be returned.
|
||||
- If recursive is true, it will recursively get the nodes of the directory.
|
||||
- If sorted is true, the result will be sorted based on the path.
|
||||
|
||||
- **Create** (path, value[optional], ttl [optional])
|
||||
|
||||
Create a file. Create operation will help to create intermediate directories with no expiration time.
|
||||
- If the file already exists, create will fail.
|
||||
- If the value is given, set will create a file.
|
||||
- If the value is not given, set will crate a directory.
|
||||
- If ttl is given, the node will be deleted when it expires.
|
||||
|
||||
- **Update** (path, value[optional], ttl [optional])
|
||||
|
||||
Update the content of the node.
|
||||
- If the value is given, the value of the key will be updated.
|
||||
- If ttl is given, the expiration time of the node will be updated.
|
||||
|
||||
- **Delete** (path, recursive)
|
||||
|
||||
Delete the node of given path.
|
||||
- If the node is a directory:
|
||||
- If recursive is true, the operation will delete all nodes under the directory.
|
||||
- If recursive is false, error will be returned.
|
||||
|
||||
- **TestAndSet** (path, prevValue [prevIndex], value, ttl)
|
||||
|
||||
Atomic *test and set* value to a file. If test succeeds, this operation will change the previous value of the file to the given value.
|
||||
- If the prevValue is given, it will test against previous value of
|
||||
the node.
|
||||
- If the prevValue is empty, it will test if the node is not existing.
|
||||
- If the prevValue is not empty, it will test if the prevValue is equal to the current value of the file.
|
||||
- If the prevIndex is given, it will test if the create/last modified index of the node is equal to prevIndex.
|
||||
|
||||
- **Renew** (path, ttl)
|
||||
|
||||
Set the node's expiration time to (current time + ttl)
|
||||
|
||||
## ACL
|
||||
|
||||
### Theory
|
||||
Etcd exports a Unix-like file system interface consisting of files and directories, collectively called nodes.
|
||||
Each node has various meta-data, including three names of the access control lists used to control reading, writing and changing (change ACL names for the node).
|
||||
|
||||
We are storing the ACL names for nodes under a special *ACL* directory.
|
||||
Each node has ACL name corresponding to one file within *ACL* dir.
|
||||
Unless overridden, a node naturally inherits the ACL names of its parent directory on creation.
|
||||
|
||||
For each ACL name, it has three children: *R (Reading)*, *W (Writing)*, *C (Changing)*
|
||||
|
||||
Each permission is also a node. Under the node it contains the users who have this permission for the file referring to this ACL name.
|
||||
|
||||
### Example
|
||||
[TODO]
|
||||
### Diagram
|
||||
[TODO]
|
||||
|
||||
### Interface
|
||||
|
||||
Testing permissions:
|
||||
|
||||
- (node *Node) get_perm()
|
||||
- (node *Node) has_perm(perm string, user string)
|
||||
|
||||
Setting/Changing permissions:
|
||||
|
||||
- (node *Node) set_perm(perm string)
|
||||
- (node *Node) change_ACLname(aclname string)
|
||||
|
||||
|
||||
## User Group
|
||||
[TODO]
|
BIN
Documentation/etcd-migration-steps.png
Normal file
BIN
Documentation/etcd-migration-steps.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
@ -28,4 +28,4 @@ Client is a caller of the cluster's HTTP API.
|
||||
|
||||
### Machine (deprecated)
|
||||
|
||||
The alternative of Member in etcd before 0.5
|
||||
The alternative of Member in etcd before 2.0
|
Binary file not shown.
Before Width: | Height: | Size: 26 KiB |
@ -7,6 +7,7 @@
|
||||
- [etcd-dump](https://npmjs.org/package/etcd-dump) - Command line utility for dumping/restoring etcd.
|
||||
- [etcd-fs](https://github.com/xetorthio/etcd-fs) - FUSE filesystem for etcd
|
||||
- [etcd-browser](https://github.com/henszey/etcd-browser) - A web-based key/value editor for etcd using AngularJS
|
||||
- [etcd-lock](https://github.com/datawisesystems/etcd-lock) - A lock implementation for etcd
|
||||
|
||||
**Go libraries**
|
||||
|
||||
@ -14,11 +15,10 @@
|
||||
|
||||
**Java libraries**
|
||||
|
||||
|
||||
- [boonproject/etcd](https://github.com/boonproject/boon/blob/master/etcd/README.md) - Supports v2, Async/Sync and waits
|
||||
- [justinsb/jetcd](https://github.com/justinsb/jetcd)
|
||||
- [diwakergupta/jetcd](https://github.com/diwakergupta/jetcd) - Supports v2
|
||||
- [jurmous/etcd4j](https://github.com/jurmous/etcd4j) - Supports v2
|
||||
- [jurmous/etcd4j](https://github.com/jurmous/etcd4j) - Supports v2, Async/Sync, waits and SSL
|
||||
- [AdoHe/etcd4j](http://github.com/AdoHe/etcd4j) - Supports v2 (enhance for real production cluster)
|
||||
|
||||
**Python libraries**
|
||||
@ -26,6 +26,7 @@
|
||||
- [jplana/python-etcd](https://github.com/jplana/python-etcd) - Supports v2
|
||||
- [russellhaering/txetcd](https://github.com/russellhaering/txetcd) - a Twisted Python library
|
||||
- [cholcombe973/autodock](https://github.com/cholcombe973/autodock) - A docker deployment automation tool
|
||||
- [lisael/aioetcd](https://github.com/lisael/aioetcd) - (Python 3.4+) Asyncio coroutines client (Supports v2)
|
||||
|
||||
**Node libraries**
|
||||
|
||||
@ -66,6 +67,10 @@
|
||||
**Haskell libraries**
|
||||
|
||||
- [wereHamster/etcd-hs](https://github.com/wereHamster/etcd-hs)
|
||||
|
||||
**Tcl libraries**
|
||||
|
||||
- [efrecon/etcd-tcl](https://github.com/efrecon/etcd-tcl) - Supports v2, except wait.
|
||||
|
||||
A detailed recap of client functionalities can be found in the [clients compatibility matrix][clients-matrix.md].
|
||||
|
||||
|
@ -1,118 +0,0 @@
|
||||
## Modules
|
||||
|
||||
etcd has a number of modules that are built on top of the core etcd API.
|
||||
These modules provide things like dashboards, locks and leader election (removed).
|
||||
|
||||
**Warning**: Modules are deprecated from v0.4 until we have a solid base we can apply them back onto.
|
||||
For now, we are choosing to focus on raft algorithm and core etcd to make sure that it works correctly and fast.
|
||||
And it is time consuming to maintain these modules in this period, given that etcd's API changes from time to time.
|
||||
Moreover, the lock module has some unfixed bugs, which may mislead users.
|
||||
But we also notice that these modules are popular and useful, and plan to add them back with full functionality as soon as possible.
|
||||
|
||||
### Dashboard
|
||||
|
||||
An HTML dashboard can be found at `http://127.0.0.1:4001/mod/dashboard/`.
|
||||
This dashboard is compiled into the etcd binary and uses the same API as regular etcd clients.
|
||||
|
||||
Use the `-cors='*'` flag to allow your browser to request information from the current master as it changes.
|
||||
|
||||
### Lock
|
||||
|
||||
The Lock module implements a fair lock that can be used when lots of clients want access to a single resource.
|
||||
A lock can be associated with a value.
|
||||
The value is unique so if a lock tries to request a value that is already queued for a lock then it will find it and watch until that value obtains the lock.
|
||||
You may supply a `timeout` which will cancel the lock request if it is not obtained within `timeout` seconds. If `timeout` is not supplied, it is presumed to be infinite. If `timeout` is `0`, the lock request will fail if it is not immediately acquired.
|
||||
If you lock the same value on a key from two separate curl sessions they'll both return at the same time.
|
||||
|
||||
Here's the API:
|
||||
|
||||
**Acquire a lock (with no value) for "customer1"**
|
||||
|
||||
```sh
|
||||
curl -X POST http://127.0.0.1:4001/mod/v2/lock/customer1?ttl=60
|
||||
```
|
||||
|
||||
**Acquire a lock for "customer1" that is associated with the value "bar"**
|
||||
|
||||
```sh
|
||||
curl -X POST http://127.0.0.1:4001/mod/v2/lock/customer1?ttl=60 -d value=bar
|
||||
```
|
||||
|
||||
**Acquire a lock for "customer1" that is associated with the value "bar" only if it is done within 2 seconds**
|
||||
|
||||
```sh
|
||||
curl -X POST http://127.0.0.1:4001/mod/v2/lock/customer1?ttl=60 -d value=bar -d timeout=2
|
||||
```
|
||||
|
||||
**Renew the TTL on the "customer1" lock for index 2**
|
||||
|
||||
```sh
|
||||
curl -X PUT http://127.0.0.1:4001/mod/v2/lock/customer1?ttl=60 -d index=2
|
||||
```
|
||||
|
||||
**Renew the TTL on the "customer1" lock for value "bar"**
|
||||
|
||||
```sh
|
||||
curl -X PUT http://127.0.0.1:4001/mod/v2/lock/customer1?ttl=60 -d value=bar
|
||||
```
|
||||
|
||||
**Retrieve the current value for the "customer1" lock.**
|
||||
|
||||
```sh
|
||||
curl http://127.0.0.1:4001/mod/v2/lock/customer1
|
||||
```
|
||||
|
||||
**Retrieve the current index for the "customer1" lock**
|
||||
|
||||
```sh
|
||||
curl http://127.0.0.1:4001/mod/v2/lock/customer1?field=index
|
||||
```
|
||||
|
||||
**Delete the "customer1" lock with the index 2**
|
||||
|
||||
```sh
|
||||
curl -X DELETE http://127.0.0.1:4001/mod/v2/lock/customer1?index=2
|
||||
```
|
||||
|
||||
**Delete the "customer1" lock with the value "bar"**
|
||||
|
||||
```sh
|
||||
curl -X DELETE http://127.0.0.1:4001/mod/v2/lock/customer1?value=bar
|
||||
```
|
||||
|
||||
|
||||
### Leader Election (Deprecated and Removed in 0.4)
|
||||
|
||||
The Leader Election module wraps the Lock module to allow clients to come to consensus on a single value.
|
||||
This is useful when you want one server to process at a time but allow other servers to fail over.
|
||||
The API is similar to the Lock module but is limited to simple strings values.
|
||||
|
||||
Here's the API:
|
||||
|
||||
**Attempt to set a value for the "order_processing" leader key:**
|
||||
|
||||
```sh
|
||||
curl -X PUT http://127.0.0.1:4001/mod/v2/leader/order_processing?ttl=60 -d name=myserver1.foo.com
|
||||
```
|
||||
|
||||
**Retrieve the current value for the "order_processing" leader key:**
|
||||
|
||||
```sh
|
||||
curl http://127.0.0.1:4001/mod/v2/leader/order_processing
|
||||
myserver1.foo.com
|
||||
```
|
||||
|
||||
**Remove a value from the "order_processing" leader key:**
|
||||
|
||||
```sh
|
||||
curl -X DELETE http://127.0.0.1:4001/mod/v2/leader/order_processing?name=myserver1.foo.com
|
||||
```
|
||||
|
||||
If multiple clients attempt to set the value for a key then only one will succeed.
|
||||
The other clients will hang until the current value is removed because of TTL or because of a `DELETE` operation.
|
||||
Multiple clients can submit the same value and will all be notified when that value succeeds.
|
||||
|
||||
To update the TTL of a value simply reissue the same `PUT` command that you used to set the value.
|
||||
|
||||
|
||||
|
@ -1,38 +0,0 @@
|
||||
# Optimal etcd Cluster Size
|
||||
|
||||
etcd's Raft consensus algorithm is most efficient in small clusters between 3 and 9 peers. For clusters larger than 9, etcd will select a subset of instances to participate in the algorithm in order to keep it efficient. The end of this document briefly explores how etcd works internally and why these choices have been made.
|
||||
|
||||
## Cluster Management
|
||||
|
||||
You can manage the active cluster size through the [cluster config API](https://github.com/coreos/etcd/blob/master/Documentation/api.md#cluster-config). `activeSize` represents the etcd peers allowed to actively participate in the consensus algorithm.
|
||||
|
||||
If the total number of etcd instances exceeds this number, additional peers are started as [standbys](https://github.com/coreos/etcd/blob/master/Documentation/design/standbys.md), which can be promoted to active participation if one of the existing active instances has failed or been removed.
|
||||
|
||||
## Internals of etcd
|
||||
|
||||
### Writing to etcd
|
||||
|
||||
Writes to an etcd peer are always redirected to the leader of the cluster and distributed to all of the peers immediately. A write is only considered successful when a majority of the peers acknowledge the write.
|
||||
|
||||
For example, in a cluster with 5 peers, a write operation is only as fast as the 3rd fastest machine. This is the main reason for keeping the number of active peers below 9. In practice, you only need to worry about write performance in high latency environments such as a cluster spanning multiple data centers.
|
||||
|
||||
### Leader Election
|
||||
|
||||
The leader election process is similar to writing a key — a majority of the active peers must acknowledge the new leader before cluster operations can continue. The longer each peer takes to elect a new leader means you have to wait longer before you can write to the cluster again. In low latency environments this process takes milliseconds.
|
||||
|
||||
### Odd Active Cluster Size
|
||||
|
||||
The other important cluster optimization is to always have an odd active cluster size (i.e. `activeSize`). Adding an odd node to the number of peers doesn't change the size of the majority and therefore doesn't increase the total latency of the majority as described above. But, you gain a higher tolerance for peer failure by adding the extra machine. You can see this in practice when comparing two even and odd sized clusters:
|
||||
|
||||
| Active Peers | Majority | Failure Tolerance |
|
||||
|--------------|------------|-------------------|
|
||||
| 1 peers | 1 peers | None |
|
||||
| 3 peers | 2 peers | 1 peer |
|
||||
| 4 peers | 3 peers | 1 peer |
|
||||
| 5 peers | 3 peers | **2 peers** |
|
||||
| 6 peers | 4 peers | 2 peers |
|
||||
| 7 peers | 4 peers | **3 peers** |
|
||||
| 8 peers | 5 peers | 3 peers |
|
||||
| 9 peers | 5 peers | **4 peers** |
|
||||
|
||||
As you can see, adding another peer to bring the number of active peers up to an odd size is always worth it. During a network partition, an odd number of active peers also guarantees that there will almost always be a majority of the cluster that can continue to operate and be the source of truth when the partition ends.
|
119
Documentation/other_apis.md
Normal file
119
Documentation/other_apis.md
Normal file
@ -0,0 +1,119 @@
|
||||
## Members API
|
||||
|
||||
* [List members](#list-members)
|
||||
* [Add a member](#add-a-member)
|
||||
* [Delete a member](#delete-a-member)
|
||||
* [Change the peer urls of a member](#change-the-peer-urls-of-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
|
||||
|
||||
```sh
|
||||
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:2380"]}
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```sh
|
||||
curl http://10.0.0.10:2379/v2/members -XPOST \
|
||||
-H "Content-Type: application/json" -d '{"peerURLs":["http://10.0.0.10:2380"]}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "3777296169",
|
||||
"peerURLs": [
|
||||
"http://10.0.0.10:2380"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Delete a member
|
||||
|
||||
Remove a member from the cluster. The member ID must be a hex-encoded uint64.
|
||||
Returns 204 with empty content 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
|
||||
|
||||
```sh
|
||||
curl http://10.0.0.10:2379/v2/members/272e204152 -XDELETE
|
||||
```
|
||||
|
||||
## Change the peer urls of a member
|
||||
|
||||
Change the peer urls of a given mamber. The member ID must be a hex-encoded uint64. Returns 204 with empty content 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 does not exist in the cluster an HTTP 404 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
|
||||
|
||||
```
|
||||
PUT /v2/members/<id> HTTP/1.1
|
||||
|
||||
{"peerURLs": ["http://10.0.0.10:2380"]}
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```sh
|
||||
curl http://10.0.0.10:2379/v2/members/272e204152 -XPUT \
|
||||
-H "Content-Type: application/json" -d '{"peerURLs":["http://10.0.0.10:2380"]}'
|
||||
```
|
@ -2,6 +2,3 @@ etcd is being used successfully by many companies in production. It is,
|
||||
however, under active development and systems like etcd are difficult to get
|
||||
correct. If you are comfortable with bleeding-edge software please use etcd and
|
||||
provide us with the feedback and testing young software needs.
|
||||
|
||||
When the etcd team feels confident removing this warning we will release etcd
|
||||
1.0.
|
||||
|
32
Documentation/proxy.md
Normal file
32
Documentation/proxy.md
Normal 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/clustering.md#discovery
|
151
Documentation/runtime-configuration.md
Normal file
151
Documentation/runtime-configuration.md
Normal file
@ -0,0 +1,151 @@
|
||||
## Runtime Reconfiguration
|
||||
|
||||
etcd comes with support for incremental runtime reconfiguration, which allows users to update the membership of the cluster at run time.
|
||||
|
||||
Reconfiguration requests can only be processed when the the majority of the cluster members are functioning. It is **highly recommended** to always have a cluster size greater than two in production. It is unsafe to remove a member from a two member cluster. The majority of a two member cluster is also two. If there is a failure during the removal process, the cluster might not able to make progress and need to [restart from majority failure][majority failure].
|
||||
|
||||
[majority failure]: #restart-cluster-from-majority-failure
|
||||
|
||||
## Reconfiguration Use Cases
|
||||
|
||||
Let us walk through some common reasons for reconfiguring a cluster. Most of these just involve combinations of adding or removing a member, which are explained below under [Cluster Reconfiguration Operations](#cluster-reconfiguration-operations).
|
||||
|
||||
### Cycle or Upgrade Multiple Machines
|
||||
|
||||
If you need to move multiple members of your cluster due to planned maintenance (hardware upgrades, network downtime, etc.), it is recommended to modify members one at a time.
|
||||
|
||||
It is safe to remove the leader, however there is a brief period of downtime while the election process takes place. If your cluster holds more than 50MB, it is recommended to [migrate the member's data directory][member migration].
|
||||
|
||||
[member migration]: admin_guide.md#member-migration
|
||||
|
||||
### Change the Cluster Size
|
||||
|
||||
Increasing the cluster size can enhance [failure tolerance][fault tolerance table] and provide better read performance. Since clients can read from any member, increasing the number of members increases the overall read throughput.
|
||||
|
||||
Decreasing the cluster size can improve the write performance of a cluster, with a trade-off of decreased resilience. Writes into the cluster are replicated to a majority of members of the cluster before considered committed. Decreasing the cluster size lowers the majority, and each write is committed more quickly.
|
||||
|
||||
[fault tolerance table]: admin_guide.md#fault-tolerance-table
|
||||
|
||||
### Replace A Failed Machine
|
||||
|
||||
If a machine fails due to hardware failure, data directory corruption, or some other fatal situation, it should be replaced as soon as possible. Machines that have failed but haven't been removed adversely affect your quorum and reduce the tolerance for an additional failure.
|
||||
|
||||
To replace the machine, follow the instructions for [removing the member][remove member] from the cluster, and then [add a new member][add member] in its place. If your cluster holds more than 50MB, it is recommended to [migrate the failed member's data directory][member migration] if you can still access it.
|
||||
|
||||
[remove member]: #remove-a-member
|
||||
[add member]: #add-a-new-member
|
||||
|
||||
### 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][disaster recovery], forcing a single member to act as the leader, and finally using runtime configuration to [add new members][add member] to this new cluster one at a time.
|
||||
|
||||
[add member]: #add-a-new-member
|
||||
[disaster recovery]: admin_guide.md#disaster-recovery
|
||||
|
||||
## 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/other_apis.md).
|
||||
|
||||
### Remove a Member
|
||||
|
||||
First, we need to find the target member's ID. You can list all members with `etcdctl`:
|
||||
|
||||
```
|
||||
$ 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.
|
||||
```
|
||||
|
||||
It is safe to remove the leader, however the cluster will be inactive while a new leader is elected. This duration is normally the period of election timeout plus the voting process.
|
||||
|
||||
### Add a New 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/other_apis.md#post-v2members) or the `etcdctl member add` command.
|
||||
* Start the new member with the new cluster configuration, including a list of the updated members (existing members + the new member).
|
||||
|
||||
Using `etcdctl` let's add the new member to the cluster by specifing its [name](configuration.md#-name) and [advertised peer URLs](configuration.md#-initial-advertise-peer-urls):
|
||||
|
||||
```
|
||||
$ 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
|
||||
```
|
||||
|
||||
`etcdctl` has informed the cluster about the new member and printed out the environment variables needed to successfully start it.
|
||||
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 a single member at a time and verify it starts correctly before adding more new members.
|
||||
If you add a new 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. You will only see this behavior between the time `etcdctl member add` informs the cluster about the new member and the new member successfully establishing a connection to the existing one.
|
||||
|
||||
#### 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
|
||||
```
|
@ -1,8 +1,8 @@
|
||||
# Etcd security model
|
||||
# security model
|
||||
|
||||
Etcd supports SSL/TLS as well as authentication through client certificates, both for clients to server as well as peer (server to server / cluster) communication.
|
||||
etcd supports SSL/TLS as well as authentication through client certificates, both for clients to server as well as peer (server to server / cluster) communication.
|
||||
|
||||
To get up and running you first need to have a CA certificate and a signed key pair for your node. It is recommended to create and sign a new key pair for every node in a cluster.
|
||||
To get up and running you first need to have a CA certificate and a signed key pair for one member. It is recommended to create and sign a new key pair for every member in a cluster.
|
||||
|
||||
For convenience the [etcd-ca](https://github.com/coreos/etcd-ca) tool provides an easy interface to certificate generation, alternatively this site provides a good reference on how to generate self-signed key pairs:
|
||||
|
||||
@ -10,38 +10,44 @@ http://www.g-loaded.eu/2005/11/10/be-your-own-ca/
|
||||
|
||||
## Basic setup
|
||||
|
||||
Etcd takes several certificate related configuration options, either through command-line flags or environment variables:
|
||||
etcd takes several certificate related configuration options, either through command-line flags or environment variables:
|
||||
|
||||
**Client-to-server communication:**
|
||||
|
||||
`--cert-file=<path>`: Certificate used for SSL/TLS connections **to** etcd. When this option is set, you can reach etcd through HTTPS - for example at `https://127.0.0.1:4001`
|
||||
`--key-file=<path>`: Key for the certificate. Must be unencrypted.
|
||||
`--cert-file=<path>`: Certificate used for SSL/TLS connections **to** etcd. When this option is set, you can set advertise-client-urls using HTTPS schema.
|
||||
|
||||
`--key-file=<path>`: Key for the certificate. Must be unencrypted.
|
||||
|
||||
`--ca-file=<path>`: When this is set etcd will check all incoming HTTPS requests for a client certificate signed by the supplied CA, requests that don't supply a valid client certificate will fail.
|
||||
|
||||
**Peer (server-to-server / cluster) communication:**
|
||||
|
||||
The peer options work the same way as the client-to-server options:
|
||||
|
||||
`--peer-cert-file=<path>`: Certificate used for SSL/TLS connections between peers. This will be used both for listening on the peer address as well as sending requests to other peers.
|
||||
`--peer-key-file=<path>`: Key for the certificate. Must be unencrypted.
|
||||
`--peer-cert-file=<path>`: Certificate used for SSL/TLS connections between peers. This will be used both for listening on the peer address as well as sending requests to other peers.
|
||||
|
||||
`--peer-key-file=<path>`: Key for the certificate. Must be unencrypted.
|
||||
|
||||
`--peer-ca-file=<path>`: When set, etcd will check all incoming peer requests from the cluster for valid client certificates signed by the supplied CA.
|
||||
|
||||
If either a client-to-server or peer certificate is supplied the key must also be set. All of these configuration options are also available through the environment variables, `ETCD_CA_FILE`, `ETCD_PEER_CA_FILE` and so on.
|
||||
|
||||
## Example 1: Client-to-server transport security with HTTPS
|
||||
|
||||
For this you need your CA certificate (`ca.crt`) and signed key pair (`server.crt`, `server.key`) ready. If you just want to test the functionality, there are example certificates provided in the [etcd git repository](https://github.com/coreos/etcd/tree/master/fixtures/ca) (namely `server.crt` and `server.key.insecure`).
|
||||
For this you need your CA certificate (`ca.crt`) and signed key pair (`server.crt`, `server.key`) ready.
|
||||
|
||||
Assuming you have these files ready, let's configure etcd to use them to provide simple HTTPS transport security.
|
||||
Let us configure etcd to provide simple HTTPS transport security step by step:
|
||||
|
||||
```sh
|
||||
etcd -name machine0 -data-dir machine0 -cert-file=/path/to/server.crt -key-file=/path/to/server.key
|
||||
$ etcd -name infra0 -data-dir infra0 \
|
||||
-cert-file=/path/to/server.crt -key-file=/path/to/server.key \
|
||||
-advertise-client-urls=https://127.0.0.1:2379 -listen-client-urls=https://127.0.0.1:2379
|
||||
```
|
||||
|
||||
This should start up fine and you can now test the configuration by speaking HTTPS to etcd:
|
||||
|
||||
```sh
|
||||
curl --cacert /path/to/ca.crt https://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar -v
|
||||
$ curl --cacert /path/to/ca.crt https://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -v
|
||||
```
|
||||
|
||||
You should be able to see the handshake succeed. Because we use self-signed certificates with our own certificate authorities you need to provide the CA to curl using the `--cacert` option. Another possibility would be to add your CA certificate to the trusted certificates on your system (usually in `/etc/ssl/certs`).
|
||||
@ -61,16 +67,17 @@ The clients will provide their certificates to the server and the server will ch
|
||||
You need the same files mentioned in the first example for this, as well as a key pair for the client (`client.crt`, `client.key`) signed by the same certificate authority.
|
||||
|
||||
```sh
|
||||
etcd -name machine0 -data-dir machine0 -ca-file=/path/to/ca.crt -cert-file=/path/to/server.crt -key-file=/path/to/server.key
|
||||
$ etcd -name infra0 -data-dir infra0 \
|
||||
-ca-file=/path/to/ca.crt -cert-file=/path/to/server.crt -key-file=/path/to/server.key \
|
||||
-advertise-client-urls https://127.0.0.1:2379 -listen-client-urls https://127.0.0.1:2379
|
||||
```
|
||||
|
||||
Notice that the addition of the `-ca-file` option automatically enables client certificate checking.
|
||||
|
||||
|
||||
Now try the same request as above to this server:
|
||||
|
||||
```sh
|
||||
curl --cacert /path/to/ca.crt https://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar -v
|
||||
$ curl --cacert /path/to/ca.crt https://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -v
|
||||
```
|
||||
|
||||
The request should be rejected by the server:
|
||||
@ -84,7 +91,8 @@ routines:SSL3_READ_BYTES:sslv3 alert bad certificate
|
||||
To make it succeed, we need to give the CA signed client certificate to the server:
|
||||
|
||||
```sh
|
||||
curl --cacert /path/to/ca.crt --cert /path/to/client.crt --key /path/to/client.key -L https://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar -v
|
||||
$ curl --cacert /path/to/ca.crt --cert /path/to/client.crt --key /path/to/client.key \
|
||||
-L https://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -v
|
||||
```
|
||||
|
||||
You should able to see:
|
||||
@ -112,22 +120,28 @@ And also the response from the server:
|
||||
|
||||
## Example 3: Transport security & client certificates in a cluster
|
||||
|
||||
Etcd supports the same model as above for **peer communication**, that means the communication between etcd nodes in a cluster.
|
||||
etcd supports the same model as above for **peer communication**, that means the communication between etcd members in a cluster.
|
||||
|
||||
Assuming we have our `ca.crt` and two nodes with their own keypairs (`node1.crt` & `node1.key`, `node2.crt` & `node2.key`) signed by this CA, we launch etcd as follows:
|
||||
Assuming we have our `ca.crt` and two members with their own keypairs (`member1.crt` & `member1.key`, `member2.crt` & `member2.key`) signed by this CA, we launch etcd as follows:
|
||||
|
||||
|
||||
```sh
|
||||
DISCOVERY_URL=... # from https://discovery.etcd.io/new
|
||||
|
||||
# Node1
|
||||
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}
|
||||
# member1
|
||||
$ etcd -name infra1 -data-dir infra1 \
|
||||
-ca-file=/path/to/ca.crt -cert-file=/path/to/member1.crt -key-file=/path/to/member1.key \
|
||||
-initial-advertise-peer-urls=https://10.0.1.10:2380 -listen-peer-urls=https://10.0.1.10:2380 \
|
||||
-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}
|
||||
# member2
|
||||
$ etcd -name infra2 -data-dir infra2 \
|
||||
-ca-file=/path/to/ca.crt -cert-file=/path/to/member2.crt -key-file=/path/to/member2.key \
|
||||
-initial-advertise-peer-urls=https://10.0.1.11:2380 -listen-peer-urls=https://10.0.1.11:2380 \
|
||||
-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.
|
||||
The etcd members will form a cluster and all communication between members 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.
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
@ -150,10 +164,10 @@ Add the following section to your openssl.cnf:
|
||||
When creating the cert be sure to reference it in the `-extensions` flag:
|
||||
|
||||
```
|
||||
openssl ca -config openssl.cnf -policy policy_anything -extensions ssl_client -out certs/machine.crt -infiles machine.csr
|
||||
$ openssl ca -config openssl.cnf -policy policy_anything -extensions ssl_client -out certs/machine.crt -infiles machine.csr
|
||||
```
|
||||
|
||||
### With peer certificate authentication I receive "certificate is valid for 127.0.0.1, not $MY_IP"
|
||||
Make sure that you sign your certificates with a Subject Name your node's public IP address. The `etcd-ca` tool for example provides an `--ip=` option for its `new-cert` command.
|
||||
Make sure that you sign your certificates with a Subject Name your member's public IP address. The `etcd-ca` tool for example provides an `--ip=` option for its `new-cert` command.
|
||||
|
||||
If you need your certificate to be signed for your node's FQDN in its Subject Name then you could use Subject Alternative Names (short IP SNAs) to add your IP address. This is not [currently supported](https://github.com/coreos/etcd-ca/issues/29) by `etcd-ca` but can be done [with openssl](http://wiki.cacert.org/FAQ/subjectAltName).
|
||||
If you need your certificate to be signed for your member's FQDN in its Subject Name then you could use Subject Alternative Names (short IP SANs) to add your IP address. The `etcd-ca` tool provides `--domain=` option for its `new-cert` command, and openssl can make [it](http://wiki.cacert.org/FAQ/subjectAltName) too.
|
||||
|
@ -11,11 +11,11 @@ The underlying distributed consensus protocol relies on two separate time parame
|
||||
The first parameter is called the *Heartbeat Interval*.
|
||||
This is the frequency with which the leader will notify followers that it is still the leader.
|
||||
etcd batches commands together for higher throughput so this heartbeat interval is also a delay for how long it takes for commands to be committed.
|
||||
By default, etcd uses a `50ms` heartbeat interval.
|
||||
By default, etcd uses a `100ms` heartbeat interval.
|
||||
|
||||
The second parameter is the *Election Timeout*.
|
||||
This timeout is how long a follower node will go without hearing a heartbeat before attempting to become leader itself.
|
||||
By default, etcd uses a `200ms` election timeout.
|
||||
By default, etcd uses a `1000ms` election timeout.
|
||||
|
||||
Adjusting these values is a trade off.
|
||||
Lowering the heartbeat interval will cause individual commands to be committed faster but it will lower the overall throughput of etcd.
|
||||
@ -32,23 +32,14 @@ You can override the default values on the command line:
|
||||
|
||||
```sh
|
||||
# Command line arguments:
|
||||
$ etcd -peer-heartbeat-interval=100 -peer-election-timeout=500
|
||||
$ etcd -heartbeat-interval=100 -election-timeout=500
|
||||
|
||||
# Environment variables:
|
||||
$ ETCD_PEER_HEARTBEAT_INTERVAL=100 ETCD_PEER_ELECTION_TIMEOUT=500 etcd
|
||||
```
|
||||
|
||||
Or you can set the values within the configuration file:
|
||||
|
||||
```toml
|
||||
[peer]
|
||||
heartbeat_interval = 100
|
||||
election_timeout = 500
|
||||
$ ETCD_HEARTBEAT_INTERVAL=100 ETCD_ELECTION_TIMEOUT=500 etcd
|
||||
```
|
||||
|
||||
The values are specified in milliseconds.
|
||||
|
||||
|
||||
### Snapshots
|
||||
|
||||
etcd appends all key changes to a log file.
|
||||
@ -72,12 +63,6 @@ $ etcd -snapshot-count=5000
|
||||
$ ETCD_SNAPSHOT_COUNT=5000 etcd
|
||||
```
|
||||
|
||||
Or you can change the setting in the configuration file:
|
||||
|
||||
```toml
|
||||
snapshot_count = 5000
|
||||
```
|
||||
|
||||
You can also disable snapshotting by adding the following to your command line:
|
||||
|
||||
```sh
|
||||
@ -87,9 +72,3 @@ $ etcd -snapshot false
|
||||
# Environment variables:
|
||||
$ ETCD_SNAPSHOT=false etcd
|
||||
```
|
||||
|
||||
You can also disable snapshotting within the configuration file:
|
||||
|
||||
```toml
|
||||
snapshot = false
|
||||
```
|
||||
|
@ -1,17 +0,0 @@
|
||||
# Upgrading an Existing Cluster
|
||||
|
||||
etcd clusters can be upgraded by doing a rolling upgrade or all at once. We make every effort to test this process, but please be sure to backup your data [by etcd-dump](https://github.com/AaronO/etcd-dump), or make a copy of data directory beforehand.
|
||||
|
||||
## Upgrade Process
|
||||
|
||||
- Stop the old etcd processes
|
||||
- Upgrade the etcd binary
|
||||
- Restart the etcd instance using the original --name, --address, --peer-address and --data-dir.
|
||||
|
||||
## Rolling Upgrade
|
||||
|
||||
During an upgrade, etcd clusters are designed to continue working in a mix of old and new versions. It's recommended to converge on the new version quickly. Using new API features before the entire cluster has been upgraded is only supported as a best effort. Each instance's version can be found with `curl http://127.0.0.1:4001/version`.
|
||||
|
||||
## All at Once
|
||||
|
||||
If downtime is not an issue, the easiest way to upgrade your cluster is to shutdown all of the etcd instances and restart them with the new binary. The current state of the cluster is saved to disk and will be loaded into the cluster when it restarts.
|
16
Godeps/Godeps.json
generated
16
Godeps/Godeps.json
generated
@ -1,15 +1,10 @@
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd",
|
||||
"GoVersion": "go1.3.1",
|
||||
"GoVersion": "go1.4.1",
|
||||
"Packages": [
|
||||
"./..."
|
||||
],
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "code.google.com/p/go.net/context",
|
||||
"Comment": "null-144",
|
||||
"Rev": "ad01a6fcc8a19d3a4478c836895ffe883bd2ceab"
|
||||
},
|
||||
{
|
||||
"ImportPath": "code.google.com/p/gogoprotobuf/proto",
|
||||
"Rev": "7fd1620f09261338b6b1ca1289ace83aee0ec946"
|
||||
@ -21,8 +16,8 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-etcd/etcd",
|
||||
"Comment": "v0.2.0-rc1-127-g6fe04d5",
|
||||
"Rev": "6fe04d580dfb71c9e34cbce2f4df9eefd1e1241e"
|
||||
"Comment": "v0.2.0-rc1-130-g6aa2da5",
|
||||
"Rev": "6aa2da5a7a905609c93036b9307185a04a5a84a5"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/jonboulle/clockwork",
|
||||
@ -31,6 +26,11 @@
|
||||
{
|
||||
"ImportPath": "github.com/stretchr/testify/assert",
|
||||
"Rev": "9cc77fa25329013ce07362c7742952ff887361f2"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/context",
|
||||
"Comment": "null-220",
|
||||
"Rev": "c5a46024776ec35eb562fa9226968b9d543bb13a"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
2
Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go
generated
vendored
2
Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go
generated
vendored
@ -5,7 +5,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||
)
|
||||
|
||||
func ExampleApp() {
|
||||
|
2
Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go
generated
vendored
2
Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go
generated
vendored
@ -3,7 +3,7 @@ package cli_test
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
|
2
Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go
generated
vendored
2
Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go
generated
vendored
@ -4,7 +4,7 @@ import (
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||
)
|
||||
|
||||
func TestCommandDoNotIgnoreFlags(t *testing.T) {
|
||||
|
2
Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go
generated
vendored
2
Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go
generated
vendored
@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||
)
|
||||
|
||||
func TestNewContext(t *testing.T) {
|
||||
|
2
Godeps/_workspace/src/github.com/codegangsta/cli/flag_test.go
generated
vendored
2
Godeps/_workspace/src/github.com/codegangsta/cli/flag_test.go
generated
vendored
@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||
)
|
||||
|
||||
var boolFlagTests = []struct {
|
||||
|
6
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/requests.go
generated
vendored
6
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/requests.go
generated
vendored
@ -379,11 +379,13 @@ func buildValues(value string, ttl uint64) url.Values {
|
||||
return v
|
||||
}
|
||||
|
||||
// convert key string to http path exclude version
|
||||
// convert key string to http path exclude version, including URL escaping
|
||||
// for example: key[foo] -> path[keys/foo]
|
||||
// key[/%z] -> path[keys/%25z]
|
||||
// key[/] -> path[keys/]
|
||||
func keyToPath(key string) string {
|
||||
p := path.Join("keys", key)
|
||||
// URL-escape our key, except for slashes
|
||||
p := strings.Replace(url.QueryEscape(path.Join("keys", key)), "%2F", "/", -1)
|
||||
|
||||
// corner case: if key is "/" or "//" ect
|
||||
// path join will clear the tailing "/"
|
||||
|
22
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/requests_test.go
generated
vendored
Normal file
22
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/requests_test.go
generated
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
package etcd
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestKeyToPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
key string
|
||||
wpath string
|
||||
}{
|
||||
{"", "keys/"},
|
||||
{"foo", "keys/foo"},
|
||||
{"foo/bar", "keys/foo/bar"},
|
||||
{"%z", "keys/%25z"},
|
||||
{"/", "keys/"},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
path := keyToPath(tt.key)
|
||||
if path != tt.wpath {
|
||||
t.Errorf("#%d: path = %s, want %s", i, path, tt.wpath)
|
||||
}
|
||||
}
|
||||
}
|
@ -108,7 +108,7 @@ type Context interface {
|
||||
// // Package user defines a User type that's stored in Contexts.
|
||||
// package user
|
||||
//
|
||||
// import "code.google.com/p/go.net/context"
|
||||
// import "golang.org/x/net/context"
|
||||
//
|
||||
// // User is the type of value stored in the Contexts.
|
||||
// type User struct {...}
|
||||
@ -124,7 +124,7 @@ type Context interface {
|
||||
//
|
||||
// // NewContext returns a new Context that carries value u.
|
||||
// func NewContext(ctx context.Context, u *User) context.Context {
|
||||
// return context.WithValue(userKey, u)
|
||||
// return context.WithValue(ctx, userKey, u)
|
||||
// }
|
||||
//
|
||||
// // FromContext returns the User value stored in ctx, if any.
|
||||
@ -142,27 +142,28 @@ var Canceled = errors.New("context canceled")
|
||||
// deadline passes.
|
||||
var DeadlineExceeded = errors.New("context deadline exceeded")
|
||||
|
||||
// An emptyCtx is never canceled, has no values, and has no deadline.
|
||||
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
|
||||
// struct{}, since vars of this type must have distinct addresses.
|
||||
type emptyCtx int
|
||||
|
||||
func (emptyCtx) Deadline() (deadline time.Time, ok bool) {
|
||||
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (emptyCtx) Done() <-chan struct{} {
|
||||
func (*emptyCtx) Done() <-chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (emptyCtx) Err() error {
|
||||
func (*emptyCtx) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (emptyCtx) Value(key interface{}) interface{} {
|
||||
func (*emptyCtx) Value(key interface{}) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n emptyCtx) String() string {
|
||||
switch n {
|
||||
func (e *emptyCtx) String() string {
|
||||
switch e {
|
||||
case background:
|
||||
return "context.Background"
|
||||
case todo:
|
||||
@ -171,9 +172,9 @@ func (n emptyCtx) String() string {
|
||||
return "unknown empty Context"
|
||||
}
|
||||
|
||||
const (
|
||||
background emptyCtx = 1
|
||||
todo emptyCtx = 2
|
||||
var (
|
||||
background = new(emptyCtx)
|
||||
todo = new(emptyCtx)
|
||||
)
|
||||
|
||||
// Background returns a non-nil, empty Context. It is never canceled, has no
|
@ -365,7 +365,7 @@ func TestAllocs(t *testing.T) {
|
||||
c := WithValue(bg, k1, nil)
|
||||
c.Value(k1)
|
||||
},
|
||||
limit: 1,
|
||||
limit: 3,
|
||||
gccgoLimit: 3,
|
||||
},
|
||||
{
|
@ -8,7 +8,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func ExampleWithTimeout() {
|
8
Procfile
8
Procfile
@ -1,5 +1,5 @@
|
||||
# Use goreman to run `go get github.com/mattn/goreman`
|
||||
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 -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://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-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://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-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'
|
||||
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 -name proxy1 -proxy=on -bind-addr 127.0.0.1:8080 -initial-cluster 'infra1=http://localhost:7001,infra2=http://localhost:7002,infra3=http://localhost:7003'
|
||||
|
59
README.md
59
README.md
@ -3,18 +3,9 @@
|
||||
[](https://travis-ci.org/coreos/etcd)
|
||||
[](https://quay.io/repository/coreos/etcd-git)
|
||||
|
||||
### WARNING ###
|
||||

|
||||
|
||||
The current `master` branch of etcd is under heavy development in anticipation of the forthcoming 0.5.0 release.
|
||||
|
||||
It is strongly recommended that users work with the latest 0.4.x release (0.4.6), which can be found on the [releases](https://github.com/coreos/etcd/releases) page.
|
||||
|
||||
Unless otherwise noted, the etcd documentation refers to configuring and running 0.4.x releases.
|
||||
|
||||
## README version 0.4.6
|
||||
|
||||
A highly-available key value store for shared configuration and service discovery.
|
||||
etcd is inspired by [Apache ZooKeeper][zookeeper] and [doozer][doozer], with a focus on being:
|
||||
etcd is a distributed, consistent key value store for shared configuration and service discovery with a focus on being:
|
||||
|
||||
* *Simple*: curl'able user facing API (HTTP+JSON)
|
||||
* *Secure*: optional SSL client cert authentication
|
||||
@ -29,7 +20,7 @@ Or feel free to just use curl, as in the examples below.
|
||||
[zookeeper]: http://zookeeper.apache.org/
|
||||
[doozer]: https://github.com/ha/doozerd
|
||||
[raft]: http://raftconsensus.github.io/
|
||||
[etcdctl]: http://github.com/coreos/etcdctl/
|
||||
[etcdctl]: https://github.com/coreos/etcd/tree/master/etcdctl
|
||||
|
||||
If you're considering etcd for production use, please see: [production-ready.md](./Documentation/production-ready.md)
|
||||
|
||||
@ -43,7 +34,7 @@ The latest release and setup instructions are available at [GitHub][github-relea
|
||||
|
||||
### Running etcd
|
||||
|
||||
First start a single-machine cluster of etcd:
|
||||
First start a single-member cluster of etcd:
|
||||
|
||||
```sh
|
||||
./bin/etcd
|
||||
@ -58,27 +49,41 @@ curl -L http://127.0.0.1:4001/v2/keys/mykey -XPUT -d value="this is awesome"
|
||||
curl -L http://127.0.0.1:4001/v2/keys/mykey
|
||||
```
|
||||
|
||||
You have successfully started an etcd on a single machine and written a key to the store. Now it's time to dig into the full etcd API and other guides.
|
||||
You have successfully started an etcd and written a key to the store.
|
||||
|
||||
### Running local etcd cluster
|
||||
|
||||
First install [goreman](https://github.com/mattn/goreman), which manages Procfile-based applications.
|
||||
|
||||
Our [Profile script](./Procfile) will set up a local example cluster. You can start it with:
|
||||
|
||||
```sh
|
||||
goreman start
|
||||
```
|
||||
|
||||
This will bring up 3 etcd members `infra1`, `infra2` and `infra3` and etcd proxy `proxy`, which runs locally and composes a cluster.
|
||||
|
||||
You can write a key to the cluster and retrieve the value back from any member or proxy.
|
||||
|
||||
### Next Steps
|
||||
|
||||
Now it's time to dig into the full etcd API and other guides.
|
||||
|
||||
- Explore the full [API][api].
|
||||
- Set up a [multi-machine cluster][clustering].
|
||||
- Learn the [config format, env variables and flags][configuration].
|
||||
- Find [language bindings and tools][libraries-and-tools].
|
||||
- Learn about the dashboard, lock and leader election [modules][modules].
|
||||
- Use TLS to [secure an etcd cluster][security].
|
||||
- [Tune etcd][tuning].
|
||||
- [Upgrade from old version][upgrade].
|
||||
- [Upgrade from 0.4.6 to 2.0.0][upgrade].
|
||||
|
||||
[api]: https://github.com/coreos/etcd/blob/master/Documentation/api.md
|
||||
[clustering]: https://github.com/coreos/etcd/blob/master/Documentation/clustering.md
|
||||
[configuration]: https://github.com/coreos/etcd/blob/master/Documentation/configuration.md
|
||||
[libraries-and-tools]: https://github.com/coreos/etcd/blob/master/Documentation/libraries-and-tools.md
|
||||
[modules]: https://github.com/coreos/etcd/blob/master/Documentation/modules.md
|
||||
[security]: https://github.com/coreos/etcd/blob/master/Documentation/security.md
|
||||
[tuning]: https://github.com/coreos/etcd/blob/master/Documentation/tuning.md
|
||||
[upgrade]: https://github.com/coreos/etcd/blob/master/Documentation/upgrade.md
|
||||
[api]: ./Documentation/api.md
|
||||
[clustering]: ./Documentation/clustering.md
|
||||
[configuration]: ./Documentation/configuration.md
|
||||
[libraries-and-tools]: ./Documentation/libraries-and-tools.md
|
||||
[security]: ./Documentation/security.md
|
||||
[tuning]: ./Documentation/tuning.md
|
||||
[upgrade]: ./Documentation/0_4_migration_tool.md
|
||||
|
||||
## Contact
|
||||
|
||||
@ -108,11 +113,7 @@ curl -L http://127.0.0.1:4001/version
|
||||
|
||||
#### API Versioning
|
||||
|
||||
The `v2` API responses should not change after the 0.2.0 release but new features will be added over time.
|
||||
|
||||
The `v1` API has been deprecated and will not be supported.
|
||||
|
||||
During the pre-v1.0.0 series of releases we may break the API as we fix bugs and get feedback.
|
||||
The `v2` API responses should not change after the 2.0.0 release but new features will be added over time.
|
||||
|
||||
#### 32-bit systems
|
||||
|
||||
|
7
build
7
build
@ -11,5 +11,8 @@ ln -s ${PWD} $GOPATH/src/${REPO_PATH}
|
||||
|
||||
eval $(go env)
|
||||
|
||||
go build -o bin/etcd ${REPO_PATH}
|
||||
go build -o bin/etcdctl ${REPO_PATH}/etcdctl
|
||||
# Static compilation is useful when etcd is run in a container
|
||||
CGO_ENABLED=0 go build -a -ldflags '-s' -o bin/etcd ${REPO_PATH}
|
||||
CGO_ENABLED=0 go build -a -ldflags '-s' -o bin/etcdctl ${REPO_PATH}/etcdctl
|
||||
go build -o bin/etcd-migrate ${REPO_PATH}/tools/etcd-migrate
|
||||
go build -o bin/etcd-dump-logs ${REPO_PATH}/tools/etcd-dump-logs
|
||||
|
@ -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 client
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
|
||||
)
|
||||
|
||||
func newHTTPClusterClient(tr *http.Transport, eps []string) (*httpClusterClient, error) {
|
||||
c := httpClusterClient{
|
||||
endpoints: make([]*httpClient, len(eps)),
|
||||
}
|
||||
|
||||
for i, ep := range eps {
|
||||
u, err := url.Parse(ep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.endpoints[i] = &httpClient{
|
||||
transport: tr,
|
||||
endpoint: *u,
|
||||
}
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
type httpClusterClient struct {
|
||||
endpoints []*httpClient
|
||||
}
|
||||
|
||||
func (c *httpClusterClient) do(ctx context.Context, act httpAction) (int, []byte, error) {
|
||||
//TODO(bcwaldon): introduce retry logic so all endpoints are attempted
|
||||
return c.endpoints[0].do(ctx, act)
|
||||
}
|
192
client/http.go
192
client/http.go
@ -1,49 +1,142 @@
|
||||
/*
|
||||
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 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
type httpActionDo interface {
|
||||
do(context.Context, httpAction) (int, []byte, error)
|
||||
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 {
|
||||
@ -52,13 +145,12 @@ type roundTripResponse struct {
|
||||
}
|
||||
|
||||
type httpClient struct {
|
||||
transport transport
|
||||
transport CancelableTransport
|
||||
endpoint url.URL
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (c *httpClient) do(ctx context.Context, act httpAction) (int, []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() {
|
||||
@ -89,9 +181,51 @@ func (c *httpClient) do(ctx context.Context, act httpAction) (int, []byte, error
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
return resp.StatusCode, body, err
|
||||
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
|
||||
}
|
||||
|
@ -1,18 +1,16 @@
|
||||
/*
|
||||
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 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package client
|
||||
|
||||
@ -26,9 +24,42 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/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 +96,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,14 +109,14 @@ func TestHTTPClientDoSuccess(t *testing.T) {
|
||||
Body: ioutil.NopCloser(strings.NewReader("foo")),
|
||||
}
|
||||
|
||||
code, 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)
|
||||
}
|
||||
|
||||
wantCode := http.StatusTeapot
|
||||
if wantCode != code {
|
||||
t.Fatalf("invalid response code: want=%d got=%d", wantCode, code)
|
||||
if wantCode != resp.StatusCode {
|
||||
t.Fatalf("invalid response code: want=%d got=%d", wantCode, resp.StatusCode)
|
||||
}
|
||||
|
||||
wantBody := []byte("foo")
|
||||
@ -100,7 +131,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 +144,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 +157,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 +180,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
116
client/keys.go
116
client/keys.go
@ -1,18 +1,16 @@
|
||||
/*
|
||||
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 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package client
|
||||
|
||||
@ -27,7 +25,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -41,44 +39,37 @@ var (
|
||||
ErrKeyExists = errors.New("client: key already exists")
|
||||
)
|
||||
|
||||
func NewKeysAPI(tr *http.Transport, eps []string, to time.Duration) (KeysAPI, error) {
|
||||
return newHTTPKeysAPIWithPrefix(tr, eps, to, DefaultV2KeysPrefix)
|
||||
func NewKeysAPI(c HTTPClient) KeysAPI {
|
||||
return &httpKeysAPI{
|
||||
client: c,
|
||||
prefix: DefaultV2KeysPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDiscoveryKeysAPI(tr *http.Transport, eps []string, to time.Duration) (KeysAPI, error) {
|
||||
return newHTTPKeysAPIWithPrefix(tr, eps, to, "")
|
||||
}
|
||||
|
||||
func newHTTPKeysAPIWithPrefix(tr *http.Transport, eps []string, to time.Duration, prefix string) (*httpKeysAPI, error) {
|
||||
c, err := newHTTPClusterClient(tr, eps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func NewDiscoveryKeysAPI(c HTTPClient) KeysAPI {
|
||||
return &httpKeysAPI{
|
||||
client: c,
|
||||
prefix: "",
|
||||
}
|
||||
|
||||
kAPI := httpKeysAPI{
|
||||
client: c,
|
||||
prefix: prefix,
|
||||
timeout: to,
|
||||
}
|
||||
|
||||
return &kAPI, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
Action string `json:"action"`
|
||||
Node *Node `json:"node"`
|
||||
PrevNode *Node `json:"prevNode"`
|
||||
Index uint64
|
||||
}
|
||||
|
||||
type Nodes []*Node
|
||||
@ -95,12 +86,11 @@ func (n *Node) String() string {
|
||||
}
|
||||
|
||||
type httpKeysAPI struct {
|
||||
client httpActionDo
|
||||
prefix string
|
||||
timeout time.Duration
|
||||
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{
|
||||
Prefix: k.prefix,
|
||||
Key: key,
|
||||
@ -111,31 +101,27 @@ func (k *httpKeysAPI) Create(key, val string, ttl time.Duration) (*Response, err
|
||||
create.TTL = &uttl
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), k.timeout)
|
||||
code, body, err := k.client.do(ctx, create)
|
||||
cancel()
|
||||
resp, body, err := k.client.Do(ctx, create)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return unmarshalHTTPResponse(code, body)
|
||||
return unmarshalHTTPResponse(resp.StatusCode, resp.Header, 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,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), k.timeout)
|
||||
code, body, err := k.client.do(ctx, get)
|
||||
cancel()
|
||||
resp, body, err := k.client.Do(ctx, get)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return unmarshalHTTPResponse(code, body)
|
||||
return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
|
||||
}
|
||||
|
||||
func (k *httpKeysAPI) Watch(key string, idx uint64) Watcher {
|
||||
@ -163,18 +149,17 @@ func (k *httpKeysAPI) RecursiveWatch(key string, idx uint64) Watcher {
|
||||
}
|
||||
|
||||
type httpWatcher struct {
|
||||
client httpActionDo
|
||||
client HTTPClient
|
||||
nextWait waitAction
|
||||
}
|
||||
|
||||
func (hw *httpWatcher) Next() (*Response, error) {
|
||||
//TODO(bcwaldon): This needs to be cancellable by the calling user
|
||||
code, 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
|
||||
}
|
||||
|
||||
resp, err := unmarshalHTTPResponse(code, body)
|
||||
resp, err := unmarshalHTTPResponse(httpresp.StatusCode, httpresp.Header, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -199,7 +184,7 @@ type getAction struct {
|
||||
Recursive bool
|
||||
}
|
||||
|
||||
func (g *getAction) httpRequest(ep url.URL) *http.Request {
|
||||
func (g *getAction) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2KeysURL(ep, g.Prefix, g.Key)
|
||||
|
||||
params := u.Query()
|
||||
@ -217,7 +202,7 @@ type waitAction struct {
|
||||
Recursive bool
|
||||
}
|
||||
|
||||
func (w *waitAction) httpRequest(ep url.URL) *http.Request {
|
||||
func (w *waitAction) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2KeysURL(ep, w.Prefix, w.Key)
|
||||
|
||||
params := u.Query()
|
||||
@ -237,7 +222,7 @@ type createAction struct {
|
||||
TTL *uint64
|
||||
}
|
||||
|
||||
func (c *createAction) httpRequest(ep url.URL) *http.Request {
|
||||
func (c *createAction) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2KeysURL(ep, c.Prefix, c.Key)
|
||||
|
||||
params := u.Query()
|
||||
@ -257,10 +242,10 @@ func (c *createAction) httpRequest(ep url.URL) *http.Request {
|
||||
return req
|
||||
}
|
||||
|
||||
func unmarshalHTTPResponse(code int, body []byte) (res *Response, err error) {
|
||||
func unmarshalHTTPResponse(code int, header http.Header, body []byte) (res *Response, err error) {
|
||||
switch code {
|
||||
case http.StatusOK, http.StatusCreated:
|
||||
res, err = unmarshalSuccessfulResponse(body)
|
||||
res, err = unmarshalSuccessfulResponse(header, body)
|
||||
default:
|
||||
err = unmarshalErrorResponse(code)
|
||||
}
|
||||
@ -268,13 +253,18 @@ func unmarshalHTTPResponse(code int, body []byte) (res *Response, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func unmarshalSuccessfulResponse(body []byte) (*Response, error) {
|
||||
func unmarshalSuccessfulResponse(header http.Header, body []byte) (*Response, error) {
|
||||
var res Response
|
||||
err := json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if header.Get("X-Etcd-Index") != "" {
|
||||
res.Index, err = strconv.ParseUint(header.Get("X-Etcd-Index"), 10, 64)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
@ -287,6 +277,8 @@ func unmarshalErrorResponse(code int) error {
|
||||
case http.StatusInternalServerError:
|
||||
// this isn't necessarily true
|
||||
return ErrNoLeader
|
||||
case http.StatusGatewayTimeout:
|
||||
return ErrTimeout
|
||||
default:
|
||||
}
|
||||
|
||||
|
@ -1,18 +1,16 @@
|
||||
/*
|
||||
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 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package client
|
||||
|
||||
@ -117,7 +115,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
|
||||
@ -166,7 +164,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
|
||||
@ -213,7 +211,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 {
|
||||
@ -255,40 +253,46 @@ func assertResponse(got http.Request, wantURL *url.URL, wantHeader http.Header,
|
||||
|
||||
func TestUnmarshalSuccessfulResponse(t *testing.T) {
|
||||
tests := []struct {
|
||||
indexHeader string
|
||||
body string
|
||||
res *Response
|
||||
expectError bool
|
||||
}{
|
||||
// Neither PrevNode or Node
|
||||
{
|
||||
"1",
|
||||
`{"action":"delete"}`,
|
||||
&Response{Action: "delete"},
|
||||
&Response{Action: "delete", Index: 1},
|
||||
false,
|
||||
},
|
||||
|
||||
// PrevNode
|
||||
{
|
||||
"15",
|
||||
`{"action":"delete", "prevNode": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`,
|
||||
&Response{Action: "delete", PrevNode: &Node{Key: "/foo", Value: "bar", ModifiedIndex: 12, CreatedIndex: 10}},
|
||||
&Response{Action: "delete", Index: 15, PrevNode: &Node{Key: "/foo", Value: "bar", ModifiedIndex: 12, CreatedIndex: 10}},
|
||||
false,
|
||||
},
|
||||
|
||||
// Node
|
||||
{
|
||||
"15",
|
||||
`{"action":"get", "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`,
|
||||
&Response{Action: "get", Node: &Node{Key: "/foo", Value: "bar", ModifiedIndex: 12, CreatedIndex: 10}},
|
||||
&Response{Action: "get", Index: 15, Node: &Node{Key: "/foo", Value: "bar", ModifiedIndex: 12, CreatedIndex: 10}},
|
||||
false,
|
||||
},
|
||||
|
||||
// PrevNode and Node
|
||||
{
|
||||
"15",
|
||||
`{"action":"update", "prevNode": {"key": "/foo", "value": "baz", "modifiedIndex": 10, "createdIndex": 10}, "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`,
|
||||
&Response{Action: "update", PrevNode: &Node{Key: "/foo", Value: "baz", ModifiedIndex: 10, CreatedIndex: 10}, Node: &Node{Key: "/foo", Value: "bar", ModifiedIndex: 12, CreatedIndex: 10}},
|
||||
&Response{Action: "update", Index: 15, PrevNode: &Node{Key: "/foo", Value: "baz", ModifiedIndex: 10, CreatedIndex: 10}, Node: &Node{Key: "/foo", Value: "bar", ModifiedIndex: 12, CreatedIndex: 10}},
|
||||
false,
|
||||
},
|
||||
|
||||
// Garbage in body
|
||||
{
|
||||
"",
|
||||
`garbage`,
|
||||
nil,
|
||||
true,
|
||||
@ -296,7 +300,9 @@ func TestUnmarshalSuccessfulResponse(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
res, err := unmarshalSuccessfulResponse([]byte(tt.body))
|
||||
h := make(http.Header)
|
||||
h.Add("X-Etcd-Index", tt.indexHeader)
|
||||
res, err := unmarshalSuccessfulResponse(h, []byte(tt.body))
|
||||
if tt.expectError != (err != nil) {
|
||||
t.Errorf("#%d: expectError=%t, err=%v", i, tt.expectError, err)
|
||||
}
|
||||
@ -305,14 +311,16 @@ func TestUnmarshalSuccessfulResponse(t *testing.T) {
|
||||
t.Errorf("#%d: received res==%v, but expected res==%v", i, res, tt.res)
|
||||
continue
|
||||
} else if tt.res == nil {
|
||||
// expected and succesfully got nil response
|
||||
// expected and successfully got nil response
|
||||
continue
|
||||
}
|
||||
|
||||
if res.Action != tt.res.Action {
|
||||
t.Errorf("#%d: Action=%s, expected %s", i, res.Action, tt.res.Action)
|
||||
}
|
||||
|
||||
if res.Index != tt.res.Index {
|
||||
t.Errorf("#%d: Index=%d, expected %d", i, res.Index, tt.res.Index)
|
||||
}
|
||||
if !reflect.DeepEqual(res.Node, tt.res.Node) {
|
||||
t.Errorf("#%d: Node=%v, expected %v", i, res.Node, tt.res.Node)
|
||||
}
|
||||
@ -350,7 +358,7 @@ func TestUnmarshalErrorResponse(t *testing.T) {
|
||||
{http.StatusNotImplemented, unrecognized},
|
||||
{http.StatusBadGateway, unrecognized},
|
||||
{http.StatusServiceUnavailable, unrecognized},
|
||||
{http.StatusGatewayTimeout, unrecognized},
|
||||
{http.StatusGatewayTimeout, ErrTimeout},
|
||||
{http.StatusHTTPVersionNotSupported, unrecognized},
|
||||
}
|
||||
|
||||
|
@ -1,18 +1,16 @@
|
||||
/*
|
||||
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 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package client
|
||||
|
||||
@ -23,9 +21,8 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
|
||||
"github.com/coreos/etcd/etcdserver/etcdhttp/httptypes"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
)
|
||||
@ -34,41 +31,30 @@ var (
|
||||
DefaultV2MembersPrefix = "/v2/members"
|
||||
)
|
||||
|
||||
func NewMembersAPI(tr *http.Transport, eps []string, to time.Duration) (MembersAPI, error) {
|
||||
c, err := newHTTPClusterClient(tr, eps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func NewMembersAPI(c HTTPClient) MembersAPI {
|
||||
return &httpMembersAPI{
|
||||
client: c,
|
||||
}
|
||||
|
||||
mAPI := httpMembersAPI{
|
||||
client: c,
|
||||
timeout: to,
|
||||
}
|
||||
|
||||
return &mAPI, nil
|
||||
}
|
||||
|
||||
type MembersAPI interface {
|
||||
List() ([]httptypes.Member, error)
|
||||
Add(peerURL string) (*httptypes.Member, error)
|
||||
Remove(mID string) error
|
||||
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 httpActionDo
|
||||
timeout time.Duration
|
||||
client HTTPClient
|
||||
}
|
||||
|
||||
func (m *httpMembersAPI) List() ([]httptypes.Member, error) {
|
||||
func (m *httpMembersAPI) List(ctx context.Context) ([]httptypes.Member, error) {
|
||||
req := &membersAPIActionList{}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
|
||||
code, body, err := m.client.do(ctx, req)
|
||||
cancel()
|
||||
resp, body, err := m.client.Do(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := assertStatusCode(http.StatusOK, code); err != nil {
|
||||
if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -80,24 +66,30 @@ func (m *httpMembersAPI) List() ([]httptypes.Member, error) {
|
||||
return []httptypes.Member(mCollection), nil
|
||||
}
|
||||
|
||||
func (m *httpMembersAPI) Add(peerURL string) (*httptypes.Member, error) {
|
||||
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}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
|
||||
code, body, err := m.client.do(ctx, req)
|
||||
cancel()
|
||||
resp, body, err := m.client.Do(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := assertStatusCode(http.StatusCreated, code); err != nil {
|
||||
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
|
||||
@ -106,21 +98,19 @@ func (m *httpMembersAPI) Add(peerURL string) (*httptypes.Member, error) {
|
||||
return &memb, nil
|
||||
}
|
||||
|
||||
func (m *httpMembersAPI) Remove(memberID string) error {
|
||||
func (m *httpMembersAPI) Remove(ctx context.Context, memberID string) error {
|
||||
req := &membersAPIActionRemove{memberID: memberID}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
|
||||
code, _, err := m.client.do(ctx, req)
|
||||
cancel()
|
||||
resp, _, err := m.client.Do(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return assertStatusCode(http.StatusNoContent, code)
|
||||
return assertStatusCode(resp.StatusCode, http.StatusNoContent)
|
||||
}
|
||||
|
||||
type membersAPIActionList struct{}
|
||||
|
||||
func (l *membersAPIActionList) httpRequest(ep url.URL) *http.Request {
|
||||
func (l *membersAPIActionList) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2MembersURL(ep)
|
||||
req, _ := http.NewRequest("GET", u.String(), nil)
|
||||
return req
|
||||
@ -130,7 +120,7 @@ type membersAPIActionRemove struct {
|
||||
memberID string
|
||||
}
|
||||
|
||||
func (d *membersAPIActionRemove) httpRequest(ep url.URL) *http.Request {
|
||||
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)
|
||||
@ -141,7 +131,7 @@ type membersAPIActionAdd struct {
|
||||
peerURLs types.URLs
|
||||
}
|
||||
|
||||
func (a *membersAPIActionAdd) httpRequest(ep url.URL) *http.Request {
|
||||
func (a *membersAPIActionAdd) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2MembersURL(ep)
|
||||
m := httptypes.MemberCreateRequest{PeerURLs: a.peerURLs}
|
||||
b, _ := json.Marshal(&m)
|
||||
@ -150,11 +140,13 @@ func (a *membersAPIActionAdd) httpRequest(ep url.URL) *http.Request {
|
||||
return req
|
||||
}
|
||||
|
||||
func assertStatusCode(want, got int) (err error) {
|
||||
if want != got {
|
||||
err = fmt.Errorf("unexpected status code %d", got)
|
||||
func assertStatusCode(got int, want ...int) (err error) {
|
||||
for _, w := range want {
|
||||
if w == got {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
return fmt.Errorf("unexpected status code %d", got)
|
||||
}
|
||||
|
||||
// v2MembersURL add the necessary path to the provided endpoint
|
||||
|
@ -1,18 +1,16 @@
|
||||
/*
|
||||
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 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package client
|
||||
|
||||
@ -35,7 +33,7 @@ func TestMembersAPIActionList(t *testing.T) {
|
||||
Path: "/v2/members",
|
||||
}
|
||||
|
||||
got := *act.httpRequest(ep)
|
||||
got := *act.HTTPRequest(ep)
|
||||
err := assertResponse(got, wantURL, http.Header{}, nil)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
@ -61,7 +59,7 @@ func TestMembersAPIActionAdd(t *testing.T) {
|
||||
}
|
||||
wantBody := []byte(`{"peerURLs":["https://127.0.0.1:8081","http://127.0.0.1:8080"]}`)
|
||||
|
||||
got := *act.httpRequest(ep)
|
||||
got := *act.HTTPRequest(ep)
|
||||
err := assertResponse(got, wantURL, wantHeader, wantBody)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
@ -78,7 +76,7 @@ func TestMembersAPIActionRemove(t *testing.T) {
|
||||
Path: "/v2/members/XXX",
|
||||
}
|
||||
|
||||
got := *act.httpRequest(ep)
|
||||
got := *act.HTTPRequest(ep)
|
||||
err := assertResponse(got, wantURL, http.Header{}, nil)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
@ -86,12 +84,12 @@ func TestMembersAPIActionRemove(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAssertStatusCode(t *testing.T) {
|
||||
if err := assertStatusCode(400, 404); err == nil {
|
||||
if err := assertStatusCode(404, 400); err == nil {
|
||||
t.Errorf("assertStatusCode failed to detect conflict in 400 vs 404")
|
||||
}
|
||||
|
||||
if err := assertStatusCode(400, 400); err != nil {
|
||||
t.Errorf("assertStatusCode found conflict in 400 vs 400: %v", err)
|
||||
if err := assertStatusCode(404, 400, 404); err != nil {
|
||||
t.Errorf("assertStatusCode found conflict in (404,400) vs 400: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,18 +1,16 @@
|
||||
/*
|
||||
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 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package discovery
|
||||
|
||||
@ -20,9 +18,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
@ -30,6 +28,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/jonboulle/clockwork"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
)
|
||||
@ -44,21 +43,34 @@ var (
|
||||
ErrTooManyRetries = errors.New("discovery: too many retries")
|
||||
)
|
||||
|
||||
const (
|
||||
// Environment variable used to configure an HTTP proxy for discovery
|
||||
DiscoveryProxyEnv = "ETCD_DISCOVERY_PROXY"
|
||||
var (
|
||||
// Number of retries discovery will attempt before giving up and erroring out.
|
||||
nRetries = uint(3)
|
||||
nRetries = uint(math.MaxUint32)
|
||||
)
|
||||
|
||||
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 types.ID
|
||||
config string
|
||||
c client.KeysAPI
|
||||
retries uint
|
||||
url *url.URL
|
||||
@ -66,11 +78,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
|
||||
}
|
||||
@ -95,52 +106,51 @@ func proxyFuncFromEnv() (func(*http.Request) (*url.URL, error), error) {
|
||||
return http.ProxyURL(proxyURL), nil
|
||||
}
|
||||
|
||||
func New(durl string, id types.ID, 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}, []string{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.
|
||||
if _, _, err := d.checkCluster(); err != nil {
|
||||
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).
|
||||
return "", err
|
||||
}
|
||||
|
||||
nodes, size, err := d.checkCluster()
|
||||
nodes, size, index, err := d.checkCluster()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
all, err := d.waitNodes(nodes, size)
|
||||
all, err := d.waitNodes(nodes, size, index)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -148,42 +158,67 @@ 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, index, err := d.checkCluster()
|
||||
if err != nil {
|
||||
if err == ErrFullCluster {
|
||||
return nodesToCluster(nodes), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
all, err := d.waitNodes(nodes, size, index)
|
||||
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 {
|
||||
if err == client.ErrKeyExists {
|
||||
return ErrDuplicateID
|
||||
}
|
||||
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) {
|
||||
func (d *discovery) checkCluster() (client.Nodes, int, uint64, 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
|
||||
return nil, 0, 0, ErrSizeNotFound
|
||||
}
|
||||
if err == client.ErrTimeout {
|
||||
return d.checkClusterRetry()
|
||||
}
|
||||
return nil, 0, err
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
size, err := strconv.Atoi(resp.Node.Value)
|
||||
if err != nil {
|
||||
return nil, 0, ErrBadSizeKey
|
||||
return nil, 0, 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()
|
||||
}
|
||||
return nil, 0, err
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
nodes := make(client.Nodes, 0)
|
||||
// append non-config keys to nodes
|
||||
@ -202,10 +237,10 @@ func (d *discovery) checkCluster() (client.Nodes, int, error) {
|
||||
break
|
||||
}
|
||||
if i >= size-1 {
|
||||
return nil, size, ErrFullCluster
|
||||
return nodes[:size], size, resp.Index, ErrFullCluster
|
||||
}
|
||||
}
|
||||
return nodes, size, nil
|
||||
return nodes, size, resp.Index, nil
|
||||
}
|
||||
|
||||
func (d *discovery) logAndBackoffForRetry(step string) {
|
||||
@ -215,31 +250,32 @@ func (d *discovery) logAndBackoffForRetry(step string) {
|
||||
d.clock.Sleep(retryTime)
|
||||
}
|
||||
|
||||
func (d *discovery) checkClusterRetry() (client.Nodes, int, error) {
|
||||
func (d *discovery) checkClusterRetry() (client.Nodes, int, uint64, error) {
|
||||
if d.retries < nRetries {
|
||||
d.logAndBackoffForRetry("cluster status check")
|
||||
return d.checkCluster()
|
||||
}
|
||||
return nil, 0, ErrTooManyRetries
|
||||
return nil, 0, 0, ErrTooManyRetries
|
||||
}
|
||||
|
||||
func (d *discovery) waitNodesRetry() (client.Nodes, error) {
|
||||
if d.retries < nRetries {
|
||||
d.logAndBackoffForRetry("waiting for other nodes")
|
||||
nodes, n, err := d.checkCluster()
|
||||
nodes, n, index, err := d.checkCluster()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.waitNodes(nodes, n)
|
||||
return d.waitNodes(nodes, n, index)
|
||||
}
|
||||
return nil, ErrTooManyRetries
|
||||
}
|
||||
|
||||
func (d *discovery) waitNodes(nodes client.Nodes, size int) (client.Nodes, error) {
|
||||
func (d *discovery) waitNodes(nodes client.Nodes, size int, index uint64) (client.Nodes, error) {
|
||||
if len(nodes) > size {
|
||||
nodes = nodes[:size]
|
||||
}
|
||||
w := d.c.RecursiveWatch(d.cluster, nodes[len(nodes)-1].ModifiedIndex+1)
|
||||
// watch from the next index
|
||||
w := d.c.RecursiveWatch(d.cluster, index+1)
|
||||
all := make(client.Nodes, len(nodes))
|
||||
copy(all, nodes)
|
||||
for _, n := range all {
|
||||
@ -253,7 +289,7 @@ func (d *discovery) waitNodes(nodes client.Nodes, size int) (client.Nodes, error
|
||||
// wait for others
|
||||
for len(all) < size {
|
||||
log.Printf("discovery: found %d peer(s), waiting for %d more", len(all), size-len(all))
|
||||
resp, err := w.Next()
|
||||
resp, err := w.Next(context.Background())
|
||||
if err != nil {
|
||||
if err == client.ErrTimeout {
|
||||
return d.waitNodesRetry()
|
||||
|
@ -1,40 +1,41 @@
|
||||
/*
|
||||
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 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/jonboulle/clockwork"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
|
||||
"github.com/coreos/etcd/client"
|
||||
)
|
||||
|
||||
func TestProxyFuncFromEnvUnset(t *testing.T) {
|
||||
os.Setenv(DiscoveryProxyEnv, "")
|
||||
pf, err := proxyFuncFromEnv()
|
||||
const (
|
||||
maxRetryInTest = 3
|
||||
)
|
||||
|
||||
func TestNewProxyFuncUnset(t *testing.T) {
|
||||
pf, err := newProxyFunc("")
|
||||
if pf != nil {
|
||||
t.Fatal("unexpected non-nil proxyFunc")
|
||||
}
|
||||
@ -43,14 +44,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 +60,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
|
||||
@ -93,6 +92,7 @@ func TestCheckCluster(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
nodes []*client.Node
|
||||
index uint64
|
||||
werr error
|
||||
wsize int
|
||||
}{
|
||||
@ -106,6 +106,7 @@ func TestCheckCluster(t *testing.T) {
|
||||
{Key: "/1000/3", CreatedIndex: 4},
|
||||
{Key: "/1000/4", CreatedIndex: 5},
|
||||
},
|
||||
5,
|
||||
nil,
|
||||
3,
|
||||
},
|
||||
@ -119,6 +120,7 @@ func TestCheckCluster(t *testing.T) {
|
||||
{Key: self, CreatedIndex: 4},
|
||||
{Key: "/1000/4", CreatedIndex: 5},
|
||||
},
|
||||
5,
|
||||
nil,
|
||||
3,
|
||||
},
|
||||
@ -132,6 +134,7 @@ func TestCheckCluster(t *testing.T) {
|
||||
{Key: "/1000/4", CreatedIndex: 4},
|
||||
{Key: self, CreatedIndex: 5},
|
||||
},
|
||||
5,
|
||||
ErrFullCluster,
|
||||
3,
|
||||
},
|
||||
@ -143,6 +146,7 @@ func TestCheckCluster(t *testing.T) {
|
||||
{Key: "/1000/2", CreatedIndex: 2},
|
||||
{Key: "/1000/3", CreatedIndex: 3},
|
||||
},
|
||||
3,
|
||||
nil,
|
||||
3,
|
||||
},
|
||||
@ -154,6 +158,7 @@ func TestCheckCluster(t *testing.T) {
|
||||
{Key: "/1000/3", CreatedIndex: 3},
|
||||
{Key: "/1000/4", CreatedIndex: 4},
|
||||
},
|
||||
3,
|
||||
ErrFullCluster,
|
||||
3,
|
||||
},
|
||||
@ -162,12 +167,14 @@ func TestCheckCluster(t *testing.T) {
|
||||
[]*client.Node{
|
||||
{Key: "/1000/_config/size", Value: "bad", CreatedIndex: 1},
|
||||
},
|
||||
0,
|
||||
ErrBadSizeKey,
|
||||
0,
|
||||
},
|
||||
{
|
||||
// no size key
|
||||
[]*client.Node{},
|
||||
0,
|
||||
ErrSizeNotFound,
|
||||
0,
|
||||
},
|
||||
@ -176,12 +183,13 @@ func TestCheckCluster(t *testing.T) {
|
||||
for i, tt := range tests {
|
||||
rs := make([]*client.Response, 0)
|
||||
if len(tt.nodes) > 0 {
|
||||
rs = append(rs, &client.Response{Node: tt.nodes[0]})
|
||||
rs = append(rs, &client.Response{Node: tt.nodes[0], Index: tt.index})
|
||||
rs = append(rs, &client.Response{
|
||||
Node: &client.Node{
|
||||
Key: cluster,
|
||||
Nodes: tt.nodes[1:],
|
||||
},
|
||||
Index: tt.index,
|
||||
})
|
||||
}
|
||||
c := &clientWithResp{rs: rs}
|
||||
@ -194,12 +202,12 @@ func TestCheckCluster(t *testing.T) {
|
||||
|
||||
for _, d := range []discovery{d, dRetry} {
|
||||
go func() {
|
||||
for i := uint(1); i <= nRetries; i++ {
|
||||
for i := uint(1); i <= maxRetryInTest; i++ {
|
||||
fc.BlockUntil(1)
|
||||
fc.Advance(time.Second * (0x1 << i))
|
||||
}
|
||||
}()
|
||||
ns, size, err := d.checkCluster()
|
||||
ns, size, index, err := d.checkCluster()
|
||||
if err != tt.werr {
|
||||
t.Errorf("#%d: err = %v, want %v", i, err, tt.werr)
|
||||
}
|
||||
@ -209,6 +217,9 @@ func TestCheckCluster(t *testing.T) {
|
||||
if size != tt.wsize {
|
||||
t.Errorf("#%d: size = %v, want %d", i, size, tt.wsize)
|
||||
}
|
||||
if index != tt.index {
|
||||
t.Errorf("#%d: index = %v, want %d", i, index, tt.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -282,12 +293,12 @@ func TestWaitNodes(t *testing.T) {
|
||||
|
||||
for _, d := range []*discovery{d, dRetry} {
|
||||
go func() {
|
||||
for i := uint(1); i <= nRetries; i++ {
|
||||
for i := uint(1); i <= maxRetryInTest; i++ {
|
||||
fc.BlockUntil(1)
|
||||
fc.Advance(time.Second * (0x1 << i))
|
||||
}
|
||||
}()
|
||||
g, err := d.waitNodes(tt.nodes, 3)
|
||||
g, err := d.waitNodes(tt.nodes, 3, 0) // we do not care about index in this test
|
||||
if err != nil {
|
||||
t.Errorf("#%d: err = %v, want %v", i, err, nil)
|
||||
}
|
||||
@ -322,7 +333,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)
|
||||
}
|
||||
}
|
||||
@ -372,6 +383,9 @@ func TestSortableNodes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRetryFailure(t *testing.T) {
|
||||
nRetries = maxRetryInTest
|
||||
defer func() { nRetries = math.MaxUint32 }()
|
||||
|
||||
cluster := "1000"
|
||||
c := &clientWithRetry{failTimes: 4}
|
||||
fc := clockwork.NewFakeClock()
|
||||
@ -382,12 +396,12 @@ func TestRetryFailure(t *testing.T) {
|
||||
clock: fc,
|
||||
}
|
||||
go func() {
|
||||
for i := uint(1); i <= nRetries; i++ {
|
||||
for i := uint(1); i <= maxRetryInTest; i++ {
|
||||
fc.BlockUntil(1)
|
||||
fc.Advance(time.Second * (0x1 << i))
|
||||
}
|
||||
}()
|
||||
if _, _, err := d.checkCluster(); err != ErrTooManyRetries {
|
||||
if _, _, _, err := d.checkCluster(); err != ErrTooManyRetries {
|
||||
t.Errorf("err = %v, want %v", err, ErrTooManyRetries)
|
||||
}
|
||||
}
|
||||
@ -397,7 +411,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
|
||||
}
|
||||
@ -406,7 +420,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
|
||||
}
|
||||
@ -428,11 +442,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
|
||||
}
|
||||
|
||||
@ -448,7 +462,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
|
||||
}
|
||||
@ -461,7 +475,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
|
||||
}
|
||||
|
||||
@ -472,20 +486,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
|
||||
@ -495,7 +509,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
|
||||
|
@ -1,18 +1,16 @@
|
||||
/*
|
||||
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 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/*
|
||||
Package discovery provides an implementation of the cluster discovery that
|
||||
|
93
discovery/srv.go
Normal file
93
discovery/srv.go
Normal file
@ -0,0 +1,93 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
)
|
||||
|
||||
var (
|
||||
// indirection for testing
|
||||
lookupSRV = net.LookupSRV
|
||||
)
|
||||
|
||||
// TODO(barakmich): Currently ignores priority and weight (as they don't make as much sense for a bootstrap)
|
||||
// Also doesn't do any lookups for the token (though it could)
|
||||
// Also sees each entry as a separate instance.
|
||||
func SRVGetCluster(name, dns string, defaultToken string, apurls types.URLs) (string, string, error) {
|
||||
stringParts := make([]string, 0)
|
||||
tempName := int(0)
|
||||
tcpAPUrls := make([]string, 0)
|
||||
|
||||
// First, resolve the apurls
|
||||
for _, url := range apurls {
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", url.Host)
|
||||
if err != nil {
|
||||
log.Printf("discovery: Couldn't resolve host %s during SRV discovery", url.Host)
|
||||
return "", "", err
|
||||
}
|
||||
tcpAPUrls = append(tcpAPUrls, tcpAddr.String())
|
||||
}
|
||||
|
||||
updateNodeMap := func(service, prefix string) error {
|
||||
_, addrs, err := lookupSRV(service, "tcp", dns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, srv := range addrs {
|
||||
host := net.JoinHostPort(srv.Target, fmt.Sprintf("%d", srv.Port))
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", host)
|
||||
if err != nil {
|
||||
log.Printf("discovery: Couldn't resolve host %s during SRV discovery", host)
|
||||
continue
|
||||
}
|
||||
n := ""
|
||||
for _, url := range tcpAPUrls {
|
||||
if url == tcpAddr.String() {
|
||||
n = name
|
||||
}
|
||||
}
|
||||
if n == "" {
|
||||
n = fmt.Sprintf("%d", tempName)
|
||||
tempName += 1
|
||||
}
|
||||
stringParts = append(stringParts, fmt.Sprintf("%s=%s%s", n, prefix, tcpAddr.String()))
|
||||
log.Printf("discovery: Got bootstrap from DNS for %s at host %s to %s%s", service, host, prefix, tcpAddr.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
failCount := 0
|
||||
err := updateNodeMap("etcd-server-ssl", "https://")
|
||||
if err != nil {
|
||||
log.Printf("discovery: Error querying DNS SRV records for _etcd-server-ssl %s", err)
|
||||
failCount += 1
|
||||
}
|
||||
err = updateNodeMap("etcd-server", "http://")
|
||||
if err != nil {
|
||||
log.Printf("discovery: Error querying DNS SRV records for _etcd-server %s", err)
|
||||
failCount += 1
|
||||
}
|
||||
if failCount == 2 {
|
||||
log.Printf("discovery: SRV discovery failed: too many errors querying DNS SRV records")
|
||||
return "", "", err
|
||||
}
|
||||
return strings.Join(stringParts, ","), defaultToken, nil
|
||||
}
|
99
discovery/srv_test.go
Normal file
99
discovery/srv_test.go
Normal file
@ -0,0 +1,99 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
)
|
||||
|
||||
func TestSRVGetCluster(t *testing.T) {
|
||||
defer func() { lookupSRV = net.LookupSRV }()
|
||||
|
||||
name := "dnsClusterTest"
|
||||
tests := []struct {
|
||||
withSSL []*net.SRV
|
||||
withoutSSL []*net.SRV
|
||||
urls []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
[]*net.SRV{},
|
||||
[]*net.SRV{},
|
||||
nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
[]*net.SRV{
|
||||
&net.SRV{Target: "10.0.0.1", Port: 2480},
|
||||
&net.SRV{Target: "10.0.0.2", Port: 2480},
|
||||
&net.SRV{Target: "10.0.0.3", Port: 2480},
|
||||
},
|
||||
[]*net.SRV{},
|
||||
nil,
|
||||
"0=https://10.0.0.1:2480,1=https://10.0.0.2:2480,2=https://10.0.0.3:2480",
|
||||
},
|
||||
{
|
||||
[]*net.SRV{
|
||||
&net.SRV{Target: "10.0.0.1", Port: 2480},
|
||||
&net.SRV{Target: "10.0.0.2", Port: 2480},
|
||||
&net.SRV{Target: "10.0.0.3", Port: 2480},
|
||||
},
|
||||
[]*net.SRV{
|
||||
&net.SRV{Target: "10.0.0.1", Port: 7001},
|
||||
},
|
||||
nil,
|
||||
"0=https://10.0.0.1:2480,1=https://10.0.0.2:2480,2=https://10.0.0.3:2480,3=http://10.0.0.1:7001",
|
||||
},
|
||||
{
|
||||
[]*net.SRV{
|
||||
&net.SRV{Target: "10.0.0.1", Port: 2480},
|
||||
&net.SRV{Target: "10.0.0.2", Port: 2480},
|
||||
&net.SRV{Target: "10.0.0.3", Port: 2480},
|
||||
},
|
||||
[]*net.SRV{
|
||||
&net.SRV{Target: "10.0.0.1", Port: 7001},
|
||||
},
|
||||
[]string{"https://10.0.0.1:2480"},
|
||||
"dnsClusterTest=https://10.0.0.1:2480,0=https://10.0.0.2:2480,1=https://10.0.0.3:2480,2=http://10.0.0.1:7001",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
lookupSRV = func(service string, proto string, domain string) (string, []*net.SRV, error) {
|
||||
if service == "etcd-server-ssl" {
|
||||
return "", tt.withSSL, nil
|
||||
}
|
||||
if service == "etcd-server" {
|
||||
return "", tt.withoutSSL, nil
|
||||
}
|
||||
return "", nil, errors.New("Unkown service in mock")
|
||||
}
|
||||
urls := testutil.MustNewURLs(t, tt.urls)
|
||||
str, token, err := SRVGetCluster(name, "example.com", "token", urls)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: err: %#v", i, err)
|
||||
}
|
||||
if token != "token" {
|
||||
t.Errorf("%d: token: %s", i, token)
|
||||
}
|
||||
if str != tt.expected {
|
||||
t.Errorf("#%d: cluster = %s, want %s", i, str, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
122
error/error.go
122
error/error.go
@ -1,19 +1,20 @@
|
||||
/*
|
||||
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 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// error package describes errors in etcd project.
|
||||
// When any change happens, Documentation/errorcode.md needs to be updated
|
||||
// correspondingly.
|
||||
package error
|
||||
|
||||
import (
|
||||
@ -27,24 +28,24 @@ var errors = map[int]string{
|
||||
EcodeKeyNotFound: "Key not found",
|
||||
EcodeTestFailed: "Compare failed", //test and set
|
||||
EcodeNotFile: "Not a file",
|
||||
EcodeNoMorePeer: "Reached the max number of peers in the cluster",
|
||||
ecodeNoMorePeer: "Reached the max number of peers in the cluster",
|
||||
EcodeNotDir: "Not a directory",
|
||||
EcodeNodeExist: "Key already exists", // create
|
||||
ecodeKeyIsPreserved: "The prefix of given key is a keyword in etcd",
|
||||
EcodeRootROnly: "Root is read only",
|
||||
EcodeKeyIsPreserved: "The prefix of given key is a keyword in etcd",
|
||||
EcodeDirNotEmpty: "Directory not empty",
|
||||
EcodeExistingPeerAddr: "Peer address has existed",
|
||||
ecodeExistingPeerAddr: "Peer address has existed",
|
||||
|
||||
// Post form related errors
|
||||
EcodeValueRequired: "Value is Required in POST form",
|
||||
ecodeValueRequired: "Value is Required in POST form",
|
||||
EcodePrevValueRequired: "PrevValue is Required in POST form",
|
||||
EcodeTTLNaN: "The given TTL in POST form is not a number",
|
||||
EcodeIndexNaN: "The given index in POST form is not a number",
|
||||
EcodeValueOrTTLRequired: "Value or TTL is required in POST form",
|
||||
EcodeTimeoutNaN: "The given timeout in POST form is not a number",
|
||||
EcodeNameRequired: "Name is required in POST form",
|
||||
EcodeIndexOrValueRequired: "Index or value is required",
|
||||
EcodeIndexValueMutex: "Index and value cannot both be specified",
|
||||
ecodeValueOrTTLRequired: "Value or TTL is required in POST form",
|
||||
ecodeTimeoutNaN: "The given timeout in POST form is not a number",
|
||||
ecodeNameRequired: "Name is required in POST form",
|
||||
ecodeIndexOrValueRequired: "Index or value is required",
|
||||
ecodeIndexValueMutex: "Index and value cannot both be specified",
|
||||
EcodeInvalidField: "Invalid field",
|
||||
EcodeInvalidForm: "Invalid POST form",
|
||||
|
||||
@ -55,35 +56,45 @@ var errors = map[int]string{
|
||||
// etcd related errors
|
||||
EcodeWatcherCleared: "watcher is cleared due to etcd recovery",
|
||||
EcodeEventIndexCleared: "The event in requested index is outdated and cleared",
|
||||
EcodeStandbyInternal: "Standby Internal Error",
|
||||
EcodeInvalidActiveSize: "Invalid active size",
|
||||
EcodeInvalidRemoveDelay: "Standby remove delay",
|
||||
ecodeStandbyInternal: "Standby Internal Error",
|
||||
ecodeInvalidActiveSize: "Invalid active size",
|
||||
ecodeInvalidRemoveDelay: "Standby remove delay",
|
||||
|
||||
// client related errors
|
||||
EcodeClientInternal: "Client Internal Error",
|
||||
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
|
||||
EcodeNotFile = 102
|
||||
EcodeNoMorePeer = 103
|
||||
ecodeNoMorePeer = 103
|
||||
EcodeNotDir = 104
|
||||
EcodeNodeExist = 105
|
||||
EcodeKeyIsPreserved = 106
|
||||
ecodeKeyIsPreserved = 106
|
||||
EcodeRootROnly = 107
|
||||
EcodeDirNotEmpty = 108
|
||||
EcodeExistingPeerAddr = 109
|
||||
ecodeExistingPeerAddr = 109
|
||||
|
||||
EcodeValueRequired = 200
|
||||
ecodeValueRequired = 200
|
||||
EcodePrevValueRequired = 201
|
||||
EcodeTTLNaN = 202
|
||||
EcodeIndexNaN = 203
|
||||
EcodeValueOrTTLRequired = 204
|
||||
EcodeTimeoutNaN = 205
|
||||
EcodeNameRequired = 206
|
||||
EcodeIndexOrValueRequired = 207
|
||||
EcodeIndexValueMutex = 208
|
||||
ecodeValueOrTTLRequired = 204
|
||||
ecodeTimeoutNaN = 205
|
||||
ecodeNameRequired = 206
|
||||
ecodeIndexOrValueRequired = 207
|
||||
ecodeIndexValueMutex = 208
|
||||
EcodeInvalidField = 209
|
||||
EcodeInvalidForm = 210
|
||||
|
||||
@ -92,11 +103,11 @@ const (
|
||||
|
||||
EcodeWatcherCleared = 400
|
||||
EcodeEventIndexCleared = 401
|
||||
EcodeStandbyInternal = 402
|
||||
EcodeInvalidActiveSize = 403
|
||||
EcodeInvalidRemoveDelay = 404
|
||||
ecodeStandbyInternal = 402
|
||||
ecodeInvalidActiveSize = 403
|
||||
ecodeInvalidRemoveDelay = 404
|
||||
|
||||
EcodeClientInternal = 500
|
||||
ecodeClientInternal = 500
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
@ -119,10 +130,6 @@ func NewError(errorCode int, cause string, index uint64) *Error {
|
||||
}
|
||||
}
|
||||
|
||||
func Message(code int) string {
|
||||
return errors[code]
|
||||
}
|
||||
|
||||
// Only for error interface
|
||||
func (e Error) Error() string {
|
||||
return e.Message + " (" + e.Cause + ")"
|
||||
@ -133,22 +140,17 @@ func (e Error) toJsonString() string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (e Error) statusCode() int {
|
||||
status, ok := errorStatus[e.ErrorCode]
|
||||
if !ok {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func (e Error) WriteTo(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
|
||||
}
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(e.statusCode())
|
||||
fmt.Fprintln(w, e.toJsonString())
|
||||
}
|
||||
|
50
error/error_test.go
Normal file
50
error/error_test.go
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -83,13 +83,6 @@ $ etcdctl get /foo/bar
|
||||
Hello world
|
||||
```
|
||||
|
||||
Get the current value for a key within the cluster:
|
||||
|
||||
```
|
||||
$ etcdctl get /foo/bar --consistent
|
||||
Hello world
|
||||
```
|
||||
|
||||
Get the value of a key with additional metadata in a parseable format:
|
||||
|
||||
```
|
||||
@ -203,26 +196,26 @@ Continuously watch a key and exec a program:
|
||||
```
|
||||
$ etcdctl exec-watch /foo/bar -- sh -c "env | grep ETCD"
|
||||
ETCD_WATCH_ACTION=set
|
||||
ETCD_VALUE=My configuration stuff
|
||||
ETCD_MODIFIED_INDEX=1999
|
||||
ETCD_KEY=/foo/bar
|
||||
ETCD_WATCH_VALUE=My configuration stuff
|
||||
ETCD_WATCH_MODIFIED_INDEX=1999
|
||||
ETCD_WATCH_KEY=/foo/bar
|
||||
ETCD_WATCH_ACTION=set
|
||||
ETCD_VALUE=My new configuration stuff
|
||||
ETCD_MODIFIED_INDEX=2000
|
||||
ETCD_KEY=/foo/bar
|
||||
ETCD_WATCH_VALUE=My new configuration stuff
|
||||
ETCD_WATCH_MODIFIED_INDEX=2000
|
||||
ETCD_WATCH_KEY=/foo/bar
|
||||
```
|
||||
|
||||
Continuously and recursively watch a key and exec a program:
|
||||
```
|
||||
$ etcdctl exec-watch --recursive /foo -- sh -c "env | grep ETCD"
|
||||
ETCD_WATCH_ACTION=set
|
||||
ETCD_VALUE=My configuration stuff
|
||||
ETCD_MODIFIED_INDEX=1999
|
||||
ETCD_KEY=/foo/bar
|
||||
ETCD_WATCH_VALUE=My configuration stuff
|
||||
ETCD_WATCH_MODIFIED_INDEX=1999
|
||||
ETCD_WATCH_KEY=/foo/bar
|
||||
ETCD_WATCH_ACTION=set
|
||||
ETCD_VALUE=My new configuration stuff
|
||||
ETCD_MODIFIED_INDEX=2000
|
||||
ETCD_KEY=/foo/barbar
|
||||
ETCD_WATCH_VALUE=My new configuration stuff
|
||||
ETCD_WATCH_MODIFIED_INDEX=2000
|
||||
ETCD_WATCH_KEY=/foo/barbar
|
||||
```
|
||||
|
||||
## Return Codes
|
||||
|
100
etcdctl/command/backup_command.go
Normal file
100
etcdctl/command/backup_command.go
Normal file
@ -0,0 +1,100 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||
"github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/pkg/idutil"
|
||||
"github.com/coreos/etcd/pkg/pbutil"
|
||||
"github.com/coreos/etcd/snap"
|
||||
"github.com/coreos/etcd/wal"
|
||||
"github.com/coreos/etcd/wal/walpb"
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
if err := os.MkdirAll(destSnap, 0700); err != nil {
|
||||
log.Fatalf("failed creating backup snapshot dir %v: %v", destSnap, err)
|
||||
}
|
||||
ss := snap.New(srcSnap)
|
||||
snapshot, err := ss.Load()
|
||||
if err != nil && err != snap.ErrNoSnapshot {
|
||||
log.Fatal(err)
|
||||
}
|
||||
var walsnap walpb.Snapshot
|
||||
if snapshot != nil {
|
||||
walsnap.Index, walsnap.Term = snapshot.Metadata.Index, snapshot.Metadata.Term
|
||||
newss := snap.New(destSnap)
|
||||
if err := newss.SaveSnap(*snapshot); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
w, err := wal.OpenNotInUse(srcWAL, walsnap)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer w.Close()
|
||||
wmetadata, state, ents, err := w.ReadAll()
|
||||
switch err {
|
||||
case nil:
|
||||
case wal.ErrSnapshotNotFound:
|
||||
fmt.Printf("Failed to find the match snapshot record %+v in wal %v.", walsnap, srcWAL)
|
||||
fmt.Printf("etcdctl will add it back. Start auto fixing...")
|
||||
default:
|
||||
log.Fatal(err)
|
||||
}
|
||||
var metadata etcdserverpb.Metadata
|
||||
pbutil.MustUnmarshal(&metadata, wmetadata)
|
||||
idgen := idutil.NewGenerator(0, time.Now())
|
||||
metadata.NodeID = idgen.Next()
|
||||
metadata.ClusterID = idgen.Next()
|
||||
|
||||
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)
|
||||
}
|
||||
if err := neww.SaveSnapshot(walsnap); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
140
etcdctl/command/cluster_health.go
Normal file
140
etcdctl/command/cluster_health.go
Normal file
@ -0,0 +1,140 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd"
|
||||
"github.com/coreos/etcd/etcdserver/stats"
|
||||
)
|
||||
|
||||
func NewClusterHealthCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "cluster-health",
|
||||
Usage: "check the health of the etcd cluster",
|
||||
Flags: []cli.Flag{},
|
||||
Action: handleClusterHealth,
|
||||
}
|
||||
}
|
||||
|
||||
func handleClusterHealth(c *cli.Context) {
|
||||
endpoints, err := getEndpoints(c)
|
||||
if err != nil {
|
||||
handleError(ErrorFromEtcd, err)
|
||||
}
|
||||
tr, err := getTransport(c)
|
||||
if err != nil {
|
||||
handleError(ErrorFromEtcd, err)
|
||||
}
|
||||
|
||||
client := etcd.NewClient(endpoints)
|
||||
client.SetTransport(tr)
|
||||
|
||||
if c.GlobalBool("debug") {
|
||||
go dumpCURL(client)
|
||||
}
|
||||
|
||||
if ok := client.SyncCluster(); !ok {
|
||||
handleError(FailedToConnectToHost, errors.New("cannot sync with the cluster using endpoints "+strings.Join(endpoints, ", ")))
|
||||
}
|
||||
|
||||
// do we have a leader?
|
||||
ep, ls0, err := getLeaderStats(tr, client.GetCluster())
|
||||
if err != nil {
|
||||
fmt.Println("cluster is unhealthy")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// is raft stable and making progress?
|
||||
client = etcd.NewClient([]string{ep})
|
||||
resp, err := client.Get("/", false, false)
|
||||
if err != nil {
|
||||
fmt.Println("cluster is unhealthy")
|
||||
os.Exit(1)
|
||||
}
|
||||
rt0, ri0 := resp.RaftTerm, resp.RaftIndex
|
||||
time.Sleep(time.Second)
|
||||
|
||||
resp, err = client.Get("/", false, false)
|
||||
if err != nil {
|
||||
fmt.Println("cluster is unhealthy")
|
||||
os.Exit(1)
|
||||
}
|
||||
rt1, ri1 := resp.RaftTerm, resp.RaftIndex
|
||||
|
||||
if rt0 != rt1 {
|
||||
fmt.Println("cluster is unhealthy")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if ri1 == ri0 {
|
||||
fmt.Println("cluster is unhealthy")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// are all the members makeing progress?
|
||||
_, ls1, err := getLeaderStats(tr, []string{ep})
|
||||
if err != nil {
|
||||
fmt.Println("cluster is unhealthy")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("cluster is healthy")
|
||||
// self is healthy
|
||||
var prints []string
|
||||
|
||||
prints = append(prints, fmt.Sprintf("member %s is healthy\n", ls1.Leader))
|
||||
for name, fs0 := range ls0.Followers {
|
||||
fs1, ok := ls1.Followers[name]
|
||||
if !ok {
|
||||
fmt.Println("Cluster configuration changed during health checking. Please retry.")
|
||||
os.Exit(1)
|
||||
}
|
||||
if fs1.Counts.Success <= fs0.Counts.Success {
|
||||
prints = append(prints, fmt.Sprintf("member %s is unhealthy\n", name))
|
||||
} else {
|
||||
prints = append(prints, fmt.Sprintf("member %s is healthy\n", name))
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(prints)
|
||||
for _, p := range prints {
|
||||
fmt.Print(p)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func getLeaderStats(tr *http.Transport, endpoints []string) (string, *stats.LeaderStats, error) {
|
||||
// go-etcd does not support cluster stats, use http client for now
|
||||
// TODO: use new etcd client with new member/stats endpoint
|
||||
httpclient := http.Client{
|
||||
Transport: tr,
|
||||
}
|
||||
|
||||
for _, ep := range endpoints {
|
||||
resp, err := httpclient.Get(ep + "/v2/stats/leader")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
continue
|
||||
}
|
||||
|
||||
ls := &stats.LeaderStats{}
|
||||
d := json.NewDecoder(resp.Body)
|
||||
err = d.Decode(ls)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return ep, ls, nil
|
||||
}
|
||||
return "", nil, errors.New("no leader")
|
||||
}
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
@ -16,7 +30,6 @@ func NewGetCommand() cli.Command {
|
||||
Usage: "retrieve the value of a key",
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{Name: "sort", Usage: "returns result in sorted order"},
|
||||
cli.BoolFlag{Name: "consistent", Usage: "send request to the leader, thereby guranteeing that any earlier writes will be seen by the read"},
|
||||
},
|
||||
Action: func(c *cli.Context) {
|
||||
handleGet(c, getCommandFunc)
|
||||
@ -45,16 +58,8 @@ func getCommandFunc(c *cli.Context, client *etcd.Client) (*etcd.Response, error)
|
||||
return nil, errors.New("Key required")
|
||||
}
|
||||
key := c.Args()[0]
|
||||
consistent := c.Bool("consistent")
|
||||
sorted := c.Bool("sort")
|
||||
|
||||
// Setup consistency on the client.
|
||||
if consistent {
|
||||
client.SetConsistency(etcd.STRONG_CONSISTENCY)
|
||||
} else {
|
||||
client.SetConsistency(etcd.WEAK_CONSISTENCY)
|
||||
}
|
||||
|
||||
// Retrieve the value from the server.
|
||||
return client.Get(key, sorted, false)
|
||||
}
|
||||
|
@ -1,10 +1,23 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@ -24,72 +37,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
|
||||
}
|
||||
|
||||
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, ",")
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
peers := getPeersFlagValue(c)
|
||||
|
||||
// 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
|
||||
endpoints, err := getEndpoints(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := etcd.NewClient(peers)
|
||||
tr, err := getTransport(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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.
|
||||
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
|
@ -1,12 +1,26 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
|
||||
"github.com/coreos/etcd/client"
|
||||
)
|
||||
|
||||
@ -35,20 +49,39 @@ func NewMemberCommand() cli.Command {
|
||||
}
|
||||
|
||||
func mustNewMembersAPI(c *cli.Context) client.MembersAPI {
|
||||
peers := getPeersFlagValue(c)
|
||||
for i, p := range peers {
|
||||
if !strings.HasPrefix(p, "http") && !strings.HasPrefix(p, "https") {
|
||||
peers[i] = fmt.Sprintf("http://%s", p)
|
||||
}
|
||||
}
|
||||
|
||||
mAPI, err := client.NewMembersAPI(&http.Transport{}, peers, client.DefaultRequestTimeout)
|
||||
eps, err := getEndpoints(c)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return mAPI
|
||||
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) {
|
||||
@ -57,7 +90,9 @@ func actionMemberList(c *cli.Context) {
|
||||
os.Exit(1)
|
||||
}
|
||||
mAPI := mustNewMembersAPI(c)
|
||||
members, err := mAPI.List()
|
||||
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)
|
||||
@ -78,7 +113,9 @@ func actionMemberAdd(c *cli.Context) {
|
||||
mAPI := mustNewMembersAPI(c)
|
||||
|
||||
url := args[1]
|
||||
m, err := mAPI.Add(url)
|
||||
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)
|
||||
@ -88,7 +125,9 @@ func actionMemberAdd(c *cli.Context) {
|
||||
newName := args[0]
|
||||
fmt.Printf("Added member named %s with ID %s to cluster\n", newName, newID)
|
||||
|
||||
members, err := mAPI.List()
|
||||
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)
|
||||
@ -117,13 +156,41 @@ func actionMemberRemove(c *cli.Context) {
|
||||
fmt.Fprintln(os.Stderr, "Provide a single member ID")
|
||||
os.Exit(1)
|
||||
}
|
||||
removalID := args[0]
|
||||
|
||||
mAPI := mustNewMembersAPI(c)
|
||||
mID := args[0]
|
||||
if err := mAPI.Remove(mID); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
// Get the list of members.
|
||||
listctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
|
||||
members, err := mAPI.List(listctx)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error while verifying ID against known members:", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
// Sanity check the input.
|
||||
foundID := false
|
||||
for _, m := range members {
|
||||
if m.ID == removalID {
|
||||
foundID = true
|
||||
}
|
||||
if m.Name == removalID {
|
||||
// Note that, so long as it's not ambiguous, we *could* do the right thing by name here.
|
||||
fmt.Fprintf(os.Stderr, "Found a member named %s; if this is correct, please use its ID, eg:\n\tetcdctl member remove %s\n", m.Name, m.ID)
|
||||
fmt.Fprintf(os.Stderr, "For more details, read the documentation at https://github.com/coreos/etcd/blob/master/Documentation/runtime-configuration.md#remove-a-member\n\n")
|
||||
}
|
||||
}
|
||||
if !foundID {
|
||||
fmt.Fprintf(os.Stderr, "Couldn't find a member in the cluster with an ID of %s.\n", removalID)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Removed member %s from cluster\n", mID)
|
||||
// Actually attempt to remove the member.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
|
||||
err = mAPI.Remove(ctx, removalID)
|
||||
cancel()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Recieved an error trying to remove member %s: %s", removalID, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Removed member %s from cluster\n", removalID)
|
||||
}
|
||||
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
|
78
etcdctl/command/upgrade.go
Normal file
78
etcdctl/command/upgrade.go
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
Copyright 2015 CoreOS, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||
"github.com/coreos/etcd/pkg/transport"
|
||||
)
|
||||
|
||||
func UpgradeCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "upgrade",
|
||||
Usage: "upgrade an old version etcd cluster to a new version",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{Name: "old-version", Value: "1", Usage: "Old internal version"},
|
||||
cli.StringFlag{Name: "new-version", Value: "2", Usage: "New internal version"},
|
||||
cli.StringFlag{Name: "peer-url", Value: "http://localhost:7001", Usage: "An etcd peer url string"},
|
||||
cli.StringFlag{Name: "peer-cert-file", Value: "", Usage: "identify HTTPS peer using this SSL certificate file"},
|
||||
cli.StringFlag{Name: "peer-key-file", Value: "", Usage: "identify HTTPS peer using this SSL key file"},
|
||||
cli.StringFlag{Name: "peer-ca-file", Value: "", Usage: "verify certificates of HTTPS-enabled peers using this CA bundle"},
|
||||
},
|
||||
Action: handleUpgrade,
|
||||
}
|
||||
}
|
||||
|
||||
func handleUpgrade(c *cli.Context) {
|
||||
if c.String("old-version") != "1" {
|
||||
fmt.Printf("Do not support upgrade from version %s\n", c.String("old-version"))
|
||||
os.Exit(1)
|
||||
}
|
||||
if c.String("new-version") != "2" {
|
||||
fmt.Printf("Do not support upgrade to version %s\n", c.String("new-version"))
|
||||
os.Exit(1)
|
||||
}
|
||||
tls := transport.TLSInfo{
|
||||
CAFile: c.String("peer-ca-file"),
|
||||
CertFile: c.String("peer-cert-file"),
|
||||
KeyFile: c.String("peer-key-file"),
|
||||
}
|
||||
t, err := transport.NewTransport(tls)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
client := http.Client{Transport: t}
|
||||
resp, err := client.Get(c.String("peer-url") + "/v2/admin/next-internal-version")
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to send upgrade request to %s: %v\n", c.String("peer-url"), err)
|
||||
return
|
||||
}
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
fmt.Println("Cluster will start upgrading from internal version 1 to 2 in 10 seconds.")
|
||||
return
|
||||
}
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
fmt.Println("Cluster cannot upgrade to 2: version is not 0.4.7")
|
||||
return
|
||||
}
|
||||
fmt.Printf("Faild to send upgrade request to %s: bad status code %d\n", c.String("cluster-url"), resp.StatusCode)
|
||||
}
|
@ -1,10 +1,30 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"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 +53,63 @@ 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) {
|
||||
cafile := c.GlobalString("ca-file")
|
||||
certfile := c.GlobalString("cert-file")
|
||||
keyfile := c.GlobalString("key-file")
|
||||
|
||||
// Use an environment variable if nothing was supplied on the
|
||||
// command line
|
||||
if cafile == "" {
|
||||
cafile = os.Getenv("ETCDCTL_CA_FILE")
|
||||
}
|
||||
if certfile == "" {
|
||||
certfile = os.Getenv("ETCDCTL_CERT_FILE")
|
||||
}
|
||||
if keyfile == "" {
|
||||
keyfile = os.Getenv("ETCDCTL_KEY_FILE")
|
||||
}
|
||||
|
||||
tls := transport.TLSInfo{
|
||||
CAFile: cafile,
|
||||
CertFile: certfile,
|
||||
KeyFile: keyfile,
|
||||
}
|
||||
return transport.NewTransport(tls)
|
||||
|
||||
}
|
||||
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@ -19,8 +33,13 @@ 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.NewClusterHealthCommand(),
|
||||
command.NewMakeCommand(),
|
||||
command.NewMakeDirCommand(),
|
||||
command.NewRemoveCommand(),
|
||||
@ -34,6 +53,7 @@ func main() {
|
||||
command.NewWatchCommand(),
|
||||
command.NewExecWatchCommand(),
|
||||
command.NewMemberCommand(),
|
||||
command.UpgradeCommand(),
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
|
270
etcdmain/config.go
Normal file
270
etcdmain/config.go
Normal file
@ -0,0 +1,270 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdmain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/pkg/cors"
|
||||
"github.com/coreos/etcd/pkg/flags"
|
||||
"github.com/coreos/etcd/pkg/netutil"
|
||||
"github.com/coreos/etcd/pkg/transport"
|
||||
"github.com/coreos/etcd/version"
|
||||
)
|
||||
|
||||
const (
|
||||
proxyFlagOff = "off"
|
||||
proxyFlagReadonly = "readonly"
|
||||
proxyFlagOn = "on"
|
||||
|
||||
fallbackFlagExit = "exit"
|
||||
fallbackFlagProxy = "proxy"
|
||||
|
||||
clusterStateFlagNew = "new"
|
||||
clusterStateFlagExisting = "existing"
|
||||
)
|
||||
|
||||
var (
|
||||
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",
|
||||
}
|
||||
|
||||
ErrConflictBootstrapFlags = fmt.Errorf("multiple discovery or bootstrap flags are set" +
|
||||
"Choose one of \"initial-cluster\", \"discovery\" or \"discovery-srv\"")
|
||||
)
|
||||
|
||||
type config struct {
|
||||
*flag.FlagSet
|
||||
|
||||
// member
|
||||
corsInfo *cors.CORSInfo
|
||||
dir string
|
||||
lpurls, lcurls []url.URL
|
||||
maxSnapFiles uint
|
||||
maxWalFiles uint
|
||||
name string
|
||||
snapCount uint64
|
||||
// TODO: decouple tickMs and heartbeat tick (current heartbeat tick = 1).
|
||||
// make ticks a cluster wide configuration.
|
||||
TickMs uint
|
||||
ElectionMs uint
|
||||
|
||||
// clustering
|
||||
apurls, acurls []url.URL
|
||||
clusterState *flags.StringsFlag
|
||||
dnsCluster string
|
||||
dproxy string
|
||||
durl string
|
||||
fallback *flags.StringsFlag
|
||||
initialCluster string
|
||||
initialClusterToken string
|
||||
|
||||
// proxy
|
||||
proxy *flags.StringsFlag
|
||||
|
||||
// security
|
||||
clientTLSInfo, peerTLSInfo transport.TLSInfo
|
||||
|
||||
// unsafe
|
||||
forceNewCluster bool
|
||||
|
||||
printVersion bool
|
||||
|
||||
ignored []string
|
||||
}
|
||||
|
||||
func NewConfig() *config {
|
||||
cfg := &config{
|
||||
corsInfo: &cors.CORSInfo{},
|
||||
clusterState: flags.NewStringsFlag(
|
||||
clusterStateFlagNew,
|
||||
clusterStateFlagExisting,
|
||||
),
|
||||
fallback: flags.NewStringsFlag(
|
||||
fallbackFlagExit,
|
||||
fallbackFlagProxy,
|
||||
),
|
||||
ignored: ignored,
|
||||
proxy: flags.NewStringsFlag(
|
||||
proxyFlagOff,
|
||||
proxyFlagReadonly,
|
||||
proxyFlagOn,
|
||||
),
|
||||
}
|
||||
|
||||
cfg.FlagSet = flag.NewFlagSet("etcd", flag.ContinueOnError)
|
||||
fs := cfg.FlagSet
|
||||
fs.Usage = func() {
|
||||
fmt.Println(usageline)
|
||||
fmt.Println(flagsline)
|
||||
}
|
||||
|
||||
// member
|
||||
fs.Var(cfg.corsInfo, "cors", "Comma-separated white list of origins for CORS (cross-origin resource sharing).")
|
||||
fs.StringVar(&cfg.dir, "data-dir", "", "Path to the data directory")
|
||||
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.UintVar(&cfg.maxSnapFiles, "max-snapshots", defaultMaxSnapshots, "Maximum number of snapshot files to retain (0 is unlimited)")
|
||||
fs.UintVar(&cfg.maxWalFiles, "max-wals", defaultMaxWALs, "Maximum number of wal files to retain (0 is unlimited)")
|
||||
fs.StringVar(&cfg.name, "name", "default", "Unique human-readable name for this node")
|
||||
fs.Uint64Var(&cfg.snapCount, "snapshot-count", etcdserver.DefaultSnapCount, "Number of committed transactions to trigger a snapshot")
|
||||
fs.UintVar(&cfg.TickMs, "heartbeat-interval", 100, "Time (in milliseconds) of a heartbeat interval.")
|
||||
fs.UintVar(&cfg.ElectionMs, "election-timeout", 1000, "Time (in milliseconds) for an election to timeout.")
|
||||
|
||||
// clustering
|
||||
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.StringVar(&cfg.durl, "discovery", "", "Discovery service used to bootstrap the initial cluster")
|
||||
fs.Var(cfg.fallback, "discovery-fallback", fmt.Sprintf("Valid values include %s", strings.Join(cfg.fallback.Values, ", ")))
|
||||
if err := cfg.fallback.Set(fallbackFlagProxy); err != nil {
|
||||
// Should never happen.
|
||||
log.Panicf("unexpected error setting up discovery-fallback flag: %v", err)
|
||||
}
|
||||
fs.StringVar(&cfg.dproxy, "discovery-proxy", "", "HTTP proxy to use for traffic to discovery service")
|
||||
fs.StringVar(&cfg.dnsCluster, "discovery-srv", "", "DNS domain used to bootstrap initial cluster")
|
||||
fs.StringVar(&cfg.initialCluster, "initial-cluster", "default=http://localhost:2380,default=http://localhost:7001", "Initial cluster configuration for bootstrapping")
|
||||
fs.StringVar(&cfg.initialClusterToken, "initial-cluster-token", "etcd-cluster", "Initial cluster token for the etcd cluster during bootstrap")
|
||||
fs.Var(cfg.clusterState, "initial-cluster-state", "Initial cluster configuration for bootstrapping")
|
||||
if err := cfg.clusterState.Set(clusterStateFlagNew); err != nil {
|
||||
// Should never happen.
|
||||
log.Panicf("unexpected error setting up clusterStateFlag: %v", err)
|
||||
}
|
||||
|
||||
// proxy
|
||||
fs.Var(cfg.proxy, "proxy", fmt.Sprintf("Valid values include %s", strings.Join(cfg.proxy.Values, ", ")))
|
||||
if err := cfg.proxy.Set(proxyFlagOff); err != nil {
|
||||
// Should never happen.
|
||||
log.Panicf("unexpected error setting up proxyFlag: %v", err)
|
||||
}
|
||||
|
||||
// security
|
||||
fs.StringVar(&cfg.clientTLSInfo.CAFile, "ca-file", "", "Path to the client server TLS CA file.")
|
||||
fs.StringVar(&cfg.clientTLSInfo.CertFile, "cert-file", "", "Path to the client server TLS cert file.")
|
||||
fs.StringVar(&cfg.clientTLSInfo.KeyFile, "key-file", "", "Path to the client server TLS key file.")
|
||||
fs.StringVar(&cfg.peerTLSInfo.CAFile, "peer-ca-file", "", "Path to the peer server TLS CA file.")
|
||||
fs.StringVar(&cfg.peerTLSInfo.CertFile, "peer-cert-file", "", "Path to the peer server TLS cert file.")
|
||||
fs.StringVar(&cfg.peerTLSInfo.KeyFile, "peer-key-file", "", "Path to the peer server TLS key file.")
|
||||
|
||||
// unsafe
|
||||
fs.BoolVar(&cfg.forceNewCluster, "force-new-cluster", false, "Force to create a new one member cluster")
|
||||
|
||||
// version
|
||||
fs.BoolVar(&cfg.printVersion, "version", false, "Print the version and exit")
|
||||
|
||||
// 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.")
|
||||
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")
|
||||
|
||||
// ignored
|
||||
for _, f := range cfg.ignored {
|
||||
fs.Var(&flags.IgnoredFlag{Name: f}, f, "")
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (cfg *config) Parse(arguments []string) error {
|
||||
perr := cfg.FlagSet.Parse(arguments)
|
||||
switch perr {
|
||||
case nil:
|
||||
case flag.ErrHelp:
|
||||
os.Exit(0)
|
||||
default:
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if cfg.printVersion {
|
||||
fmt.Println("etcd version", version.Version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
err := flags.SetFlagsFromEnv(cfg.FlagSet)
|
||||
if err != nil {
|
||||
log.Fatalf("etcd: %v", err)
|
||||
}
|
||||
|
||||
set := make(map[string]bool)
|
||||
cfg.FlagSet.Visit(func(f *flag.Flag) {
|
||||
set[f.Name] = true
|
||||
})
|
||||
nSet := 0
|
||||
for _, v := range []bool{set["discovery"], set["initial-cluster"], set["discovery-srv"]} {
|
||||
if v {
|
||||
nSet += 1
|
||||
}
|
||||
}
|
||||
if nSet > 1 {
|
||||
return ErrConflictBootstrapFlags
|
||||
}
|
||||
|
||||
flags.SetBindAddrFromAddr(cfg.FlagSet, "peer-bind-addr", "peer-addr")
|
||||
flags.SetBindAddrFromAddr(cfg.FlagSet, "bind-addr", "addr")
|
||||
|
||||
cfg.lpurls, err = flags.URLsFromFlags(cfg.FlagSet, "listen-peer-urls", "peer-bind-addr", cfg.peerTLSInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.apurls, err = flags.URLsFromFlags(cfg.FlagSet, "initial-advertise-peer-urls", "peer-addr", cfg.peerTLSInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.lcurls, err = flags.URLsFromFlags(cfg.FlagSet, "listen-client-urls", "bind-addr", cfg.clientTLSInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.acurls, err = flags.URLsFromFlags(cfg.FlagSet, "advertise-client-urls", "addr", cfg.clientTLSInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cfg.resolveUrls(); err != nil {
|
||||
return errors.New("cannot resolve DNS hostnames.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *config) resolveUrls() error {
|
||||
return netutil.ResolveTCPAddrs(cfg.lpurls, cfg.apurls, cfg.lcurls, cfg.acurls)
|
||||
}
|
||||
|
||||
func (cfg config) isNewCluster() bool { return cfg.clusterState.String() == clusterStateFlagNew }
|
||||
func (cfg config) isProxy() bool { return cfg.proxy.String() != proxyFlagOff }
|
||||
func (cfg config) isReadonlyProxy() bool { return cfg.proxy.String() == proxyFlagReadonly }
|
||||
func (cfg config) shouldFallbackToProxy() bool { return cfg.fallback.String() == fallbackFlagProxy }
|
||||
|
||||
func (cfg config) electionTicks() int { return int(cfg.ElectionMs / cfg.TickMs) }
|
289
etcdmain/config_test.go
Normal file
289
etcdmain/config_test.go
Normal file
@ -0,0 +1,289 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdmain
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigParsingMemberFlags(t *testing.T) {
|
||||
args := []string{
|
||||
"-data-dir=testdir",
|
||||
"-name=testname",
|
||||
"-max-wals=10",
|
||||
"-max-snapshots=10",
|
||||
"-snapshot-count=10",
|
||||
"-listen-peer-urls=http://localhost:8000,https://localhost:8001",
|
||||
"-listen-client-urls=http://localhost:7000,https://localhost:7001",
|
||||
}
|
||||
wcfg := &config{
|
||||
dir: "testdir",
|
||||
lpurls: []url.URL{{Scheme: "http", Host: "localhost:8000"}, {Scheme: "https", Host: "localhost:8001"}},
|
||||
lcurls: []url.URL{{Scheme: "http", Host: "localhost:7000"}, {Scheme: "https", Host: "localhost:7001"}},
|
||||
maxSnapFiles: 10,
|
||||
maxWalFiles: 10,
|
||||
name: "testname",
|
||||
snapCount: 10,
|
||||
}
|
||||
|
||||
cfg := NewConfig()
|
||||
err := cfg.Parse(args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.dir != wcfg.dir {
|
||||
t.Errorf("dir = %v, want %v", cfg.dir, wcfg.dir)
|
||||
}
|
||||
if cfg.maxSnapFiles != wcfg.maxSnapFiles {
|
||||
t.Errorf("maxsnap = %v, want %v", cfg.maxSnapFiles, wcfg.maxSnapFiles)
|
||||
}
|
||||
if cfg.maxWalFiles != wcfg.maxWalFiles {
|
||||
t.Errorf("maxwal = %v, want %v", cfg.maxWalFiles, wcfg.maxWalFiles)
|
||||
}
|
||||
if cfg.name != wcfg.name {
|
||||
t.Errorf("name = %v, want %v", cfg.name, wcfg.name)
|
||||
}
|
||||
if cfg.snapCount != wcfg.snapCount {
|
||||
t.Errorf("snapcount = %v, want %v", cfg.snapCount, wcfg.snapCount)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.lpurls, wcfg.lpurls) {
|
||||
t.Errorf("listen-peer-urls = %v, want %v", cfg.lpurls, wcfg.lpurls)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.lcurls, wcfg.lcurls) {
|
||||
t.Errorf("listen-client-urls = %v, want %v", cfg.lcurls, wcfg.lcurls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigParsingClusteringFlags(t *testing.T) {
|
||||
args := []string{
|
||||
"-initial-cluster=0=http://localhost:8000",
|
||||
"-initial-cluster-state=existing",
|
||||
"-initial-cluster-token=etcdtest",
|
||||
"-initial-advertise-peer-urls=http://localhost:8000,https://localhost:8001",
|
||||
"-advertise-client-urls=http://localhost:7000,https://localhost:7001",
|
||||
"-discovery-fallback=exit",
|
||||
}
|
||||
wcfg := NewConfig()
|
||||
wcfg.apurls = []url.URL{{Scheme: "http", Host: "localhost:8000"}, {Scheme: "https", Host: "localhost:8001"}}
|
||||
wcfg.acurls = []url.URL{{Scheme: "http", Host: "localhost:7000"}, {Scheme: "https", Host: "localhost:7001"}}
|
||||
wcfg.clusterState.Set(clusterStateFlagExisting)
|
||||
wcfg.fallback.Set(fallbackFlagExit)
|
||||
wcfg.initialCluster = "0=http://localhost:8000"
|
||||
wcfg.initialClusterToken = "etcdtest"
|
||||
|
||||
cfg := NewConfig()
|
||||
err := cfg.Parse(args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.clusterState.String() != wcfg.clusterState.String() {
|
||||
t.Errorf("clusterState = %v, want %v", cfg.clusterState, wcfg.clusterState)
|
||||
}
|
||||
if cfg.fallback.String() != wcfg.fallback.String() {
|
||||
t.Errorf("fallback = %v, want %v", cfg.fallback, wcfg.fallback)
|
||||
}
|
||||
if cfg.initialCluster != wcfg.initialCluster {
|
||||
t.Errorf("initialCluster = %v, want %v", cfg.initialCluster, wcfg.initialCluster)
|
||||
}
|
||||
if cfg.initialClusterToken != wcfg.initialClusterToken {
|
||||
t.Errorf("initialClusterToken = %v, want %v", cfg.initialClusterToken, wcfg.initialClusterToken)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.apurls, wcfg.apurls) {
|
||||
t.Errorf("initial-advertise-peer-urls = %v, want %v", cfg.lpurls, wcfg.lpurls)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.acurls, wcfg.acurls) {
|
||||
t.Errorf("advertise-client-urls = %v, want %v", cfg.lcurls, wcfg.lcurls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigParsingOtherFlags(t *testing.T) {
|
||||
args := []string{
|
||||
"-proxy=readonly",
|
||||
"-ca-file=cafile",
|
||||
"-cert-file=certfile",
|
||||
"-key-file=keyfile",
|
||||
"-peer-ca-file=peercafile",
|
||||
"-peer-cert-file=peercertfile",
|
||||
"-peer-key-file=peerkeyfile",
|
||||
"-force-new-cluster=true",
|
||||
}
|
||||
|
||||
wcfg := NewConfig()
|
||||
wcfg.proxy.Set(proxyFlagReadonly)
|
||||
wcfg.clientTLSInfo.CAFile = "cafile"
|
||||
wcfg.clientTLSInfo.CertFile = "certfile"
|
||||
wcfg.clientTLSInfo.KeyFile = "keyfile"
|
||||
wcfg.peerTLSInfo.CAFile = "peercafile"
|
||||
wcfg.peerTLSInfo.CertFile = "peercertfile"
|
||||
wcfg.peerTLSInfo.KeyFile = "peerkeyfile"
|
||||
wcfg.forceNewCluster = true
|
||||
|
||||
cfg := NewConfig()
|
||||
err := cfg.Parse(args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.proxy.String() != wcfg.proxy.String() {
|
||||
t.Errorf("proxy = %v, want %v", cfg.proxy, wcfg.proxy)
|
||||
}
|
||||
if cfg.clientTLSInfo.String() != wcfg.clientTLSInfo.String() {
|
||||
t.Errorf("clientTLS = %v, want %v", cfg.clientTLSInfo, wcfg.clientTLSInfo)
|
||||
}
|
||||
if cfg.peerTLSInfo.String() != wcfg.peerTLSInfo.String() {
|
||||
t.Errorf("peerTLS = %v, want %v", cfg.peerTLSInfo, wcfg.peerTLSInfo)
|
||||
}
|
||||
if cfg.forceNewCluster != wcfg.forceNewCluster {
|
||||
t.Errorf("forceNewCluster = %t, want %t", cfg.forceNewCluster, wcfg.forceNewCluster)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigParsingV1Flags(t *testing.T) {
|
||||
args := []string{
|
||||
"-peer-addr=127.0.0.1:7001",
|
||||
"-addr=127.0.0.1:4001",
|
||||
}
|
||||
wcfg := NewConfig()
|
||||
wcfg.lpurls = []url.URL{{Scheme: "http", Host: "0.0.0.0:7001"}}
|
||||
wcfg.apurls = []url.URL{{Scheme: "http", Host: "127.0.0.1:7001"}}
|
||||
wcfg.lcurls = []url.URL{{Scheme: "http", Host: "0.0.0.0:4001"}}
|
||||
wcfg.acurls = []url.URL{{Scheme: "http", Host: "127.0.0.1:4001"}}
|
||||
|
||||
cfg := NewConfig()
|
||||
if err := cfg.Parse(args); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.lpurls, wcfg.lpurls) {
|
||||
t.Errorf("listen peer urls = %+v, want %+v", cfg.lpurls, wcfg.lpurls)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.apurls, wcfg.apurls) {
|
||||
t.Errorf("advertise peer urls = %+v, want %+v", cfg.apurls, wcfg.apurls)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.lcurls, wcfg.lcurls) {
|
||||
t.Errorf("listen client urls = %+v, want %+v", cfg.lcurls, wcfg.lcurls)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.acurls, wcfg.acurls) {
|
||||
t.Errorf("advertise client urls = %+v, want %+v", cfg.acurls, wcfg.acurls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigParsingConflictClusteringFlags(t *testing.T) {
|
||||
conflictArgs := [][]string{
|
||||
[]string{
|
||||
"-initial-cluster=0=localhost:8000",
|
||||
"-discovery=http://example.com/abc",
|
||||
},
|
||||
[]string{
|
||||
"-discovery-srv=example.com",
|
||||
"-discovery=http://example.com/abc",
|
||||
},
|
||||
[]string{
|
||||
"-initial-cluster=0=localhost:8000",
|
||||
"-discovery-srv=example.com",
|
||||
},
|
||||
[]string{
|
||||
"-initial-cluster=0=localhost:8000",
|
||||
"-discovery=http://example.com/abc",
|
||||
"-discovery-srv=example.com",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range conflictArgs {
|
||||
cfg := NewConfig()
|
||||
err := cfg.Parse(tt)
|
||||
if err != ErrConflictBootstrapFlags {
|
||||
t.Errorf("%d: err = %v, want %v", i, err, ErrConflictBootstrapFlags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigIsNewCluster(t *testing.T) {
|
||||
tests := []struct {
|
||||
state string
|
||||
wIsNew bool
|
||||
}{
|
||||
{clusterStateFlagExisting, false},
|
||||
{clusterStateFlagNew, true},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
cfg := NewConfig()
|
||||
if err := cfg.clusterState.Set(tt.state); err != nil {
|
||||
t.Fatalf("#%d: unexpected clusterState.Set error: %v", i, err)
|
||||
}
|
||||
if g := cfg.isNewCluster(); g != tt.wIsNew {
|
||||
t.Errorf("#%d: isNewCluster = %v, want %v", i, g, tt.wIsNew)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigIsProxy(t *testing.T) {
|
||||
tests := []struct {
|
||||
proxy string
|
||||
wIsProxy bool
|
||||
}{
|
||||
{proxyFlagOff, false},
|
||||
{proxyFlagReadonly, true},
|
||||
{proxyFlagOn, true},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
cfg := NewConfig()
|
||||
if err := cfg.proxy.Set(tt.proxy); err != nil {
|
||||
t.Fatalf("#%d: unexpected proxy.Set error: %v", i, err)
|
||||
}
|
||||
if g := cfg.isProxy(); g != tt.wIsProxy {
|
||||
t.Errorf("#%d: isProxy = %v, want %v", i, g, tt.wIsProxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigIsReadonlyProxy(t *testing.T) {
|
||||
tests := []struct {
|
||||
proxy string
|
||||
wIsReadonly bool
|
||||
}{
|
||||
{proxyFlagOff, false},
|
||||
{proxyFlagReadonly, true},
|
||||
{proxyFlagOn, false},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
cfg := NewConfig()
|
||||
if err := cfg.proxy.Set(tt.proxy); err != nil {
|
||||
t.Fatalf("#%d: unexpected proxy.Set error: %v", i, err)
|
||||
}
|
||||
if g := cfg.isReadonlyProxy(); g != tt.wIsReadonly {
|
||||
t.Errorf("#%d: isReadonlyProxy = %v, want %v", i, g, tt.wIsReadonly)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigShouldFallbackToProxy(t *testing.T) {
|
||||
tests := []struct {
|
||||
fallback string
|
||||
wFallback bool
|
||||
}{
|
||||
{fallbackFlagProxy, true},
|
||||
{fallbackFlagExit, false},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
cfg := NewConfig()
|
||||
if err := cfg.fallback.Set(tt.fallback); err != nil {
|
||||
t.Fatalf("#%d: unexpected fallback.Set error: %v", i, err)
|
||||
}
|
||||
if g := cfg.shouldFallbackToProxy(); g != tt.wFallback {
|
||||
t.Errorf("#%d: shouldFallbackToProxy = %v, want %v", i, g, tt.wFallback)
|
||||
}
|
||||
}
|
||||
}
|
22
etcdmain/const_unix.go
Normal file
22
etcdmain/const_unix.go
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !windows,!plan9
|
||||
|
||||
package etcdmain
|
||||
|
||||
const (
|
||||
defaultMaxSnapshots = 5
|
||||
defaultMaxWALs = 5
|
||||
)
|
26
etcdmain/const_windows.go
Normal file
26
etcdmain/const_windows.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build windows
|
||||
|
||||
package etcdmain
|
||||
|
||||
// TODO(barakmich): So because file locking on Windows is untested, the
|
||||
// temporary fix is to default to unlimited snapshots and WAL files, with manual
|
||||
// removal. Perhaps not the most elegant solution, but it's at least safe and
|
||||
// we'd totally love a PR to fix the story around locking.
|
||||
const (
|
||||
defaultMaxSnapshots = 0
|
||||
defaultMaxWALs = 0
|
||||
)
|
@ -1,18 +1,16 @@
|
||||
/*
|
||||
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 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/* Package etcd contains the main entry point for the etcd binary. */
|
||||
|
||||
|
469
etcdmain/etcd.go
469
etcdmain/etcd.go
@ -1,38 +1,40 @@
|
||||
/*
|
||||
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 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdmain
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"github.com/coreos/etcd/rafthttp"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -40,273 +42,288 @@ const (
|
||||
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")
|
||||
initialClusterToken = fs.String("initial-cluster-token", "etcd-cluster", "Initial cluster token for the etcd cluster during bootstrap")
|
||||
clusterState = new(etcdserver.ClusterState)
|
||||
|
||||
corsInfo = &cors.CORSInfo{}
|
||||
proxyFlag = new(flags.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")
|
||||
if err := clusterState.Set(etcdserver.ClusterStateValueNew); err != nil {
|
||||
// Should never happen.
|
||||
log.Panicf("unexpected error setting up clusterState: %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(flags.ProxyValues, ", ")))
|
||||
if err := proxyFlag.Set(flags.ProxyValueOff); err != nil {
|
||||
// Should never happen.
|
||||
log.Panicf("unexpected error setting up proxyFlag: %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)
|
||||
err := fs.Parse(os.Args[1:])
|
||||
switch err {
|
||||
case nil:
|
||||
case flag.ErrHelp:
|
||||
os.Exit(0)
|
||||
default:
|
||||
cfg := NewConfig()
|
||||
err := cfg.Parse(os.Args[1:])
|
||||
if err != nil {
|
||||
log.Printf("etcd: error verifying flags, %v", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if *printVersion {
|
||||
fmt.Println("etcd version", version.Version)
|
||||
os.Exit(0)
|
||||
var stopped <-chan struct{}
|
||||
|
||||
shouldProxy := cfg.isProxy()
|
||||
if !shouldProxy {
|
||||
stopped, err = startEtcd(cfg)
|
||||
if err == discovery.ErrFullCluster && cfg.shouldFallbackToProxy() {
|
||||
log.Printf("etcd: discovery cluster full, falling back to %s", fallbackFlagProxy)
|
||||
shouldProxy = true
|
||||
}
|
||||
}
|
||||
if shouldProxy {
|
||||
err = startProxy(cfg)
|
||||
}
|
||||
if err != nil {
|
||||
switch err {
|
||||
case discovery.ErrDuplicateID:
|
||||
log.Fatalf("etcd: member %s has previously registered with discovery service (%s), but the data-dir (%s) on disk cannot be found.",
|
||||
cfg.name, cfg.durl, cfg.dir)
|
||||
default:
|
||||
log.Fatalf("etcd: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
flags.SetFlagsFromEnv(fs)
|
||||
|
||||
if string(*proxyFlag) == flags.ProxyValueOff {
|
||||
startEtcd()
|
||||
} else {
|
||||
startProxy()
|
||||
}
|
||||
|
||||
// Block indefinitely
|
||||
<-make(chan struct{})
|
||||
<-stopped
|
||||
}
|
||||
|
||||
// startEtcd launches the etcd server and HTTP handlers for client/server communication.
|
||||
func startEtcd() {
|
||||
cls, err := setupCluster()
|
||||
func startEtcd(cfg *config) (<-chan struct{}, error) {
|
||||
cls, err := setupCluster(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("etcd: error setting up initial cluster: %v", err)
|
||||
return nil, fmt.Errorf("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)
|
||||
}
|
||||
if err := fileutil.IsDirWriteable(*dir); err != nil {
|
||||
log.Fatalf("etcd: cannot write to data directory: %v", err)
|
||||
if cfg.dir == "" {
|
||||
cfg.dir = fmt.Sprintf("%v.etcd", cfg.name)
|
||||
log.Printf("no data-dir provided, using default data-dir ./%s", cfg.dir)
|
||||
}
|
||||
|
||||
pt, err := transport.NewTransport(peerTLSInfo)
|
||||
pt, err := transport.NewTimeoutTransport(cfg.peerTLSInfo, rafthttp.ConnReadTimeout, rafthttp.ConnWriteTimeout)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
acurls, err := flags.URLsFromFlags(fs, "advertise-client-urls", "addr", clientTLSInfo)
|
||||
if !cfg.peerTLSInfo.Empty() {
|
||||
log.Printf("etcd: peerTLS: %s", cfg.peerTLSInfo)
|
||||
}
|
||||
plns := make([]net.Listener, 0)
|
||||
for _, u := range cfg.lpurls {
|
||||
var l net.Listener
|
||||
l, err = transport.NewTimeoutListener(u.Host, u.Scheme, cfg.peerTLSInfo, rafthttp.ConnReadTimeout, rafthttp.ConnWriteTimeout)
|
||||
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)
|
||||
}
|
||||
|
||||
if !cfg.clientTLSInfo.Empty() {
|
||||
log.Printf("etcd: clientTLS: %s", cfg.clientTLSInfo)
|
||||
}
|
||||
clns := make([]net.Listener, 0)
|
||||
for _, u := range cfg.lcurls {
|
||||
var l net.Listener
|
||||
l, err = transport.NewKeepAliveListener(u.Host, u.Scheme, cfg.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)
|
||||
}
|
||||
|
||||
srvcfg := &etcdserver.ServerConfig{
|
||||
Name: cfg.name,
|
||||
ClientURLs: cfg.acurls,
|
||||
PeerURLs: cfg.apurls,
|
||||
DataDir: cfg.dir,
|
||||
SnapCount: cfg.snapCount,
|
||||
MaxSnapFiles: cfg.maxSnapFiles,
|
||||
MaxWALFiles: cfg.maxWalFiles,
|
||||
Cluster: cls,
|
||||
DiscoveryURL: cfg.durl,
|
||||
DiscoveryProxy: cfg.dproxy,
|
||||
NewCluster: cfg.isNewCluster(),
|
||||
ForceNewCluster: cfg.forceNewCluster,
|
||||
Transport: pt,
|
||||
TickMs: cfg.TickMs,
|
||||
ElectionTicks: cfg.electionTicks(),
|
||||
}
|
||||
var s *etcdserver.EtcdServer
|
||||
s, err = etcdserver.NewServer(srvcfg)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
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()
|
||||
|
||||
if cfg.corsInfo.String() != "" {
|
||||
log.Printf("etcd: cors = %s", cfg.corsInfo)
|
||||
}
|
||||
ch := &cors.CORSHandler{
|
||||
Handler: etcdhttp.NewClientHandler(s),
|
||||
Info: corsInfo,
|
||||
Info: cfg.corsInfo,
|
||||
}
|
||||
ph := etcdhttp.NewPeerHandler(s)
|
||||
|
||||
lpurls, err := flags.URLsFromFlags(fs, "listen-peer-urls", "peer-bind-addr", peerTLSInfo)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
ph := etcdhttp.NewPeerHandler(s.Cluster, s.RaftHandler())
|
||||
// Start the peer server in a goroutine
|
||||
for _, l := range plns {
|
||||
go func(l net.Listener) {
|
||||
log.Fatal(serveHTTP(l, ph, 5*time.Minute))
|
||||
}(l)
|
||||
}
|
||||
|
||||
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("etcd: listening for peers on ", urlStr)
|
||||
log.Fatal(http.Serve(l, ph))
|
||||
}()
|
||||
}
|
||||
|
||||
lcurls, err := flags.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("etcd: listening for client requests on ", urlStr)
|
||||
log.Fatal(http.Serve(l, ch))
|
||||
}()
|
||||
for _, l := range clns {
|
||||
go func(l net.Listener) {
|
||||
// read timeout does not work with http close notify
|
||||
// TODO: https://github.com/golang/go/issues/9524
|
||||
log.Fatal(serveHTTP(l, ch, 0))
|
||||
}(l)
|
||||
}
|
||||
return s.StopNotify(), nil
|
||||
}
|
||||
|
||||
// startProxy launches an HTTP proxy for client communication which proxies to other etcd nodes.
|
||||
func startProxy() {
|
||||
cls, err := setupCluster()
|
||||
func startProxy(cfg *config) error {
|
||||
cls, err := setupCluster(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("etcd: error setting up initial cluster: %v", err)
|
||||
return fmt.Errorf("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 cfg.durl != "" {
|
||||
s, err := discovery.GetCluster(cfg.durl, cfg.dproxy)
|
||||
if err != nil {
|
||||
log.Printf("etcd: %v", err)
|
||||
return err
|
||||
}
|
||||
if cls, err = etcdserver.NewClusterFromString(cfg.durl, s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pt, err := transport.NewTransport(cfg.clientTLSInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tr, err := transport.NewTransport(cfg.peerTLSInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.dir == "" {
|
||||
cfg.dir = fmt.Sprintf("%v.etcd", cfg.name)
|
||||
log.Printf("no proxy data-dir provided, using default proxy data-dir ./%s", cfg.dir)
|
||||
}
|
||||
cfg.dir = path.Join(cfg.dir, "proxy")
|
||||
err = os.MkdirAll(cfg.dir, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var peerURLs []string
|
||||
clusterfile := path.Join(cfg.dir, "cluster")
|
||||
|
||||
b, err := ioutil.ReadFile(clusterfile)
|
||||
switch {
|
||||
case err == nil:
|
||||
urls := struct{ PeerURLs []string }{}
|
||||
err := json.Unmarshal(b, &urls)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
peerURLs = urls.PeerURLs
|
||||
log.Printf("proxy: using peer urls %v from cluster file ./%s", peerURLs, clusterfile)
|
||||
case os.IsNotExist(err):
|
||||
peerURLs = cls.PeerURLs()
|
||||
log.Printf("proxy: using peer urls %v ", peerURLs)
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
uf := func() []string {
|
||||
gcls, err := etcdserver.GetClusterFromPeers(peerURLs, tr)
|
||||
// TODO: remove the 2nd check when we fix GetClusterFromPeers
|
||||
// GetClusterFromPeers should not return nil error with an invaild empty cluster
|
||||
if err != nil {
|
||||
log.Printf("proxy: %v", err)
|
||||
return []string{}
|
||||
}
|
||||
if len(gcls.Members()) == 0 {
|
||||
return cls.ClientURLs()
|
||||
}
|
||||
cls = gcls
|
||||
|
||||
urls := struct{ PeerURLs []string }{cls.PeerURLs()}
|
||||
b, err := json.Marshal(urls)
|
||||
if err != nil {
|
||||
log.Printf("proxy: error on marshal peer urls %s", err)
|
||||
return cls.ClientURLs()
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(clusterfile+".bak", b, 0600)
|
||||
if err != nil {
|
||||
log.Printf("proxy: error on writing urls %s", err)
|
||||
return cls.ClientURLs()
|
||||
}
|
||||
err = os.Rename(clusterfile+".bak", clusterfile)
|
||||
if err != nil {
|
||||
log.Printf("proxy: error on updating clusterfile %s", err)
|
||||
return cls.ClientURLs()
|
||||
}
|
||||
if !reflect.DeepEqual(cls.PeerURLs(), peerURLs) {
|
||||
log.Printf("proxy: updated peer urls in cluster file from %v to %v", peerURLs, cls.PeerURLs())
|
||||
}
|
||||
peerURLs = cls.PeerURLs()
|
||||
|
||||
return cls.ClientURLs()
|
||||
}
|
||||
ph := proxy.NewHandler(pt, uf)
|
||||
ph = &cors.CORSHandler{
|
||||
Handler: ph,
|
||||
Info: corsInfo,
|
||||
Info: cfg.corsInfo,
|
||||
}
|
||||
|
||||
if string(*proxyFlag) == flags.ProxyValueReadonly {
|
||||
if cfg.isReadonlyProxy() {
|
||||
ph = proxy.NewReadonlyHandler(ph)
|
||||
}
|
||||
|
||||
lcurls, err := flags.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)
|
||||
for _, u := range cfg.lcurls {
|
||||
l, err := transport.NewListener(u.Host, u.Scheme, cfg.clientTLSInfo)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
host := u.Host
|
||||
go func() {
|
||||
log.Print("etcd: proxy listening for client requests on ", host)
|
||||
log.Print("proxy: listening for client requests on ", host)
|
||||
log.Fatal(http.Serve(l, ph))
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 := flags.URLsFromFlags(fs, "initial-advertise-peer-urls", "addr", peerTLSInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// setupCluster sets up an initial cluster definition for bootstrap or discovery.
|
||||
func setupCluster(cfg *config) (*etcdserver.Cluster, error) {
|
||||
var cls *etcdserver.Cluster
|
||||
var err error
|
||||
switch {
|
||||
case set["discovery"]:
|
||||
clusterStr := genClusterString(*name, apurls)
|
||||
cls, err = etcdserver.NewClusterFromString(*durl, clusterStr)
|
||||
case set["initial-cluster"]:
|
||||
fallthrough
|
||||
case cfg.durl != "":
|
||||
// If using discovery, generate a temporary cluster based on
|
||||
// self's advertised peer URLs
|
||||
clusterStr := genClusterString(cfg.name, cfg.apurls)
|
||||
cls, err = etcdserver.NewClusterFromString(cfg.durl, clusterStr)
|
||||
case cfg.dnsCluster != "":
|
||||
clusterStr, clusterToken, err := discovery.SRVGetCluster(cfg.name, cfg.dnsCluster, cfg.initialClusterToken, cfg.apurls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cls, err = etcdserver.NewClusterFromString(clusterToken, clusterStr)
|
||||
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(*initialClusterToken, *initialCluster)
|
||||
cls, err = etcdserver.NewClusterFromString(cfg.initialClusterToken, cfg.initialCluster)
|
||||
}
|
||||
return cls, err
|
||||
}
|
||||
|
@ -1,25 +1,23 @@
|
||||
/*
|
||||
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 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdmain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
)
|
||||
|
||||
func TestGenClusterString(t *testing.T) {
|
||||
@ -38,10 +36,7 @@ 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)
|
||||
}
|
||||
urls := testutil.MustNewURLs(t, tt.urls)
|
||||
str := genClusterString(tt.token, urls)
|
||||
if str != tt.wstr {
|
||||
t.Errorf("#%d: cluster = %s, want %s", i, str, tt.wstr)
|
||||
|
100
etcdmain/help.go
Normal file
100
etcdmain/help.go
Normal file
@ -0,0 +1,100 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdmain
|
||||
|
||||
var (
|
||||
usageline = `usage: etcd [flags]
|
||||
start an etcd server
|
||||
|
||||
etcd --version
|
||||
show the version of etcd
|
||||
|
||||
etcd -h | --help
|
||||
show the help information about etcd
|
||||
`
|
||||
flagsline = `
|
||||
member flags:
|
||||
|
||||
--name 'default'
|
||||
human-readable name for this member.
|
||||
--data-dir '${name}.etcd'
|
||||
path to the data directory.
|
||||
--snapshot-count '10000'
|
||||
number of committed transactions to trigger a snapshot to disk.
|
||||
--heartbeat-interval '100'
|
||||
time (in milliseconds) of a heartbeat interval.
|
||||
--election-timeout '1000'
|
||||
time (in milliseconds) for an election to timeout.
|
||||
--listen-peer-urls 'http://localhost:2380,http://localhost:7001'
|
||||
list of URLs to listen on for peer traffic.
|
||||
--listen-client-urls 'http://localhost:2379,http://localhost:4001'
|
||||
list of URLs to listen on for client traffic.
|
||||
-cors ''
|
||||
comma-separated whitelist of origins for CORS (cross-origin resource sharing).
|
||||
|
||||
|
||||
clustering flags:
|
||||
|
||||
--initial-advertise-peer-urls 'http://localhost:2380,http://localhost:7001'
|
||||
list of this member's peer URLs to advertise to the rest of the cluster.
|
||||
--initial-cluster 'default=http://localhost:2380,default=http://localhost:7001'
|
||||
initial cluster configuration for bootstrapping.
|
||||
--initial-cluster-state 'new'
|
||||
initial cluster state ('new' or 'existing').
|
||||
--initial-cluster-token 'etcd-cluster'
|
||||
initial cluster token for the etcd cluster during bootstrap.
|
||||
--advertise-client-urls 'http://localhost:2379,http://localhost:4001'
|
||||
list of this member's client URLs to advertise to the rest of the cluster.
|
||||
--discovery ''
|
||||
discovery URL used to bootstrap the cluster.
|
||||
--discovery-fallback 'proxy'
|
||||
expected behavior ('exit' or 'proxy') when discovery services fails.
|
||||
--discovery-proxy ''
|
||||
HTTP proxy to use for traffic to discovery service.
|
||||
--discovery-srv ''
|
||||
dns srv domain used to bootstrap the cluster.
|
||||
|
||||
|
||||
proxy flags:
|
||||
|
||||
--proxy 'off'
|
||||
proxy mode setting ('off', 'readonly' or 'on').
|
||||
|
||||
|
||||
security flags:
|
||||
|
||||
--ca-file ''
|
||||
path to the client server TLS CA file.
|
||||
--cert-file ''
|
||||
path to the client server TLS cert file.
|
||||
--key-file ''
|
||||
path to the client server TLS key file.
|
||||
--peer-ca-file ''
|
||||
path to the peer server TLS CA file.
|
||||
--peer-cert-file ''
|
||||
path to the peer server TLS cert file.
|
||||
--peer-key-file ''
|
||||
path to the peer server TLS key file.
|
||||
|
||||
|
||||
unsafe flags:
|
||||
|
||||
Please be CAUTIOUS to use unsafe flags because it will break the guarantee given
|
||||
by consensus protocol.
|
||||
|
||||
--force-new-cluster 'false'
|
||||
force to create a new one-member cluster.
|
||||
`
|
||||
)
|
37
etcdmain/http.go
Normal file
37
etcdmain/http.go
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdmain
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// serveHTTP accepts incoming HTTP connections on the listener l,
|
||||
// creating a new service goroutine for each. The service goroutines
|
||||
// read requests and then call handler to reply to them.
|
||||
func serveHTTP(l net.Listener, handler http.Handler, readTimeout time.Duration) error {
|
||||
logger := log.New(ioutil.Discard, "etcdhttp", 0)
|
||||
// TODO: add debug flag; enable logging when debug flag is set
|
||||
srv := &http.Server{
|
||||
Handler: handler,
|
||||
ReadTimeout: readTimeout,
|
||||
ErrorLog: logger, // do not log user error
|
||||
}
|
||||
return srv.Serve(l)
|
||||
}
|
@ -1,18 +1,16 @@
|
||||
/*
|
||||
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 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
@ -24,13 +22,14 @@ import (
|
||||
"log"
|
||||
"net/url"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/pkg/flags"
|
||||
"github.com/coreos/etcd/pkg/netutil"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/raft/raftpb"
|
||||
"github.com/coreos/etcd/store"
|
||||
)
|
||||
|
||||
@ -40,11 +39,19 @@ const (
|
||||
)
|
||||
|
||||
type ClusterInfo interface {
|
||||
// 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 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
|
||||
@ -56,10 +63,12 @@ type Cluster struct {
|
||||
// removed id cannot be reused.
|
||||
removed map[types.ID]bool
|
||||
store store.Store
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// NewClusterFromString returns Cluster through given cluster token 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(token string, cluster string) (*Cluster, error) {
|
||||
c := newCluster(token)
|
||||
@ -89,33 +98,7 @@ func NewClusterFromString(token string, cluster string) (*Cluster, error) {
|
||||
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[mustParseMemberIDFromKey(n.Key)] = true
|
||||
}
|
||||
|
||||
c.members, c.removed = membersFromStore(c.store)
|
||||
return c
|
||||
}
|
||||
|
||||
@ -136,43 +119,45 @@ func newCluster(token string) *Cluster {
|
||||
}
|
||||
}
|
||||
|
||||
func (c Cluster) ID() types.ID { 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)
|
||||
}
|
||||
|
||||
type SortableMemberSlice []*Member
|
||||
|
||||
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 types.ID) *Member {
|
||||
return c.members[id]
|
||||
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() []types.ID {
|
||||
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)
|
||||
@ -182,27 +167,31 @@ func (c Cluster) MemberIDs() []types.ID {
|
||||
}
|
||||
|
||||
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 {
|
||||
endpoints := make([]string, 0)
|
||||
// PeerURLs returns a list of all peer addresses.
|
||||
// The returned list is sorted in ascending lexicographical order.
|
||||
func (c *Cluster) PeerURLs() []string {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
urls := make([]string, 0)
|
||||
for _, p := range c.members {
|
||||
for _, addr := range p.PeerURLs {
|
||||
endpoints = append(endpoints, addr)
|
||||
urls = append(urls, addr)
|
||||
}
|
||||
}
|
||||
sort.Strings(endpoints)
|
||||
return endpoints
|
||||
sort.Strings(urls)
|
||||
return urls
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// ClientURLs returns a list of all client addresses.
|
||||
// The returned list is sorted in ascending lexicographical order.
|
||||
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 +202,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 {
|
||||
@ -224,33 +215,6 @@ func (c Cluster) String() string {
|
||||
return strings.Join(sl, ",")
|
||||
}
|
||||
|
||||
// ValidateAndAssignIDs validates the given members by matching their PeerURLs
|
||||
// with the existing members in the cluster. If the validation succeeds, it
|
||||
// assigns the IDs from the given members to the existing members in the
|
||||
// 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("member count is unequal")
|
||||
}
|
||||
omembs := make([]*Member, 0)
|
||||
for _, m := range c.members {
|
||||
omembs = append(omembs, m)
|
||||
}
|
||||
sort.Sort(SortableMemberSliceByPeerURLs(omembs))
|
||||
sort.Sort(SortableMemberSliceByPeerURLs(membs))
|
||||
for i := range omembs {
|
||||
if !reflect.DeepEqual(omembs[i].PeerURLs, membs[i].PeerURLs) {
|
||||
return fmt.Errorf("unmatched member while checking PeerURLs")
|
||||
}
|
||||
omembs[i].ID = membs[i].ID
|
||||
}
|
||||
c.members = make(map[types.ID]*Member)
|
||||
for _, m := range omembs {
|
||||
c.members[m.ID] = m
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cluster) genID() {
|
||||
mIDs := c.MemberIDs()
|
||||
b := make([]byte, 8*len(mIDs))
|
||||
@ -265,24 +229,83 @@ 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
|
||||
}
|
||||
@ -290,39 +313,93 @@ func (c *Cluster) AddMember(m *Member) {
|
||||
// RemoveMember removes a member from the store.
|
||||
// The given id MUST exist, or the function panics.
|
||||
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
|
||||
}
|
||||
|
||||
// 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: mustParseMemberIDFromKey(n.Key)}
|
||||
if len(n.Nodes) != 2 {
|
||||
return m, fmt.Errorf("len(nodes) = %d, want 2", len(n.Nodes))
|
||||
}
|
||||
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 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)
|
||||
}
|
||||
return m, nil
|
||||
func (c *Cluster) UpdateAttributes(id types.ID, attr Attributes) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.members[id].Attributes = attr
|
||||
// TODO: update store in this function
|
||||
}
|
||||
|
||||
func isKeyNotFound(err error) bool {
|
||||
e, ok := err.(*etcdErr.Error)
|
||||
return ok && e.ErrorCode == etcdErr.EcodeKeyNotFound
|
||||
func (c *Cluster) UpdateRaftAttributes(id types.ID, raftAttr RaftAttributes) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
b, err := json.Marshal(raftAttr)
|
||||
if err != nil {
|
||||
log.Panicf("marshal raftAttributes should never fail: %v", err)
|
||||
}
|
||||
p := path.Join(memberStoreKey(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[id].RaftAttributes = raftAttr
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ValidateClusterAndAssignIDs validates the local cluster by matching the PeerURLs
|
||||
// with the existing cluster. If the validation succeeds, it assigns the IDs
|
||||
// from the existing cluster to the local cluster.
|
||||
// If the validation fails, an error will be returned.
|
||||
func ValidateClusterAndAssignIDs(local *Cluster, existing *Cluster) error {
|
||||
ems := existing.Members()
|
||||
lms := local.Members()
|
||||
if len(ems) != len(lms) {
|
||||
return fmt.Errorf("member count is unequal")
|
||||
}
|
||||
sort.Sort(SortableMemberSliceByPeerURLs(ems))
|
||||
sort.Sort(SortableMemberSliceByPeerURLs(lms))
|
||||
|
||||
for i := range ems {
|
||||
// TODO: Remove URLStringsEqual after improvement of using hostnames #2150 #2123
|
||||
if !netutil.URLStringsEqual(ems[i].PeerURLs, lms[i].PeerURLs) {
|
||||
return fmt.Errorf("unmatched member while checking PeerURLs")
|
||||
}
|
||||
lms[i].ID = ems[i].ID
|
||||
}
|
||||
local.members = make(map[types.ID]*Member)
|
||||
for _, m := range lms {
|
||||
local.members[m.ID] = m
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -1,43 +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 (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClusterStateSet(t *testing.T) {
|
||||
tests := []struct {
|
||||
val string
|
||||
pass bool
|
||||
}{
|
||||
// known values
|
||||
{"new", true},
|
||||
|
||||
// unrecognized values
|
||||
{"foo", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
pf := new(ClusterState)
|
||||
err := pf.Set(tt.val)
|
||||
if tt.pass != (err == nil) {
|
||||
t.Errorf("#%d: want pass=%t, but got err=%v", i, tt.pass, err)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,40 +1,42 @@
|
||||
/*
|
||||
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 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
"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),
|
||||
},
|
||||
},
|
||||
@ -47,9 +49,8 @@ func TestClusterFromString(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -77,41 +78,39 @@ 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)
|
||||
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),
|
||||
}
|
||||
@ -136,7 +135,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),
|
||||
}
|
||||
@ -161,7 +160,7 @@ 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),
|
||||
@ -175,12 +174,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"},
|
||||
@ -188,7 +187,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"},
|
||||
@ -196,7 +195,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),
|
||||
@ -206,13 +205,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{},
|
||||
@ -230,12 +229,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"},
|
||||
@ -243,7 +242,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"},
|
||||
@ -251,7 +250,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"}),
|
||||
@ -261,13 +260,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{},
|
||||
@ -285,40 +284,41 @@ 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),
|
||||
},
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
cl := newTestCluster(tt.clmembs)
|
||||
if err := cl.ValidateAndAssignIDs(tt.membs); err == nil {
|
||||
ecl := newTestCluster(tt.clmembs)
|
||||
lcl := newTestCluster(tt.membs)
|
||||
if err := ValidateClusterAndAssignIDs(lcl, ecl); err == nil {
|
||||
t.Errorf("#%d: unexpected update success", i)
|
||||
}
|
||||
}
|
||||
@ -326,35 +326,158 @@ func TestClusterValidateAndAssignIDsBad(t *testing.T) {
|
||||
|
||||
func TestClusterValidateAndAssignIDs(t *testing.T) {
|
||||
tests := []struct {
|
||||
clmembs []Member
|
||||
clmembs []*Member
|
||||
membs []*Member
|
||||
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),
|
||||
},
|
||||
[]types.ID{3, 4},
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
cl := newTestCluster(tt.clmembs)
|
||||
if err := cl.ValidateAndAssignIDs(tt.membs); err != nil {
|
||||
lcl := newTestCluster(tt.clmembs)
|
||||
ecl := newTestCluster(tt.membs)
|
||||
if err := ValidateClusterAndAssignIDs(lcl, ecl); err != nil {
|
||||
t.Errorf("#%d: unexpect update error: %v", i, err)
|
||||
}
|
||||
if !reflect.DeepEqual(cl.MemberIDs(), tt.wids) {
|
||||
t.Errorf("#%d: ids = %v, want %v", i, cl.MemberIDs(), tt.wids)
|
||||
if !reflect.DeepEqual(lcl.MemberIDs(), tt.wids) {
|
||||
t.Errorf("#%d: ids = %v, want %v", i, lcl.MemberIDs(), tt.wids)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
})
|
||||
@ -366,7 +489,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)
|
||||
@ -379,22 +502,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"},
|
||||
}},
|
||||
}
|
||||
@ -409,12 +532,12 @@ 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{
|
||||
wactions := []testutil.Action{
|
||||
{
|
||||
name: "Create",
|
||||
params: []interface{}{
|
||||
Name: "Create",
|
||||
Params: []interface{}{
|
||||
path.Join(storeMembersPrefix, "1", "raftAttributes"),
|
||||
false,
|
||||
`{"peerURLs":null}`,
|
||||
@ -422,16 +545,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)
|
||||
@ -463,32 +576,32 @@ func TestClusterMembers(t *testing.T) {
|
||||
func TestClusterString(t *testing.T) {
|
||||
cls := &Cluster{
|
||||
members: map[types.ID]*Member{
|
||||
1: newTestMemberp(
|
||||
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",
|
||||
@ -509,9 +622,9 @@ func TestClusterRemoveMember(t *testing.T) {
|
||||
c.SetStore(st)
|
||||
c.RemoveMember(1)
|
||||
|
||||
wactions := []action{
|
||||
{name: "Delete", params: []interface{}{memberStoreKey(1), true, true}},
|
||||
{name: "Create", params: []interface{}{removedMemberStoreKey(1), false, "", false, store.Permanent}},
|
||||
wactions := []testutil.Action{
|
||||
{Name: "Delete", Params: []interface{}{memberStoreKey(1), true, true}},
|
||||
{Name: "Create", Params: []interface{}{removedMemberStoreKey(1), false, "", false, store.Permanent}},
|
||||
}
|
||||
if !reflect.DeepEqual(st.Action(), wactions) {
|
||||
t.Errorf("actions = %v, want %v", st.Action(), wactions)
|
||||
@ -533,23 +646,12 @@ func TestNodeToMember(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func newTestCluster(membs []Member) *Cluster {
|
||||
func newTestCluster(membs []*Member) *Cluster {
|
||||
c := &Cluster{members: make(map[types.ID]*Member), removed: make(map[types.ID]bool)}
|
||||
for i, m := range membs {
|
||||
c.members[m.ID] = &membs[i]
|
||||
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: types.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
|
||||
}
|
||||
func stringp(s string) *string { return &s }
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user