Compare commits
1011 Commits
Author | SHA1 | Date | |
---|---|---|---|
66722b1ada | |||
963339d265 | |||
c87594f27c | |||
e72ad5dd2a | |||
3eb5d24cab | |||
8b9041a938 | |||
864ffec88c | |||
12bc2bba36 | |||
3a43afce5a | |||
0e56ea37e7 | |||
743192aa3b | |||
e8b156578f | |||
61f3338ce7 | |||
effffdbdca | |||
9bac803bee | |||
9169ad0d7d | |||
482a7839d9 | |||
ba3058ca79 | |||
0e90e504f5 | |||
998fa0de76 | |||
c273735729 | |||
c85f736522 | |||
a375ff172e | |||
1893af9bbd | |||
b4c655677a | |||
c2160adf1d | |||
5ada311416 | |||
f042cd7d9c | |||
f0a400a3a8 | |||
6066977280 | |||
fc88eccc74 | |||
5cb28a7d83 | |||
de57e88643 | |||
967fc70173 | |||
4a8d32eaa6 | |||
643c2a310d | |||
c3a191b38d | |||
83efd2c745 | |||
307331cc31 | |||
2abd22a13b | |||
2a4db4307f | |||
ebd6e8c4b1 | |||
8c1ab62bc5 | |||
8d2b340629 | |||
0b449a24bb | |||
a1804390b1 | |||
8b290c680a | |||
c1c9a2c96c | |||
f75e333264 | |||
378bac79e1 | |||
20a747ea09 | |||
4cd5e7ebb2 | |||
881903b6d3 | |||
939912c425 | |||
cbd3807b30 | |||
10b1ba7886 | |||
2f1467cb27 | |||
bd680c3302 | |||
fd7de051a4 | |||
9d7ed0e63a | |||
b82ef007f5 | |||
29bbcdd110 | |||
0afc51c762 | |||
4a8fbb9d5d | |||
d690634bd6 | |||
62b44a85f8 | |||
e7d705b25f | |||
e1640cc72f | |||
a6a1eb8378 | |||
33c375dc44 | |||
1f2dcbb935 | |||
c6cf88ef7f | |||
4e84bd2e3c | |||
c09f0ca9d4 | |||
218ee40f11 | |||
32c252f003 | |||
f4641accc3 | |||
b7cda38653 | |||
5bd9b9614f | |||
201fd70afc | |||
1763f7d4d1 | |||
271785cd55 | |||
8f0d4092c3 | |||
c6219a209d | |||
22db11f876 | |||
d826f95c77 | |||
b6e4858a25 | |||
6526097bfc | |||
3e7feb4033 | |||
fba225cee5 | |||
95078c296d | |||
e15020055e | |||
74fd7709ad | |||
31e3899663 | |||
8516d8ccc5 | |||
6ce9aed8c5 | |||
7a1739a3e8 | |||
5b4677b7d7 | |||
b9f5a00b13 | |||
90893735cf | |||
2e3d27e910 | |||
f337754e72 | |||
aa58aff18c | |||
0bcab05465 | |||
71d7c85b6b | |||
16e92d1379 | |||
8468b38631 | |||
7a65cb5847 | |||
f6cd4d4f5b | |||
63c7e9f840 | |||
f63eb2f6a4 | |||
3505c254e1 | |||
386374a6d0 | |||
066062a5e0 | |||
00da3ca725 | |||
713e006bc6 | |||
fd01db9e60 | |||
b44bd6d2a9 | |||
47f5b7c3ad | |||
87d99fe038 | |||
dfdaf082c5 | |||
8b7b7222dd | |||
a53a9e167e | |||
b8875515a4 | |||
01a985eda5 | |||
010ffc0692 | |||
8c9f01ef53 | |||
aa85b0cea7 | |||
aac2292ab5 | |||
3a2e7653f2 | |||
c232814003 | |||
2655540481 | |||
25eef5a6e4 | |||
7d21d6c894 | |||
af7d051019 | |||
90af2ff302 | |||
230106dd3c | |||
8b081ce9b3 | |||
07ad18178d | |||
db6f45e939 | |||
1f8de1aab0 | |||
f7f30f2361 | |||
9451fa1f9c | |||
c3b96f8a69 | |||
60dbad5a85 | |||
505bf8c708 | |||
2e32d2142d | |||
282c6fd17d | |||
c2959c998f | |||
e9a63473a0 | |||
7f05e220a4 | |||
4edbae4a91 | |||
3b251b0ed3 | |||
4203320d04 | |||
feb930e357 | |||
e4e057f8f7 | |||
9fee35b02d | |||
f6d0dda187 | |||
8f40517adb | |||
61c5a0c6ae | |||
85fa594265 | |||
c2d6a92b01 | |||
24e85b2454 | |||
27b3bf230b | |||
de2e959b27 | |||
31d5d610fc | |||
e33b10a666 | |||
61abf25859 | |||
43e5f892f6 | |||
5533c3058a | |||
72d2adca62 | |||
01b6cdf13d | |||
24f0423088 | |||
3d504737e4 | |||
bb42ba5f4e | |||
6dd8fb6f24 | |||
fdf445b5a0 | |||
f065d8e258 | |||
b0e9d24fb6 | |||
b1720b779c | |||
6c1ce697a6 | |||
3f1f5e5215 | |||
b8f08d400d | |||
066f9bf7e3 | |||
f0ca65a95d | |||
7e6d876385 | |||
7239249155 | |||
cfeab9324e | |||
77fd369b1c | |||
cbd7ef4ee6 | |||
747993de08 | |||
96d6f05391 | |||
22943e7e06 | |||
d818ef2c76 | |||
4e21f87e3d | |||
52613b262b | |||
c309d745a6 | |||
2a3229c00a | |||
3e7bd47cd5 | |||
2059c8e9e7 | |||
b77de97136 | |||
633a0a847b | |||
f674a1b583 | |||
7cb860a31b | |||
d2e69b339f | |||
41e77c9db6 | |||
50f29bd661 | |||
6486be673b | |||
4959663f90 | |||
c49a87bd04 | |||
60b9adc267 | |||
3ce31acda4 | |||
96aaeee4f5 | |||
a9e04061b1 | |||
77fbe10dfc | |||
debc69e1f2 | |||
72fb756af3 | |||
d57ad8ec8d | |||
fa85445ef8 | |||
327f09fcb4 | |||
2af1605db3 | |||
91f6aee4f2 | |||
b94b8b5707 | |||
fbbc4a4979 | |||
2fd6df922a | |||
cb8524fbec | |||
78afc853f4 | |||
b5384ac1c0 | |||
70f0bbe38c | |||
f3053265ae | |||
f224d74ed7 | |||
d5f414f69b | |||
f254e38385 | |||
2ef3eac5ca | |||
76fb6ebcbb | |||
978cf804ca | |||
6f06e1cb47 | |||
c5d4f3e7db | |||
7f159b6a8d | |||
ca4acceb1e | |||
94f6a11bbf | |||
c1300c81b3 | |||
e6a789d541 | |||
2bb33181b6 | |||
7da451640f | |||
4ab818a856 | |||
ec470944f8 | |||
fe1ce3a2f0 | |||
91039bef7c | |||
a73950545a | |||
14d6ed9e5f | |||
a9087ee659 | |||
bf987185a9 | |||
07c07cea25 | |||
0c8988aa07 | |||
8309ca92d7 | |||
fb6287240f | |||
85e87e8f6b | |||
8bad78cb98 | |||
bfd5f38af3 | |||
3a93928b07 | |||
82b7e4fd3b | |||
da1bba8f39 | |||
633a4e6b52 | |||
cf8c66c9f0 | |||
85c9ea92bb | |||
a2b5444a26 | |||
393e4335b7 | |||
fd11523af9 | |||
04fc57ac1d | |||
385e18bc6c | |||
35dff4cbc3 | |||
d24a763a12 | |||
fcd4871e2a | |||
d3456b5ecd | |||
3d8e2e1171 | |||
c654370d6d | |||
e1306bff8f | |||
ba299bcaaf | |||
8fa4b8da6e | |||
cb408ace21 | |||
05582ad5b2 | |||
30552e28ed | |||
f10a70401b | |||
94044cee4f | |||
5161b74799 | |||
dd0d590217 | |||
2511535ea0 | |||
714b48a4b4 | |||
8fdf8f752b | |||
6dd807481c | |||
45406d8486 | |||
4fcea334ad | |||
8aaa1ed911 | |||
99a2d6c4b1 | |||
cbe37e5213 | |||
e7e7451213 | |||
e771c6042b | |||
c011e2ddd5 | |||
81291b23b1 | |||
c798f81398 | |||
8a5f085a65 | |||
cb979bc2cc | |||
253e5a90bb | |||
ac69e63fa8 | |||
5000d29b4a | |||
8ffd58fb3b | |||
cd470f9ccd | |||
472a536052 | |||
c407e097e2 | |||
ea5f6dab6b | |||
0d52598fc1 | |||
cf8ab8c7a6 | |||
6b030ed7db | |||
6ad9d1609a | |||
f92c11e1f2 | |||
f0143916de | |||
7e3dd74314 | |||
0e7fd4a37c | |||
e2d0db95eb | |||
2951e7f6e4 | |||
fdf7798137 | |||
8efc42e25f | |||
cfbc5e5c3b | |||
04354f32ab | |||
957c9cd1df | |||
8fdfac2843 | |||
1153e1e7d9 | |||
7607ace95a | |||
6c2fb5105d | |||
56b111df0c | |||
8ce579aac9 | |||
9eb3e2c6b4 | |||
0b19921ec0 | |||
537c7100b0 | |||
2dd361aba5 | |||
8077be93b8 | |||
b9f9d2e786 | |||
67f2e41f20 | |||
4582a7e900 | |||
46971fa1db | |||
9b8e39e7ca | |||
e58d39611a | |||
780a7d359c | |||
33a0496b5e | |||
d9ec6b4d22 | |||
68837b9693 | |||
2046d66927 | |||
2d97500e64 | |||
373a04a181 | |||
70a9929b5d | |||
cad1215b18 | |||
18bccb4285 | |||
712f6cb0e1 | |||
817825d549 | |||
79d27328e3 | |||
95c6c4b713 | |||
4f9aa276bd | |||
6ebadda395 | |||
e521a9116f | |||
7684bfdf65 | |||
ce2f65508d | |||
b4869cb03e | |||
216a6347b2 | |||
fd5766bdf6 | |||
7fb1f68ff8 | |||
a0dc471520 | |||
4d1b8b1e47 | |||
7da79de74b | |||
b694cfc69f | |||
d26bdbaf81 | |||
8db8d01712 | |||
2030c85071 | |||
93594006df | |||
78a5eb79b5 | |||
b5dd41e625 | |||
a1a72202ff | |||
2a074523a4 | |||
25acdbf41b | |||
55e2355326 | |||
5f366db7d1 | |||
78422eaa17 | |||
bf047ed9d5 | |||
dc8115a534 | |||
9ba69ff317 | |||
135a40751e | |||
4b4f5be74a | |||
80c1b9c13a | |||
d1ae4cd5bd | |||
4b5bb7f212 | |||
a6cab69c88 | |||
2769cae6bd | |||
c0560be98a | |||
9ba435d902 | |||
63bb560820 | |||
88d4e7ebeb | |||
7e05b33aa0 | |||
d31701bab5 | |||
25ed908c18 | |||
369d561350 | |||
7c5991c2e6 | |||
bea4c62965 | |||
2bc1dfd921 | |||
e1cf766695 | |||
7388911e0c | |||
aab2eda7df | |||
408de4124b | |||
dee467dc24 | |||
83577a5d08 | |||
d42c1f5131 | |||
43f795a485 | |||
4f27981c46 | |||
c7bdd7e2c5 | |||
c4a45c5713 | |||
42d56d5ef7 | |||
d51d381eca | |||
63355062dc | |||
f7c99208b5 | |||
c0fc389c98 | |||
31c1931b7b | |||
6978471712 | |||
3edd36315d | |||
23e952ccfd | |||
1e3274dfa2 | |||
8a7a548a6d | |||
d9069120bb | |||
972d8c55ab | |||
9bc3c0bd05 | |||
7f2d6b3ef6 | |||
7adf4d7c94 | |||
a204b14503 | |||
0a7fc7cd34 | |||
fd5984af56 | |||
d6efc0b22b | |||
f67bdc2eed | |||
63c6824905 | |||
7dbc4549d9 | |||
a0149106b8 | |||
8963cf2f8b | |||
e56e43064f | |||
f13bea0bb0 | |||
ea06ea41e5 | |||
24e4c94d98 | |||
8dafaf390a | |||
8d07200bbf | |||
38a9149735 | |||
d204b6c3b7 | |||
f5f4791023 | |||
8ad935ef2c | |||
5aebe1a52d | |||
62d7bae496 | |||
e6b685b1ed | |||
512bac0ee9 | |||
8024a0d15f | |||
7db7744737 | |||
833769f59f | |||
b55ea6a70b | |||
9ca7f22e84 | |||
0472b2dc9f | |||
d0d4b1378b | |||
ca22c4c384 | |||
ef3bd4ecc5 | |||
809e6110a0 | |||
1ff0b71b30 | |||
a0c97282c3 | |||
dae2755253 | |||
36735d52a4 | |||
eafab47f05 | |||
faad828c51 | |||
6b784908ad | |||
c90a4b96d1 | |||
0bf110e27f | |||
a915ff8419 | |||
5c642ae314 | |||
123b25845c | |||
65ad91b14d | |||
60d3375599 | |||
a4ab5e55f9 | |||
4c7ffe4442 | |||
fded83f111 | |||
e70c8ac4a2 | |||
caa73c176f | |||
5dea73860f | |||
e6f72b4f42 | |||
9381b103bb | |||
2e1e1c95bd | |||
da6a035afb | |||
997e83f8ea | |||
631f790689 | |||
b2a465e354 | |||
b9cfa4cef9 | |||
a26964c855 | |||
f18ae033a7 | |||
608a2be9c5 | |||
f763048156 | |||
54efb460af | |||
ab1cf751a3 | |||
ad2111a6f4 | |||
e9bfcc02ce | |||
b81cb999fb | |||
204335d304 | |||
54928f5deb | |||
3f8eab8439 | |||
21217c30f9 | |||
37bdc94860 | |||
36ece32a61 | |||
0256953b28 | |||
8afc468b64 | |||
161c7f6bdf | |||
23719f99c6 | |||
7ef75e373a | |||
9dcb975724 | |||
ed68bf89ff | |||
26abd25cd3 | |||
e7a0c9128a | |||
8d0d942c47 | |||
c40b86bcde | |||
0c87467f69 | |||
068d806bde | |||
25e3ce1feb | |||
a39107a3b8 | |||
049ca8746a | |||
85f989ab3d | |||
397a42efbe | |||
f35d7d9608 | |||
66d147766f | |||
facbb64090 | |||
e0de6536c8 | |||
1f8c7b33e7 | |||
f9b6066dd6 | |||
da10d5d057 | |||
9f34d3493d | |||
1a75165ed8 | |||
dd465d0e40 | |||
ff6d6867b0 | |||
5cda22a17d | |||
9e034f4b4b | |||
22c52b6d2e | |||
d1a9ccb2b9 | |||
6511171725 | |||
e127214c6c | |||
327e255695 | |||
7698a2a546 | |||
2d5f890091 | |||
17e2e762b1 | |||
cd70ea33ce | |||
95870a21eb | |||
5594f695bc | |||
b9d91483d0 | |||
004c1388fb | |||
27550b229a | |||
effa6e0767 | |||
aca2abd8fe | |||
3a1368d4d2 | |||
ae7b4ee8ed | |||
53ca03b655 | |||
cfdad38f4e | |||
432c19de61 | |||
fba87558a6 | |||
21ac657e67 | |||
8a3fee15a3 | |||
5e4b008106 | |||
f292a4c953 | |||
5015480e0c | |||
79f4c196b8 | |||
2f1542c06d | |||
1a91ed0e99 | |||
d78b03fb27 | |||
39c733ebe7 | |||
5856c8bce9 | |||
80c10e150f | |||
902c676cdb | |||
a23609efe6 | |||
a2a6b693f1 | |||
dea2516177 | |||
8f83d11724 | |||
27960911af | |||
7a6b61cd6f | |||
df839f3b7f | |||
3e86779ad5 | |||
b36734f1d3 | |||
18a813a9fe | |||
a087325452 | |||
7f0733cf46 | |||
eed4a3f035 | |||
a9588952a0 | |||
ace3a217b0 | |||
276039e835 | |||
01d1a579bc | |||
781196fa87 | |||
e3218e2dd1 | |||
1a6be700d8 | |||
148c923c72 | |||
4409932132 | |||
1b1fabef8f | |||
3a61fe596b | |||
94d5936180 | |||
7b541f9003 | |||
300323fa50 | |||
ad1a790116 | |||
c737bf3d2a | |||
47cd9d0277 | |||
763a37d3f1 | |||
d51c8bb640 | |||
a2cdd908dc | |||
b025cdd097 | |||
90b5f3587d | |||
5193965005 | |||
34fca0caa9 | |||
312ac5824f | |||
d051b3b4e4 | |||
76aa7f6935 | |||
bf0aa68f89 | |||
fbcc6db64c | |||
60bdc47fa0 | |||
38f27599b9 | |||
593489d454 | |||
eb6a47f87e | |||
52bc997e0b | |||
d0d3c768d9 | |||
9c9156b478 | |||
b744cecd20 | |||
0d851e49e3 | |||
debeccd605 | |||
c848ee9d86 | |||
0a692b0524 | |||
911ae60edf | |||
0c38f1ff8d | |||
0a9e2fe1f2 | |||
9afe4e87fd | |||
310641630e | |||
8baaa06cce | |||
5351953425 | |||
01dd60c0f7 | |||
ad1d48b73d | |||
c8ea343a76 | |||
4d69d9663b | |||
095407df58 | |||
f862b47e92 | |||
5f4412996d | |||
ddcf14102e | |||
889dd1b22f | |||
dbf654cf77 | |||
8bc6cea90c | |||
d1dcc828c8 | |||
0ed3c83e49 | |||
58da8b17ee | |||
f0c184b3a2 | |||
33acbb694b | |||
8d438c2939 | |||
39dc5315ed | |||
cd7d68fed0 | |||
e4f40f6554 | |||
beb58c434c | |||
7f94afdb8c | |||
13e36f963d | |||
e016015196 | |||
1bcbd82c8b | |||
7a25257fb2 | |||
9713b1f3ef | |||
234c4b1685 | |||
6f0723f23f | |||
0d48fc5511 | |||
7f43fdde74 | |||
3fa3d7dac6 | |||
320768b2e9 | |||
4a7b27921d | |||
3f515e1849 | |||
43eca30a08 | |||
1a3a345468 | |||
3eb2fdbd99 | |||
df657d4690 | |||
7907936066 | |||
1fc0803840 | |||
382ffe679d | |||
831abf82b1 | |||
ed90481510 | |||
f8a290e7ca | |||
a7a93f54a4 | |||
7b1ccca373 | |||
a4a84184e8 | |||
e5d94a296f | |||
3d75395875 | |||
bd6e6c11f8 | |||
79de3be6a7 | |||
db560574dd | |||
317f3571ff | |||
3f187a103b | |||
c8a2c7f64f | |||
270dc9427b | |||
4e1ce81e17 | |||
4e2fe050f5 | |||
b6eedbacf9 | |||
5039c7b4ab | |||
8a57b90e7f | |||
9ba658f59b | |||
b68416f735 | |||
71937151d0 | |||
4aa68e0231 | |||
b7ee8f4967 | |||
2831b9dcfd | |||
fb81fb44fa | |||
e16db3347a | |||
e52f41a6d1 | |||
bd6f1c9e48 | |||
85c22f4562 | |||
42c98123b3 | |||
ad45958841 | |||
1753623f87 | |||
5da5b834e5 | |||
9cc013fec0 | |||
1e252f1feb | |||
763aef87b9 | |||
6092e1ad24 | |||
ae0c4b4c87 | |||
3296c15a32 | |||
5cdb557560 | |||
db91277216 | |||
2eb8243d94 | |||
134d1cb4e0 | |||
931cf3454a | |||
010cc287bb | |||
28e9ba365a | |||
d111c8fe3b | |||
cf547aa403 | |||
f76ca01aed | |||
d3aebbf0ce | |||
edd298f85a | |||
1f413cff64 | |||
aca4ea2a29 | |||
17ae440991 | |||
324d2383b8 | |||
1a9cd7bf36 | |||
8f744fe46b | |||
633cfbe241 | |||
bbd8f4e6f6 | |||
22f0386683 | |||
c4f1e64de7 | |||
298d58841e | |||
01557ebc8f | |||
c231950cdb | |||
15d8ca7726 | |||
2ec8572a8c | |||
833aa518d8 | |||
9fbdd0a84a | |||
3eaf2f6558 | |||
119d0520c6 | |||
3f756d502b | |||
9d74eb5c60 | |||
86c9bf5c3f | |||
72a531e8b2 | |||
5df56fa615 | |||
df3bb333ca | |||
c3a678be75 | |||
c0c4c7cb76 | |||
f97a077257 | |||
f2e9936de5 | |||
6431382a75 | |||
c90c757a56 | |||
dd16463ad4 | |||
25403970f5 | |||
12d3e4e473 | |||
3c306cdb3e | |||
8b097f279d | |||
0c0fbbd7c5 | |||
3c20bdd004 | |||
86cb9f2490 | |||
29a6fd65ad | |||
56b4e6b71f | |||
2ac44eab81 | |||
6f193ea1df | |||
4e114d3549 | |||
bc6bebe7b0 | |||
9b84127739 | |||
2533c2a50c | |||
f203a61469 | |||
07129a6370 | |||
78fbe669ad | |||
b5be18a744 | |||
51435df179 | |||
4d2aa80ecf | |||
c9452c6ad4 | |||
9342647e0c | |||
a5cf7fdc87 | |||
507bd2ab4b | |||
4fb8d30f0a | |||
5d3597a5f2 | |||
65b59f4423 | |||
ba52bd07ba | |||
05b82f2022 | |||
4274db46f2 | |||
49a12371c1 | |||
4608210154 | |||
80de75431e | |||
2510a1488c | |||
80ab321f9d | |||
1d521556ae | |||
2f8b9ce9aa | |||
a4a8393cb7 | |||
36f5b713bf | |||
49a0a63fc3 | |||
ad1b754e02 | |||
8cb5e05fc9 | |||
67e3fc55d7 | |||
78d153fc5a | |||
2cc273291d | |||
808ee4e57c | |||
3d994f8653 | |||
c200be6432 | |||
e0ddded077 | |||
853f68071b | |||
43740a8d3c | |||
e52a985a3a | |||
fb7dd0f688 | |||
ab03a42f06 | |||
2925f02aac | |||
0d08ffa282 | |||
bcfbb096e2 | |||
2ca1823a96 | |||
c22ba766d5 | |||
9f8e82e1c0 | |||
1fe2a9b124 | |||
47cb8a012a | |||
cc14f14216 | |||
1a4a4fa7ac | |||
98249bc950 | |||
5afa4e4fdf | |||
0914b8b707 | |||
c4fc8c0989 | |||
9b72c8ba1b | |||
366e689eae | |||
0944a50d3f | |||
c182428e52 | |||
bf5ecf6555 | |||
cf5cc18f02 | |||
a213b3abf5 | |||
739accc242 | |||
a9f10bdeee | |||
9976d869c1 | |||
280b65fe4d | |||
6fb99a8585 | |||
4d055ca73b | |||
950a9da9d9 | |||
720234d32b | |||
23b5a29101 | |||
8c43bd06a0 | |||
4203c766fb | |||
01a1dae7ae | |||
d159353d51 | |||
ae5c89ff12 | |||
56c706ff91 | |||
e42fa18ccf | |||
9def4cb9fe | |||
e3f4b43614 | |||
b465b48476 | |||
42e7d4d09d | |||
f74142187d | |||
2656b594bb | |||
5d41e7f09b | |||
beef5eea37 | |||
0df1822212 | |||
46cac6f292 | |||
89bb9048dd | |||
c6e9892af4 | |||
0f53ad0b84 | |||
cd9f0a1721 | |||
0191509637 | |||
7d6280fa82 | |||
c586218ec6 | |||
d2716fc5ae | |||
9767098331 | |||
f127f462c6 | |||
75ae50a90f | |||
5dace5f6dc | |||
19d30fd4a7 | |||
78540c5e7b | |||
3351a71e84 | |||
ae2e8fa462 | |||
18af48a9dc | |||
e3b325c196 | |||
9dbde1cc52 | |||
0c4e67c1f4 | |||
5a67b0aba6 | |||
63572567b4 | |||
54cf0317c3 | |||
b1b78c537c | |||
6838ac3ba5 | |||
072eda508b | |||
fa1cbd5890 | |||
094be295a1 | |||
56286ccd29 | |||
a2c44a8b65 | |||
11619f8db2 | |||
eb42a5cb2f | |||
55c98982d1 | |||
10a401c7c6 | |||
fb7365ef3c | |||
af20ba21cb | |||
6bef2bddca | |||
d7cc9be3fd | |||
2fce80e4c0 | |||
37fb2c454f | |||
84a81d8caf | |||
d3191d1afb | |||
95edd1bc58 | |||
8a87769a09 | |||
5ac4e4255a | |||
508c9dfe5c | |||
a9bf593bdc | |||
90f6a4a28d | |||
ce9f73a34c | |||
a674116f07 | |||
b9bbfda874 | |||
13b70ed545 | |||
0fea49f8fa | |||
02f4a9a034 | |||
ab532524b0 | |||
aace95a5bd | |||
e75f52b97a | |||
6443c25422 | |||
e0f4dd4cca | |||
8b952fb8dc | |||
5aab92414f | |||
861cb5cfa2 | |||
165c77f14e | |||
4374d944d4 | |||
89f7cc51fa | |||
deb11b3594 | |||
555b8047e6 | |||
13420b33a0 | |||
8695511153 | |||
8604d1863b | |||
a81234a25b | |||
59880a0ab8 | |||
7e31ddd32a | |||
dfb2ed07db | |||
92cec10103 | |||
074af101f1 | |||
aa79523d33 | |||
367b064bcd | |||
3196d08c7a | |||
b788790e56 | |||
71c45906ad | |||
a630735c29 | |||
5d8cceb164 | |||
06a27d8590 | |||
1ada4f939f | |||
9d0d4be7d1 | |||
3902d5ab0a | |||
96e0f50673 | |||
82d56b6314 | |||
e446c2c2c7 | |||
e4b8c874d2 | |||
a94d20d1e4 | |||
f80914fba2 | |||
acec15ebc6 | |||
94eec5d41a | |||
1cd3fd81c8 | |||
8ad5e29447 | |||
be28981234 | |||
4fad94246d | |||
8a779ce709 | |||
81c1288d60 | |||
5cf0d6678b | |||
d03d7f0c0d | |||
99639186cd | |||
eb88a5f288 | |||
b2d5c91f0e | |||
90509008e9 | |||
8c0282ab24 | |||
85e14a841a | |||
933bcac6da | |||
293c75b133 | |||
fcaa509e4c | |||
1a962df596 | |||
5c774ff571 | |||
307e14028c | |||
462dbfe10d | |||
abf7847fa5 | |||
69606bb95f | |||
3a40421aa5 | |||
2db9d3b702 | |||
bad2f03cd0 | |||
df55438a60 | |||
b8e9bd2b42 | |||
eba41cd7b3 | |||
017ea3df50 | |||
eb7a804ca8 | |||
b9d3bd8d42 | |||
6f9a20803c | |||
699b1e5b3a | |||
26d99269c0 | |||
783eaf9de6 | |||
9886e9448e | |||
c5a9d54835 | |||
118fd18eb6 | |||
0f8060bede | |||
5dffa38fb2 | |||
e03850c4ac | |||
d94d22122b | |||
a66f133209 | |||
8752ee52a5 | |||
e655420d33 | |||
7f8b5774a4 | |||
8eea93942d | |||
4730bddea7 | |||
fa9a78450c | |||
ea94aea136 | |||
a8cc11375f | |||
3d97da0672 | |||
c624caabb1 |
3
.github/ISSUE_TEMPLATE.md
vendored
3
.github/ISSUE_TEMPLATE.md
vendored
@ -1,7 +1,6 @@
|
||||
# Bug reporting
|
||||
|
||||
A good bug report has some very specific qualities, so please read over our short document on
|
||||
[reporting bugs][report_bugs] before you submit your bug report.
|
||||
A good bug report has some very specific qualities, so please read over our short document on [reporting bugs][report_bugs] before submitting a bug report.
|
||||
|
||||
To ask a question, go ahead and ignore this.
|
||||
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,3 +12,4 @@
|
||||
*.test
|
||||
tools/functional-tester/docker/bin
|
||||
hack/tls-setup/certs
|
||||
.idea
|
||||
|
30
.travis.yml
30
.travis.yml
@ -4,12 +4,18 @@ go_import_path: github.com/coreos/etcd
|
||||
sudo: false
|
||||
|
||||
go:
|
||||
- 1.7.4
|
||||
- 1.8.3
|
||||
- tip
|
||||
|
||||
notifications:
|
||||
on_success: never
|
||||
on_failure: never
|
||||
|
||||
env:
|
||||
matrix:
|
||||
- TARGET=amd64
|
||||
- TARGET=darwin-amd64
|
||||
- TARGET=windows-amd64
|
||||
- TARGET=arm64
|
||||
- TARGET=arm
|
||||
- TARGET=386
|
||||
@ -20,6 +26,10 @@ matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
exclude:
|
||||
- go: tip
|
||||
env: TARGET=darwin-amd64
|
||||
- go: tip
|
||||
env: TARGET=windows-amd64
|
||||
- go: tip
|
||||
env: TARGET=arm
|
||||
- go: tip
|
||||
@ -31,15 +41,21 @@ matrix:
|
||||
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- debian-sid
|
||||
packages:
|
||||
- libpcap-dev
|
||||
- libaspell-dev
|
||||
- libhunspell-dev
|
||||
- shellcheck
|
||||
|
||||
before_install:
|
||||
- go get -v github.com/chzchzchz/goword
|
||||
- go get -v honnef.co/go/simple/cmd/gosimple
|
||||
- go get -v honnef.co/go/unused/cmd/unused
|
||||
- go get -v -u github.com/chzchzchz/goword
|
||||
- go get -v -u github.com/coreos/license-bill-of-materials
|
||||
- go get -v -u honnef.co/go/tools/cmd/gosimple
|
||||
- go get -v -u honnef.co/go/tools/cmd/unused
|
||||
- go get -v -u honnef.co/go/tools/cmd/staticcheck
|
||||
- ./scripts/install-marker.sh amd64
|
||||
|
||||
# disable godep restore override
|
||||
install:
|
||||
@ -51,6 +67,12 @@ script:
|
||||
amd64)
|
||||
GOARCH=amd64 ./test
|
||||
;;
|
||||
darwin-amd64)
|
||||
GO_BUILD_FLAGS="-a -v" GOPATH="" GOOS=darwin GOARCH=amd64 ./build
|
||||
;;
|
||||
windows-amd64)
|
||||
GO_BUILD_FLAGS="-a -v" GOPATH="" GOOS=windows GOARCH=amd64 ./build
|
||||
;;
|
||||
386)
|
||||
GOARCH=386 PASSES="build unit" ./test
|
||||
;;
|
||||
|
@ -1,6 +1,6 @@
|
||||
# How to contribute
|
||||
|
||||
etcd is Apache 2.0 licensed and accepts contributions via GitHub pull requests. This document outlines some of the conventions on commit message formatting, contact points for developers and other resources to make getting your contribution into etcd easier.
|
||||
etcd is Apache 2.0 licensed and accepts contributions via GitHub pull requests. This document outlines some of the conventions on commit message formatting, contact points for developers, and other resources to help get contributions into etcd.
|
||||
|
||||
# Email and chat
|
||||
|
||||
@ -14,24 +14,20 @@ etcd is Apache 2.0 licensed and accepts contributions via GitHub pull requests.
|
||||
|
||||
## Reporting bugs and creating issues
|
||||
|
||||
Reporting bugs is one of the best ways to contribute. However, a good bug report
|
||||
has some very specific qualities, so please read over our short document on
|
||||
[reporting bugs](https://github.com/coreos/etcd/blob/master/Documentation/reporting_bugs.md)
|
||||
before you submit your bug report. This document might contain links known
|
||||
issues, another good reason to take a look there, before reporting your bug.
|
||||
Reporting bugs is one of the best ways to contribute. However, a good bug report has some very specific qualities, so please read over our short document on [reporting bugs](https://github.com/coreos/etcd/blob/master/Documentation/reporting_bugs.md) before submitting a bug report. This document might contain links to known issues, another good reason to take a look there before reporting a bug.
|
||||
|
||||
## Contribution flow
|
||||
|
||||
This is a rough outline of what a contributor's workflow looks like:
|
||||
|
||||
- Create a topic branch from where you want to base your work. This is usually master.
|
||||
- Create a topic branch from where to base the contribution. This is usually master.
|
||||
- Make commits of logical units.
|
||||
- Make sure your commit messages are in the proper format (see below).
|
||||
- Push your changes to a topic branch in your fork of the repository.
|
||||
- Make sure commit messages are in the proper format (see below).
|
||||
- Push changes in a topic branch to a personal fork of the repository.
|
||||
- Submit a pull request to coreos/etcd.
|
||||
- Your PR must receive a LGTM from two maintainers found in the MAINTAINERS file.
|
||||
- The PR must receive a LGTM from two maintainers found in the MAINTAINERS file.
|
||||
|
||||
Thanks for your contributions!
|
||||
Thanks for contributing!
|
||||
|
||||
### Code style
|
||||
|
||||
@ -48,8 +44,7 @@ the body of the commit should describe the why.
|
||||
```
|
||||
scripts: add the test-cluster command
|
||||
|
||||
this uses tmux to setup a test cluster that you can easily kill and
|
||||
start for debugging.
|
||||
this uses tmux to setup a test cluster that can easily be killed and started for debugging.
|
||||
|
||||
Fixes #38
|
||||
```
|
||||
@ -64,7 +59,4 @@ The format can be described more formally as follows:
|
||||
<footer>
|
||||
```
|
||||
|
||||
The first line is the subject and should be no longer than 70 characters, the
|
||||
second line is always blank, and other lines should be wrapped at 80 characters.
|
||||
This allows the message to be easier to read on GitHub as well as in various
|
||||
git tools.
|
||||
The first line is the subject and should be no longer than 70 characters, the second line is always blank, and other lines should be wrapped at 80 characters. This allows the message to be easier to read on GitHub as well as in various git tools.
|
||||
|
@ -5,6 +5,12 @@ ADD etcdctl /usr/local/bin/
|
||||
RUN mkdir -p /var/etcd/
|
||||
RUN mkdir -p /var/lib/etcd/
|
||||
|
||||
# Alpine Linux doesn't use pam, which means that there is no /etc/nsswitch.conf,
|
||||
# but Golang relies on /etc/nsswitch.conf to check the order of DNS resolving
|
||||
# (see https://github.com/golang/go/commit/9dee7771f561cf6aee081c0af6658cc81fac3918)
|
||||
# To fix this we just create /etc/nsswitch.conf and add the following line:
|
||||
RUN echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf
|
||||
|
||||
EXPOSE 2379 2380
|
||||
|
||||
# Define default command.
|
||||
|
11
Dockerfile-release.arm64
Normal file
11
Dockerfile-release.arm64
Normal file
@ -0,0 +1,11 @@
|
||||
FROM aarch64/ubuntu:16.04
|
||||
|
||||
ADD etcd /usr/local/bin/
|
||||
ADD etcdctl /usr/local/bin/
|
||||
ADD var/etcd /var/etcd
|
||||
ADD var/lib/etcd /var/lib/etcd
|
||||
|
||||
EXPOSE 2379 2380
|
||||
|
||||
# Define default command.
|
||||
CMD ["/usr/local/bin/etcd"]
|
11
Dockerfile-release.ppc64le
Normal file
11
Dockerfile-release.ppc64le
Normal file
@ -0,0 +1,11 @@
|
||||
FROM ppc64le/ubuntu:16.04
|
||||
|
||||
ADD etcd /usr/local/bin/
|
||||
ADD etcdctl /usr/local/bin/
|
||||
ADD var/etcd /var/etcd
|
||||
ADD var/lib/etcd /var/lib/etcd
|
||||
|
||||
EXPOSE 2379 2380
|
||||
|
||||
# Define default command.
|
||||
CMD ["/usr/local/bin/etcd"]
|
@ -49,4 +49,4 @@ Bootstrap another machine and use the [hey HTTP benchmark tool][hey] to send req
|
||||
| 256 | 256 | all servers | 3061 | 119.3 |
|
||||
|
||||
[hey]: https://github.com/rakyll/hey
|
||||
[hack-benchmark]: /hack/benchmark/
|
||||
[hack-benchmark]: https://github.com/coreos/etcd/tree/master/hack/benchmark
|
||||
|
@ -69,4 +69,4 @@ Bootstrap another machine and use the [hey HTTP benchmark tool][hey] to send req
|
||||
[hey]: https://github.com/rakyll/hey
|
||||
[c7146bd5]: https://github.com/coreos/etcd/commits/c7146bd5f2c73716091262edc638401bb8229144
|
||||
[etcd-2.1-benchmark]: etcd-2-1-0-alpha-benchmarks.md
|
||||
[hack-benchmark]: /hack/benchmark/
|
||||
[hack-benchmark]: ../../hack/benchmark/
|
||||
|
@ -39,7 +39,7 @@ The length of key name is always 64 bytes, which is a reasonable length of avera
|
||||
## Data Size Threshold
|
||||
|
||||
- When etcd reaches data size threshold, it may trigger leader election easily and drop part of proposals.
|
||||
- At most cases, etcd cluster should work smoothly if it doesn't hit the threshold. If it doesn't work well due to insufficient resources, you need to decrease its data size.
|
||||
- For most cases, the etcd cluster should work smoothly if it doesn't hit the threshold. If it doesn't work well due to insufficient resources, decrease its data size.
|
||||
|
||||
| value bytes | key number limitation | suggested data size threshold(MB) | consumed RSS(MB) |
|
||||
|-------------|-----------------------|-----------------------------------|------------------|
|
||||
|
@ -39,4 +39,4 @@ The performance is nearly the same as the one with empty server handler.
|
||||
The performance with empty server handler is not affected by one put. So the
|
||||
performance downgrade should be caused by storage package.
|
||||
|
||||
[etcd-v3-benchmark]: /tools/benchmark/
|
||||
[etcd-v3-benchmark]: ../../tools/benchmark/
|
||||
|
168
Documentation/dev-guide/api_concurrency_reference_v3.md
Normal file
168
Documentation/dev-guide/api_concurrency_reference_v3.md
Normal file
@ -0,0 +1,168 @@
|
||||
### etcd concurrency API Reference
|
||||
|
||||
|
||||
This is a generated documentation. Please read the proto files for more.
|
||||
|
||||
|
||||
##### service `Lock` (etcdserver/api/v3lock/v3lockpb/v3lock.proto)
|
||||
|
||||
The lock service exposes client-side locking facilities as a gRPC interface.
|
||||
|
||||
| Method | Request Type | Response Type | Description |
|
||||
| ------ | ------------ | ------------- | ----------- |
|
||||
| Lock | LockRequest | LockResponse | Lock acquires a distributed shared lock on a given named lock. On success, it will return a unique key that exists so long as the lock is held by the caller. This key can be used in conjunction with transactions to safely ensure updates to etcd only occur while holding lock ownership. The lock is held until Unlock is called on the key or the lease associate with the owner expires. |
|
||||
| Unlock | UnlockRequest | UnlockResponse | Unlock takes a key returned by Lock and releases the hold on lock. The next Lock caller waiting for the lock will then be woken up and given ownership of the lock. |
|
||||
|
||||
|
||||
|
||||
##### message `LockRequest` (etcdserver/api/v3lock/v3lockpb/v3lock.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| name | name is the identifier for the distributed shared lock to be acquired. | bytes |
|
||||
| lease | lease is the ID of the lease that will be attached to ownership of the lock. If the lease expires or is revoked and currently holds the lock, the lock is automatically released. Calls to Lock with the same lease will be treated as a single acquistion; locking twice with the same lease is a no-op. | int64 |
|
||||
|
||||
|
||||
|
||||
##### message `LockResponse` (etcdserver/api/v3lock/v3lockpb/v3lock.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| header | | etcdserverpb.ResponseHeader |
|
||||
| key | key is a key that will exist on etcd for the duration that the Lock caller owns the lock. Users should not modify this key or the lock may exhibit undefined behavior. | bytes |
|
||||
|
||||
|
||||
|
||||
##### message `UnlockRequest` (etcdserver/api/v3lock/v3lockpb/v3lock.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| key | key is the lock ownership key granted by Lock. | bytes |
|
||||
|
||||
|
||||
|
||||
##### message `UnlockResponse` (etcdserver/api/v3lock/v3lockpb/v3lock.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| header | | etcdserverpb.ResponseHeader |
|
||||
|
||||
|
||||
|
||||
##### service `Election` (etcdserver/api/v3election/v3electionpb/v3election.proto)
|
||||
|
||||
The election service exposes client-side election facilities as a gRPC interface.
|
||||
|
||||
| Method | Request Type | Response Type | Description |
|
||||
| ------ | ------------ | ------------- | ----------- |
|
||||
| Campaign | CampaignRequest | CampaignResponse | Campaign waits to acquire leadership in an election, returning a LeaderKey representing the leadership if successful. The LeaderKey can then be used to issue new values on the election, transactionally guard API requests on leadership still being held, and resign from the election. |
|
||||
| Proclaim | ProclaimRequest | ProclaimResponse | Proclaim updates the leader's posted value with a new value. |
|
||||
| Leader | LeaderRequest | LeaderResponse | Leader returns the current election proclamation, if any. |
|
||||
| Observe | LeaderRequest | LeaderResponse | Observe streams election proclamations in-order as made by the election's elected leaders. |
|
||||
| Resign | ResignRequest | ResignResponse | Resign releases election leadership so other campaigners may acquire leadership on the election. |
|
||||
|
||||
|
||||
|
||||
##### message `CampaignRequest` (etcdserver/api/v3election/v3electionpb/v3election.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| name | name is the election's identifier for the campaign. | bytes |
|
||||
| lease | lease is the ID of the lease attached to leadership of the election. If the lease expires or is revoked before resigning leadership, then the leadership is transferred to the next campaigner, if any. | int64 |
|
||||
| value | value is the initial proclaimed value set when the campaigner wins the election. | bytes |
|
||||
|
||||
|
||||
|
||||
##### message `CampaignResponse` (etcdserver/api/v3election/v3electionpb/v3election.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| header | | etcdserverpb.ResponseHeader |
|
||||
| leader | leader describes the resources used for holding leadereship of the election. | LeaderKey |
|
||||
|
||||
|
||||
|
||||
##### message `LeaderKey` (etcdserver/api/v3election/v3electionpb/v3election.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| name | name is the election identifier that correponds to the leadership key. | bytes |
|
||||
| key | key is an opaque key representing the ownership of the election. If the key is deleted, then leadership is lost. | bytes |
|
||||
| rev | rev is the creation revision of the key. It can be used to test for ownership of an election during transactions by testing the key's creation revision matches rev. | int64 |
|
||||
| lease | lease is the lease ID of the election leader. | int64 |
|
||||
|
||||
|
||||
|
||||
##### message `LeaderRequest` (etcdserver/api/v3election/v3electionpb/v3election.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| name | name is the election identifier for the leadership information. | bytes |
|
||||
|
||||
|
||||
|
||||
##### message `LeaderResponse` (etcdserver/api/v3election/v3electionpb/v3election.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| header | | etcdserverpb.ResponseHeader |
|
||||
| kv | kv is the key-value pair representing the latest leader update. | mvccpb.KeyValue |
|
||||
|
||||
|
||||
|
||||
##### message `ProclaimRequest` (etcdserver/api/v3election/v3electionpb/v3election.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| leader | leader is the leadership hold on the election. | LeaderKey |
|
||||
| value | value is an update meant to overwrite the leader's current value. | bytes |
|
||||
|
||||
|
||||
|
||||
##### message `ProclaimResponse` (etcdserver/api/v3election/v3electionpb/v3election.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| header | | etcdserverpb.ResponseHeader |
|
||||
|
||||
|
||||
|
||||
##### message `ResignRequest` (etcdserver/api/v3election/v3electionpb/v3election.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| leader | leader is the leadership to relinquish by resignation. | LeaderKey |
|
||||
|
||||
|
||||
|
||||
##### message `ResignResponse` (etcdserver/api/v3election/v3electionpb/v3election.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| header | | etcdserverpb.ResponseHeader |
|
||||
|
||||
|
||||
|
||||
##### message `Event` (mvcc/mvccpb/kv.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| type | type is the kind of event. If type is a PUT, it indicates new data has been stored to the key. If type is a DELETE, it indicates the key was deleted. | EventType |
|
||||
| kv | kv holds the KeyValue for the event. A PUT event contains current kv pair. A PUT event with kv.Version=1 indicates the creation of a key. A DELETE/EXPIRE event contains the deleted key with its modification revision set to the revision of deletion. | KeyValue |
|
||||
| prev_kv | prev_kv holds the key-value pair before the event happens. | KeyValue |
|
||||
|
||||
|
||||
|
||||
##### message `KeyValue` (mvcc/mvccpb/kv.proto)
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| key | key is the key in bytes. An empty key is not allowed. | bytes |
|
||||
| create_revision | create_revision is the revision of last creation on this key. | int64 |
|
||||
| mod_revision | mod_revision is the revision of last modification on this key. | int64 |
|
||||
| version | version is the version of the key. A deletion resets the version to zero and any modification of the key increases its version. | int64 |
|
||||
| value | value is the value held by the key, in bytes. | bytes |
|
||||
| lease | lease is the ID of the lease that attached to key. When the attached lease expires, the key will be deleted. If lease is 0, then no lease is attached to the key. | int64 |
|
||||
|
||||
|
||||
|
@ -40,8 +40,6 @@ This is a generated documentation. Please read the proto files for more.
|
||||
|
||||
##### service `KV` (etcdserver/etcdserverpb/rpc.proto)
|
||||
|
||||
for grpc-gateway
|
||||
|
||||
| Method | Request Type | Response Type | Description |
|
||||
| ------ | ------------ | ------------- | ----------- |
|
||||
| Range | RangeRequest | RangeResponse | Range gets the keys in the range from the key-value store. |
|
||||
@ -94,8 +92,6 @@ for grpc-gateway
|
||||
|
||||
##### message `AlarmRequest` (etcdserver/etcdserverpb/rpc.proto)
|
||||
|
||||
default, used to query if any alarm is active space quota is exhausted
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| action | action is the kind of alarm request to issue. The action may GET alarm statuses, ACTIVATE an alarm, or DEACTIVATE a raised alarm. | AlarmAction |
|
||||
@ -427,8 +423,8 @@ Empty field.
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| key | key is the first key to delete in the range. | bytes |
|
||||
| range_end | range_end is the key following the last key to delete for the range [key, range_end). If range_end is not given, the range is defined to contain only the key argument. If range_end is one bit larger than the given key, then the range is all the all keys with the prefix (the given key). If range_end is '\0', the range is all keys greater than or equal to the key argument. | bytes |
|
||||
| prev_kv | If prev_kv is set, etcd gets the previous key-value pairs before deleting it. The previous key-value pairs will be returned in the delte response. | bool |
|
||||
| range_end | range_end is the key following the last key to delete for the range [key, range_end). If range_end is not given, the range is defined to contain only the key argument. If range_end is one bit larger than the given key, then the range is all the keys with the prefix (the given key). If range_end is '\0', the range is all keys greater than or equal to the key argument. | bytes |
|
||||
| prev_kv | If prev_kv is set, etcd gets the previous key-value pairs before deleting it. The previous key-value pairs will be returned in the delete response. | bool |
|
||||
|
||||
|
||||
|
||||
@ -557,6 +553,7 @@ Empty field.
|
||||
| ----- | ----------- | ---- |
|
||||
| header | | ResponseHeader |
|
||||
| member | member is the member information for the added member. | Member |
|
||||
| members | members is a list of all members after adding the new member. | (slice of) Member |
|
||||
|
||||
|
||||
|
||||
@ -588,6 +585,7 @@ Empty field.
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| header | | ResponseHeader |
|
||||
| members | members is a list of all members after removing the member. | (slice of) Member |
|
||||
|
||||
|
||||
|
||||
@ -605,6 +603,7 @@ Empty field.
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| header | | ResponseHeader |
|
||||
| members | members is a list of all members after updating the member. | (slice of) Member |
|
||||
|
||||
|
||||
|
||||
@ -616,6 +615,8 @@ Empty field.
|
||||
| value | value is the value, in bytes, to associate with the key in the key-value store. | bytes |
|
||||
| lease | lease is the lease ID to associate with the key in the key-value store. A lease value of 0 indicates no lease. | int64 |
|
||||
| prev_kv | If prev_kv is set, etcd gets the previous key-value pair before changing it. The previous key-value pair will be returned in the put response. | bool |
|
||||
| ignore_value | If ignore_value is set, etcd updates the key using its current value. Returns an error if the key does not exist. | bool |
|
||||
| ignore_lease | If ignore_lease is set, etcd updates the key using its current lease. Returns an error if the key does not exist. | bool |
|
||||
|
||||
|
||||
|
||||
@ -632,9 +633,9 @@ Empty field.
|
||||
|
||||
| Field | Description | Type |
|
||||
| ----- | ----------- | ---- |
|
||||
| key | default, no sorting lowest target value first highest target value first key is the first key for the range. If range_end is not given, the request only looks up key. | bytes |
|
||||
| range_end | range_end is the upper bound on the requested range [key, range_end). If range_end is '\0', the range is all keys >= key. If the range_end is one bit larger than the given key, then the range requests get the all keys with the prefix (the given key). If both key and range_end are '\0', then range requests returns all keys. | bytes |
|
||||
| limit | limit is a limit on the number of keys returned for the request. | int64 |
|
||||
| key | key is the first key for the range. If range_end is not given, the request only looks up key. | bytes |
|
||||
| range_end | range_end is the upper bound on the requested range [key, range_end). If range_end is '\0', the range is all keys >= key. If range_end is key plus one (e.g., "aa"+1 == "ab", "a\xff"+1 == "b"), then the range request gets all keys prefixed with key. If both key and range_end are '\0', then the range request returns all keys. | bytes |
|
||||
| limit | limit is a limit on the number of keys returned for the request. When limit is set to 0, it is treated as no limit. | int64 |
|
||||
| revision | revision is the point-in-time of the key-value store to use for the range. If revision is less or equal to zero, the range is over the newest key-value store. If the revision has been compacted, ErrCompacted is returned as a response. | int64 |
|
||||
| sort_order | sort_order is the order for returned sorted results. | SortOrder |
|
||||
| sort_target | sort_target is the key-value field to use for sorting. | SortTarget |
|
||||
@ -765,7 +766,7 @@ From google paxosdb paper: Our implementation hinges around a powerful primitive
|
||||
| range_end | range_end is the end of the range [key, range_end) to watch. If range_end is not given, only the key argument is watched. If range_end is equal to '\0', all keys greater than or equal to the key argument are watched. If the range_end is one bit larger than the given key, then all keys with the prefix (the given key) will be watched. | bytes |
|
||||
| start_revision | start_revision is an optional revision to watch from (inclusive). No start_revision is "now". | int64 |
|
||||
| progress_notify | progress_notify is set so that the etcd server will periodically send a WatchResponse with no events to the new watcher if there are no recent events. It is useful when clients wish to recover a disconnected watcher starting from a recent known revision. The etcd server may decide how often it will send notifications based on current load. | bool |
|
||||
| filters | filter out put event. filter out delete event. filters filter the events at server side before it sends back to the watcher. | (slice of) FilterType |
|
||||
| filters | filters filter the events at server side before it sends back to the watcher. | (slice of) FilterType |
|
||||
| prev_kv | If prev_kv is set, created watcher gets the previous KV before the event happens. If the previous KV is already compacted, nothing will be returned. | bool |
|
||||
|
||||
|
||||
@ -789,6 +790,7 @@ From google paxosdb paper: Our implementation hinges around a powerful primitive
|
||||
| created | created is set to true if the response is for a create watch request. The client should record the watch_id and expect to receive events for the created watcher from the same stream. All events sent to the created watcher will attach with the same watch_id. | bool |
|
||||
| canceled | canceled is set to true if the response is for a cancel watch request. No further events will be sent to the canceled watcher. | bool |
|
||||
| compact_revision | compact_revision is set to the minimum index if a watcher tries to watch at a compacted index. This happens when creating a watcher at a compacted revision or the watcher cannot catch up with the progress of the key-value store. The client should treat the watcher as canceled and should not try to create any watcher with the same start_revision again. | int64 |
|
||||
| cancel_reason | cancel_reason indicates the reason for canceling the watcher. | string |
|
||||
| events | | (slice of) mvccpb.Event |
|
||||
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
334
Documentation/dev-guide/apispec/swagger/v3election.swagger.json
Normal file
334
Documentation/dev-guide/apispec/swagger/v3election.swagger.json
Normal file
@ -0,0 +1,334 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "etcdserver/api/v3election/v3electionpb/v3election.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {
|
||||
"/v3alpha/election/campaign": {
|
||||
"post": {
|
||||
"summary": "Campaign waits to acquire leadership in an election, returning a LeaderKey\nrepresenting the leadership if successful. The LeaderKey can then be used\nto issue new values on the election, transactionally guard API requests on\nleadership still being held, and resign from the election.",
|
||||
"operationId": "Campaign",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v3electionpbCampaignResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v3electionpbCampaignRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Election"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v3alpha/election/leader": {
|
||||
"post": {
|
||||
"summary": "Leader returns the current election proclamation, if any.",
|
||||
"operationId": "Leader",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v3electionpbLeaderResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v3electionpbLeaderRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Election"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v3alpha/election/observe": {
|
||||
"post": {
|
||||
"summary": "Observe streams election proclamations in-order as made by the election's\nelected leaders.",
|
||||
"operationId": "Observe",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "(streaming responses)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v3electionpbLeaderResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v3electionpbLeaderRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Election"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v3alpha/election/proclaim": {
|
||||
"post": {
|
||||
"summary": "Proclaim updates the leader's posted value with a new value.",
|
||||
"operationId": "Proclaim",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v3electionpbProclaimResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v3electionpbProclaimRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Election"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v3alpha/election/resign": {
|
||||
"post": {
|
||||
"summary": "Resign releases election leadership so other campaigners may acquire\nleadership on the election.",
|
||||
"operationId": "Resign",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v3electionpbResignResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v3electionpbResignRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Election"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"etcdserverpbResponseHeader": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cluster_id": {
|
||||
"type": "string",
|
||||
"format": "uint64",
|
||||
"description": "cluster_id is the ID of the cluster which sent the response."
|
||||
},
|
||||
"member_id": {
|
||||
"type": "string",
|
||||
"format": "uint64",
|
||||
"description": "member_id is the ID of the member which sent the response."
|
||||
},
|
||||
"revision": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "revision is the key-value store revision when the request was applied."
|
||||
},
|
||||
"raft_term": {
|
||||
"type": "string",
|
||||
"format": "uint64",
|
||||
"description": "raft_term is the raft term when the request was applied."
|
||||
}
|
||||
}
|
||||
},
|
||||
"mvccpbKeyValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "key is the key in bytes. An empty key is not allowed."
|
||||
},
|
||||
"create_revision": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "create_revision is the revision of last creation on this key."
|
||||
},
|
||||
"mod_revision": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "mod_revision is the revision of last modification on this key."
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "version is the version of the key. A deletion resets\nthe version to zero and any modification of the key\nincreases its version."
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "value is the value held by the key, in bytes."
|
||||
},
|
||||
"lease": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "lease is the ID of the lease that attached to key.\nWhen the attached lease expires, the key will be deleted.\nIf lease is 0, then no lease is attached to the key."
|
||||
}
|
||||
}
|
||||
},
|
||||
"v3electionpbCampaignRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "name is the election's identifier for the campaign."
|
||||
},
|
||||
"lease": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "lease is the ID of the lease attached to leadership of the election. If the\nlease expires or is revoked before resigning leadership, then the\nleadership is transferred to the next campaigner, if any."
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "value is the initial proclaimed value set when the campaigner wins the\nelection."
|
||||
}
|
||||
}
|
||||
},
|
||||
"v3electionpbCampaignResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"header": {
|
||||
"$ref": "#/definitions/etcdserverpbResponseHeader"
|
||||
},
|
||||
"leader": {
|
||||
"$ref": "#/definitions/v3electionpbLeaderKey",
|
||||
"description": "leader describes the resources used for holding leadereship of the election."
|
||||
}
|
||||
}
|
||||
},
|
||||
"v3electionpbLeaderKey": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "name is the election identifier that correponds to the leadership key."
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "key is an opaque key representing the ownership of the election. If the key\nis deleted, then leadership is lost."
|
||||
},
|
||||
"rev": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "rev is the creation revision of the key. It can be used to test for ownership\nof an election during transactions by testing the key's creation revision\nmatches rev."
|
||||
},
|
||||
"lease": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "lease is the lease ID of the election leader."
|
||||
}
|
||||
}
|
||||
},
|
||||
"v3electionpbLeaderRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "name is the election identifier for the leadership information."
|
||||
}
|
||||
}
|
||||
},
|
||||
"v3electionpbLeaderResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"header": {
|
||||
"$ref": "#/definitions/etcdserverpbResponseHeader"
|
||||
},
|
||||
"kv": {
|
||||
"$ref": "#/definitions/mvccpbKeyValue",
|
||||
"description": "kv is the key-value pair representing the latest leader update."
|
||||
}
|
||||
}
|
||||
},
|
||||
"v3electionpbProclaimRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"leader": {
|
||||
"$ref": "#/definitions/v3electionpbLeaderKey",
|
||||
"description": "leader is the leadership hold on the election."
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "value is an update meant to overwrite the leader's current value."
|
||||
}
|
||||
}
|
||||
},
|
||||
"v3electionpbProclaimResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"header": {
|
||||
"$ref": "#/definitions/etcdserverpbResponseHeader"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v3electionpbResignRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"leader": {
|
||||
"$ref": "#/definitions/v3electionpbLeaderKey",
|
||||
"description": "leader is the leadership to relinquish by resignation."
|
||||
}
|
||||
}
|
||||
},
|
||||
"v3electionpbResignResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"header": {
|
||||
"$ref": "#/definitions/etcdserverpbResponseHeader"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
146
Documentation/dev-guide/apispec/swagger/v3lock.swagger.json
Normal file
146
Documentation/dev-guide/apispec/swagger/v3lock.swagger.json
Normal file
@ -0,0 +1,146 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "etcdserver/api/v3lock/v3lockpb/v3lock.proto",
|
||||
"version": "version not set"
|
||||
},
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"paths": {
|
||||
"/v3alpha/lock/lock": {
|
||||
"post": {
|
||||
"summary": "Lock acquires a distributed shared lock on a given named lock.\nOn success, it will return a unique key that exists so long as the\nlock is held by the caller. This key can be used in conjunction with\ntransactions to safely ensure updates to etcd only occur while holding\nlock ownership. The lock is held until Unlock is called on the key or the\nlease associate with the owner expires.",
|
||||
"operationId": "Lock",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v3lockpbLockResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v3lockpbLockRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Lock"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v3alpha/lock/unlock": {
|
||||
"post": {
|
||||
"summary": "Unlock takes a key returned by Lock and releases the hold on lock. The\nnext Lock caller waiting for the lock will then be woken up and given\nownership of the lock.",
|
||||
"operationId": "Unlock",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v3lockpbUnlockResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v3lockpbUnlockRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Lock"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"etcdserverpbResponseHeader": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cluster_id": {
|
||||
"type": "string",
|
||||
"format": "uint64",
|
||||
"description": "cluster_id is the ID of the cluster which sent the response."
|
||||
},
|
||||
"member_id": {
|
||||
"type": "string",
|
||||
"format": "uint64",
|
||||
"description": "member_id is the ID of the member which sent the response."
|
||||
},
|
||||
"revision": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "revision is the key-value store revision when the request was applied."
|
||||
},
|
||||
"raft_term": {
|
||||
"type": "string",
|
||||
"format": "uint64",
|
||||
"description": "raft_term is the raft term when the request was applied."
|
||||
}
|
||||
}
|
||||
},
|
||||
"v3lockpbLockRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "name is the identifier for the distributed shared lock to be acquired."
|
||||
},
|
||||
"lease": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "lease is the ID of the lease that will be attached to ownership of the\nlock. If the lease expires or is revoked and currently holds the lock,\nthe lock is automatically released. Calls to Lock with the same lease will\nbe treated as a single acquistion; locking twice with the same lease is a\nno-op."
|
||||
}
|
||||
}
|
||||
},
|
||||
"v3lockpbLockResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"header": {
|
||||
"$ref": "#/definitions/etcdserverpbResponseHeader"
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "key is a key that will exist on etcd for the duration that the Lock caller\nowns the lock. Users should not modify this key or the lock may exhibit\nundefined behavior."
|
||||
}
|
||||
}
|
||||
},
|
||||
"v3lockpbUnlockRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "key is the lock ownership key granted by Lock."
|
||||
}
|
||||
}
|
||||
},
|
||||
"v3lockpbUnlockResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"header": {
|
||||
"$ref": "#/definitions/etcdserverpbResponseHeader"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
# Experimental APIs and features
|
||||
|
||||
For the most part, the etcd project is stable, but we are still moving fast! We believe in the release fast philosophy. We want to get early feedback on features still in development and stabilizing. Thus, there are, and will be more, experimental features and APIs. We plan to improve these features based on the early feedback from the community, or abandon them if there is little interest, in the next few releases. If you are running a production system, please do not rely on any experimental features or APIs.
|
||||
For the most part, the etcd project is stable, but we are still moving fast! We believe in the release fast philosophy. We want to get early feedback on features still in development and stabilizing. Thus, there are, and will be more, experimental features and APIs. We plan to improve these features based on the early feedback from the community, or abandon them if there is little interest, in the next few releases. Please do not rely on any experimental features or APIs in production environment.
|
||||
|
||||
## The current experimental API/features are:
|
||||
|
||||
- v3 auth API: expect to be stable in 3.1 release
|
||||
- etcd gateway: expect to be stable in 3.1 release
|
||||
- [gateway][gateway]: beta, to be stable in 3.2 release
|
||||
- [gRPC proxy][grpc-proxy]: alpha, to be stable in 3.2 release
|
||||
|
||||
[gateway]: ../op-guide/gateway.md
|
||||
[grpc-proxy]: ../op-guide/grpc_proxy.md
|
||||
|
@ -10,7 +10,7 @@ export ETCDCTL_API=3
|
||||
|
||||
## Find versions
|
||||
|
||||
etcdctl version and Server API version can be useful in finding the appropriate commands to be used for performing various opertions on etcd.
|
||||
etcdctl version and Server API version can be useful in finding the appropriate commands to be used for performing various operations on etcd.
|
||||
|
||||
Here is the command to find the versions:
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
## Request size limit
|
||||
|
||||
etcd is designed to handle small key value pairs typical for metadata. Larger requests will work, but may increase the latency of other requests. For the time being, etcd guarantees to support RPC requests with up to 1MB of data. In the future, the size limit may be loosened or made it configurable.
|
||||
etcd is designed to handle small key value pairs typical for metadata. Larger requests will work, but may increase the latency of other requests. For the time being, etcd guarantees to support RPC requests with up to 1MB of data. In the future, the size limit may be loosened or made configurable.
|
||||
|
||||
## Storage size limit
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
etcd uses the [capnslog][capnslog] library for logging application output categorized into *levels*. A log message's level is determined according to these conventions:
|
||||
|
||||
* Error: Data has been lost, a request has failed for a bad reason, or a required resource has been lost
|
||||
* Examples:
|
||||
* Examples:
|
||||
* A failure to allocate disk space for WAL
|
||||
|
||||
* Warning: (Hopefully) Temporary conditions that may cause errors, but may work fine. A replica disappearing (that may reconnect) is a warning.
|
||||
@ -26,4 +26,4 @@ etcd uses the [capnslog][capnslog] library for logging application output catego
|
||||
* Send a normal message to a remote peer
|
||||
* Write a log entry to disk
|
||||
|
||||
[capnslog]: [https://github.com/coreos/pkg/tree/master/capnslog]
|
||||
[capnslog]: https://github.com/coreos/pkg/tree/master/capnslog
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
The guide talks about how to release a new version of etcd.
|
||||
|
||||
The procedure includes some manual steps for sanity checking but it can probably be further scripted. Please keep this document up-to-date if making changes to the release process.
|
||||
The procedure includes some manual steps for sanity checking, but it can probably be further scripted. Please keep this document up-to-date if making changes to the release process.
|
||||
|
||||
## Prepare release
|
||||
|
||||
@ -58,7 +58,7 @@ Run release script in root directory:
|
||||
|
||||
It generates all release binaries and images under directory ./release.
|
||||
|
||||
## Sign binaries and images
|
||||
## Sign binaries, images, and source code
|
||||
|
||||
etcd project key must be used to sign the generated binaries and images.`$SUBKEYID` is the key ID of etcd project Yubikey. Connect the key and run `gpg2 --card-status` to get the ID.
|
||||
|
||||
@ -68,6 +68,15 @@ The following commands are used for public release sign:
|
||||
cd release
|
||||
for i in etcd-*{.zip,.tar.gz}; do gpg2 --default-key $SUBKEYID --armor --output ${i}.asc --detach-sign ${i}; done
|
||||
for i in etcd-*{.zip,.tar.gz}; do gpg2 --verify ${i}.asc ${i}; done
|
||||
|
||||
# sign zipped source code files
|
||||
wget https://github.com/coreos/etcd/archive/${VERSION}.zip
|
||||
gpg2 --armor --default-key $SUBKEYID --output ${VERSION}.zip.asc --detach-sign ${VERSION}.zip
|
||||
gpg2 --verify ${VERSION}.zip.asc ${VERSION}.zip
|
||||
|
||||
wget https://github.com/coreos/etcd/archive/${VERSION}.tar.gz
|
||||
gpg2 --armor --default-key $SUBKEYID --output ${VERSION}.tar.gz.asc --detach-sign ${VERSION}.tar.gz
|
||||
gpg2 --verify ${VERSION}.tar.gz.asc ${VERSION}.tar.gz
|
||||
```
|
||||
|
||||
The public key for GPG signing can be found at [CoreOS Application Signing Key](https://coreos.com/security/app-signing-key)
|
||||
|
@ -10,29 +10,35 @@ The easiest way to get etcd is to use one of the pre-built release binaries whic
|
||||
|
||||
## Build the latest version
|
||||
|
||||
For those wanting to try the very latest version, build etcd from the `master` branch.
|
||||
[Go](https://golang.org/) version 1.6+ (with HTTP2 support) is required to build the latest version of etcd.
|
||||
etcd vendors its dependency for official release binaries, while making vendoring optional to avoid import conflicts.
|
||||
[`build` script][build-script] would automatically include the vendored dependencies from [`cmd`][cmd-directory] directory.
|
||||
For those wanting to try the very latest version, build etcd from the `master` branch. [Go](https://golang.org/) version 1.8+ is required to build the latest version of etcd. To ensure etcd is built against well-tested libraries, etcd vendors its dependencies for official release binaries. However, etcd's vendoring is also optional to avoid potential import conflicts when embedding the etcd server or using the etcd client.
|
||||
|
||||
Here are the commands to build an etcd binary from the `master` branch:
|
||||
To build `etcd` from the `master` branch without a `GOPATH` using the official `build` script:
|
||||
|
||||
```
|
||||
# go is required
|
||||
$ go version
|
||||
go version go1.6 darwin/amd64
|
||||
|
||||
# GOPATH should be set correctly
|
||||
$ echo $GOPATH
|
||||
/Users/example/go
|
||||
|
||||
$ mkdir -p $GOPATH/src/github.com/coreos
|
||||
$ cd $GOPATH/src/github.com/coreos
|
||||
```sh
|
||||
$ git clone https://github.com/coreos/etcd.git
|
||||
$ cd etcd
|
||||
$ ./build
|
||||
$ ./bin/etcd
|
||||
...
|
||||
```
|
||||
|
||||
To build a vendored `etcd` from the `master` branch via `go get`:
|
||||
|
||||
```sh
|
||||
# GOPATH should be set
|
||||
$ echo $GOPATH
|
||||
/Users/example/go
|
||||
$ go get github.com/coreos/etcd/cmd/etcd
|
||||
$ $GOPATH/bin/etcd
|
||||
```
|
||||
|
||||
To build `etcd` from the `master` branch without vendoring (may not build due to upstream conflicts):
|
||||
|
||||
```sh
|
||||
# GOPATH should be set
|
||||
$ echo $GOPATH
|
||||
/Users/example/go
|
||||
$ go get github.com/coreos/etcd
|
||||
$ $GOPATH/bin/etcd
|
||||
```
|
||||
|
||||
## Test the installation
|
||||
|
@ -12,9 +12,10 @@ The easiest way to get started using etcd as a distributed key-value store is to
|
||||
|
||||
- [Setting up local clusters][local_cluster]
|
||||
- [Interacting with etcd][interacting]
|
||||
- [API references][api_ref]
|
||||
- [gRPC gateway][api_grpc_gateway]
|
||||
- gRPC [etcd core][api_ref] and [etcd concurrency][api_concurrency_ref] API references
|
||||
- [HTTP JSON API through the gRPC gateway][api_grpc_gateway]
|
||||
- [gRPC naming and discovery][grpc_naming]
|
||||
- [Client][namespace_client] and [proxy][namespace_proxy] namespacing
|
||||
- [Embedding etcd][embed_etcd]
|
||||
- [Experimental features and APIs][experimental]
|
||||
- [System limits][system-limit]
|
||||
@ -25,39 +26,51 @@ Administrators who need to create reliable and scalable key-value stores for the
|
||||
|
||||
- [Setting up etcd clusters][clustering]
|
||||
- [Setting up etcd gateways][gateway]
|
||||
- [Setting up etcd gRPC proxy (pre-alpha)][grpc_proxy]
|
||||
- [Run etcd clusters inside containers][container]
|
||||
- [Setting up etcd gRPC proxy][grpc_proxy]
|
||||
- [Hardware recommendations][hardware]
|
||||
- [Configuration][conf]
|
||||
- [Security][security]
|
||||
- [Authentication][authentication]
|
||||
- [Monitoring][monitoring]
|
||||
- [Maintenance][maintenance]
|
||||
- [Understand failures][failures]
|
||||
- [Disaster recovery][recovery]
|
||||
- [Performance][performance]
|
||||
- [Versioning][versioning]
|
||||
- [Supported platform][supported_platform]
|
||||
|
||||
### Platform guides
|
||||
|
||||
- [Supported systems][supported_platforms]
|
||||
- [Docker container][container_docker]
|
||||
- [Container Linux, systemd][container_linux_platform]
|
||||
- [rkt container][container_rkt]
|
||||
- [Amazon Web Services][aws_platform]
|
||||
- [FreeBSD][freebsd_platform]
|
||||
|
||||
### Upgrading and compatibility
|
||||
|
||||
- [Migrate applications from using API v2 to API v3][v2_migration]
|
||||
- [Upgrading a v2.3 cluster to v3.0][v3_upgrade]
|
||||
- [Upgrading a v3.0 cluster to v3.1][v31_upgrade]
|
||||
- [Upgrading a v3.1 cluster to v3.2][v32_upgrade]
|
||||
|
||||
## Learning
|
||||
|
||||
To learn more about the concepts and internals behind etcd, read the following pages:
|
||||
|
||||
- [Why etcd][why] (TODO)
|
||||
- [Why etcd?][why]
|
||||
- [Understand data model][data_model]
|
||||
- [Understand APIs][understand_apis]
|
||||
- [Glossary][glossary]
|
||||
- Internals (TODO)
|
||||
|
||||
## Upgrading and compatibility
|
||||
|
||||
- [Migrate applications from using API v2 to API v3][v2_migration]
|
||||
- [Updating v2.3 to v3.0][v3_upgrade]
|
||||
- Internals
|
||||
- [Auth subsystem][auth_design]
|
||||
|
||||
## Frequently Asked Questions (FAQ)
|
||||
|
||||
Answers to [common questions] about etcd.
|
||||
|
||||
[api_ref]: dev-guide/api_reference_v3.md
|
||||
[api_concurrency_ref]: dev-guide/api_concurrency_reference_v3.md
|
||||
[api_grpc_gateway]: dev-guide/api_grpc_gateway.md
|
||||
[clustering]: op-guide/clustering.md
|
||||
[conf]: op-guide/configuration.md
|
||||
@ -72,6 +85,8 @@ Answers to [common questions] about etcd.
|
||||
[failures]: op-guide/failures.md
|
||||
[gateway]: op-guide/gateway.md
|
||||
[glossary]: learning/glossary.md
|
||||
[namespace_client]: https://godoc.org/github.com/coreos/etcd/clientv3/namespace
|
||||
[namespace_proxy]: op-guide/grpc_proxy.md#namespacing
|
||||
[grpc_proxy]: op-guide/grpc_proxy.md
|
||||
[hardware]: op-guide/hardware.md
|
||||
[interacting]: dev-guide/interacting_v3.md
|
||||
@ -82,9 +97,17 @@ Answers to [common questions] about etcd.
|
||||
[security]: op-guide/security.md
|
||||
[monitoring]: op-guide/monitoring.md
|
||||
[v2_migration]: op-guide/v2-migration.md
|
||||
[container]: op-guide/container.md
|
||||
[container_rkt]: op-guide/container.md#rkt
|
||||
[container_docker]: op-guide/container.md#docker
|
||||
[understand_apis]: learning/api.md
|
||||
[versioning]: op-guide/versioning.md
|
||||
[supported_platform]: op-guide/supported-platform.md
|
||||
[supported_platforms]: op-guide/supported-platform.md
|
||||
[container_linux_platform]: platforms/container-linux-systemd.md
|
||||
[freebsd_platform]: platforms/freebsd.md
|
||||
[aws_platform]: platforms/aws.md
|
||||
[experimental]: dev-guide/experimental_apis.md
|
||||
[v3_upgrade]: upgrades/upgrade_3_0.md
|
||||
[v31_upgrade]: upgrades/upgrade_3_1.md
|
||||
[v32_upgrade]: upgrades/upgrade_3_2.md
|
||||
[authentication]: op-guide/authentication.md
|
||||
[auth_design]: learning/auth_design.md
|
||||
|
@ -78,6 +78,26 @@ On the other hand, if the downed member is removed from cluster membership first
|
||||
|
||||
etcd sets `strict-reconfig-check` in order to reject reconfiguration requests that would cause quorum loss. Abandoning quorum is really risky (especially when the cluster is already unhealthy). Although it may be tempting to disable quorum checking if there's quorum loss to add a new member, this could lead to full fledged cluster inconsistency. For many applications, this will make the problem even worse ("disk geometry corruption" being a candidate for most terrifying).
|
||||
|
||||
#### Why does etcd lose its leader from disk latency spikes?
|
||||
|
||||
This is intentional; disk latency is part of leader liveness. Suppose the cluster leader takes a minute to fsync a raft log update to disk, but the etcd cluster has a one second election timeout. Even though the leader can process network messages within the election interval (e.g., send heartbeats), it's effectively unavailable because it can't commit any new proposals; it's waiting on the slow disk. If the cluster frequently loses its leader due to disk latencies, try [tuning][tuning] the disk settings or etcd time parameters.
|
||||
|
||||
#### What does the etcd warning "request ignored (cluster ID mismatch)" mean?
|
||||
|
||||
Every new etcd cluster generates a new cluster ID based on the initial cluster configuration and a user-provided unique `initial-cluster-token` value. By having unique cluster ID's, etcd is protected from cross-cluster interaction which could corrupt the cluster.
|
||||
|
||||
Usually this warning happens after tearing down an old cluster, then reusing some of the peer addresses for the new cluster. If any etcd process from the old cluster is still running it will try to contact the new cluster. The new cluster will recognize a cluster ID mismatch, then ignore the request and emit this warning. This warning is often cleared by ensuring peer addresses among distinct clusters are disjoint.
|
||||
|
||||
#### What does "mvcc: database space exceeded" mean and how do I fix it?
|
||||
|
||||
The [multi-version concurrency control][api-mvcc] data model in etcd keeps an exact history of the keyspace. Without periodically compacting this history (e.g., by setting `--auto-compaction`), etcd will eventually exhaust its storage space. If etcd runs low on storage space, it raises a space quota alarm to protect the cluster from further writes. So long as the alarm is raised, etcd responds to write requests with the error `mvcc: database space exceeded`.
|
||||
|
||||
To recover from the low space quota alarm:
|
||||
|
||||
1. [Compact][maintenance-compact] etcd's history.
|
||||
2. [Defragment][maintenance-defragment] every etcd endpoint.
|
||||
3. [Disarm][maintenance-disarm] the alarm.
|
||||
|
||||
### Performance
|
||||
|
||||
#### How should I benchmark etcd?
|
||||
@ -108,11 +128,10 @@ A slow network can also cause this issue. If network metrics among the etcd mach
|
||||
|
||||
If none of the above suggestions clear the warnings, please [open an issue][new_issue] with detailed logging, monitoring, metrics and optionally workload information.
|
||||
|
||||
#### What does the etcd warning "request ignored (cluster ID mismatch)" mean?
|
||||
#### What does the etcd warning "snapshotting is taking more than x seconds to finish ..." mean?
|
||||
|
||||
Every new etcd cluster generates a new cluster ID based on the initial cluster configuration and a user-provided unique `initial-cluster-token` value. By having unique cluster ID's, etcd is protected from cross-cluster interaction which could corrupt the cluster.
|
||||
etcd sends a snapshot of its complete key-value store to refresh slow followers and for [backups][backup]. Slow snapshot transfer times increase MTTR; if the cluster is ingesting data with high throughput, slow followers may livelock by needing a new snapshot before finishing receiving a snapshot. To catch slow snapshot performance, etcd warns when sending a snapshot takes more than thirty seconds and exceeds the expected transfer time for a 1Gbps connection.
|
||||
|
||||
Usually this warning happens after tearing down an old cluster, then reusing some of the peer addresses for the new cluster. If any etcd process from the old cluster is still running it will try to contact the new cluster. The new cluster will recognize a cluster ID mismatch, then ignore the request and emit this warning. This warning is often cleared by ensuring peer addresses among distinct clusters are disjoint.
|
||||
|
||||
[hardware-setup]: ./op-guide/hardware.md
|
||||
[supported-platform]: ./op-guide/supported-platform.md
|
||||
@ -126,3 +145,7 @@ Usually this warning happens after tearing down an old cluster, then reusing som
|
||||
[runtime reconfiguration]: https://github.com/coreos/etcd/blob/master/Documentation/op-guide/runtime-configuration.md
|
||||
[benchmark]: https://github.com/coreos/etcd/tree/master/tools/benchmark
|
||||
[benchmark-result]: https://github.com/coreos/etcd/blob/master/Documentation/op-guide/performance.md
|
||||
[api-mvcc]: learning/api.md#revisions
|
||||
[maintenance-compact]: op-guide/maintenance.md#history-compaction
|
||||
[maintenance-defragment]: op-guide/maintenance.md#defragmentation
|
||||
[maintenance-disarm]: ../etcdctl/README.md#alarm-disarm
|
||||
|
@ -21,6 +21,7 @@
|
||||
- [etcd/clientv3](https://github.com/coreos/etcd/blob/master/clientv3) - the officially maintained Go client for v3
|
||||
- [etcd/client](https://github.com/coreos/etcd/blob/master/client) - the officially maintained Go client for v2
|
||||
- [go-etcd](https://github.com/coreos/go-etcd) - the deprecated official client. May be useful for older (<2.0.0) versions of etcd.
|
||||
- [encWrapper](https://github.com/lumjjb/etcd/tree/enc_wrapper/clientwrap/encwrapper) - encWrapper is an encryption wrapper for the etcd client Keys API/KV.
|
||||
|
||||
**Java libraries**
|
||||
|
||||
@ -44,6 +45,8 @@
|
||||
- [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)
|
||||
- [txaio-etcd](https://github.com/crossbario/txaio-etcd) - Asynchronous etcd v3-only client library for Twisted (today) and asyncio (future)
|
||||
- [dims/etcd3-gateway](https://github.com/dims/etcd3-gateway) - etcd v3 API library using the HTTP grpc gateway
|
||||
|
||||
**Node libraries**
|
||||
|
||||
@ -56,9 +59,11 @@
|
||||
- [iconara/etcd-rb](https://github.com/iconara/etcd-rb)
|
||||
- [jpfuentes2/etcd-ruby](https://github.com/jpfuentes2/etcd-ruby)
|
||||
- [ranjib/etcd-ruby](https://github.com/ranjib/etcd-ruby) - Supports v2
|
||||
- [davissp14/etcdv3-ruby](https://github.com/davissp14/etcdv3-ruby) - Supports v3
|
||||
|
||||
**C libraries**
|
||||
|
||||
- [apache/celix/etcdlib](https://github.com/apache/celix/tree/develop/etcdlib) - Supports v2
|
||||
- [jdarcy/etcd-api](https://github.com/jdarcy/etcd-api) - Supports v2
|
||||
- [shafreeck/cetcd](https://github.com/shafreeck/cetcd) - Supports v2
|
||||
|
||||
@ -86,7 +91,7 @@
|
||||
**PHP Libraries**
|
||||
|
||||
- [linkorb/etcd-php](https://github.com/linkorb/etcd-php)
|
||||
- [activecollab/etcd](https://github.com/activecollab/etcd)
|
||||
- [activecollab/etcd](https://github.com/activecollab/etcd)
|
||||
|
||||
**Haskell libraries**
|
||||
|
||||
@ -104,6 +109,10 @@
|
||||
|
||||
- [efrecon/etcd-tcl](https://github.com/efrecon/etcd-tcl) - Supports v2, except wait.
|
||||
|
||||
**Rust libraries**
|
||||
|
||||
- [jimmycuadra/rust-etcd](https://github.com/jimmycuadra/rust-etcd) - Supports v2
|
||||
|
||||
**Gradle Plugins**
|
||||
|
||||
- [gradle-etcd-rest-plugin](https://github.com/cdancy/gradle-etcd-rest-plugin) - Supports v2
|
||||
@ -123,10 +132,11 @@
|
||||
|
||||
**Projects using etcd**
|
||||
|
||||
- [apache/celix](https://github.com/apache/celix) - an implementation of the OSGi specification adapted to C and C++
|
||||
- [binocarlos/yoda](https://github.com/binocarlos/yoda) - etcd + ZeroMQ
|
||||
- [blox/blox](https://github.com/blox/blox) - a collection of open source projects for container management and orchestration with AWS ECS
|
||||
- [calavera/active-proxy](https://github.com/calavera/active-proxy) - HTTP Proxy configured with etcd
|
||||
- [chain/chain](https://github.com/chain/chain) - software designed to operate and connect to highly scalable permissioned blockchain networks
|
||||
- [chain/chain](https://github.com/chain/chain) - software designed to operate and connect to highly scalable permissioned blockchain networks
|
||||
- [derekchiang/etcdplus](https://github.com/derekchiang/etcdplus) - A set of distributed synchronization primitives built upon etcd
|
||||
- [go-discover](https://github.com/flynn/go-discover) - service discovery in Go
|
||||
- [gleicon/goreman](https://github.com/gleicon/goreman/tree/etcd) - Branch of the Go Foreman clone with etcd support
|
||||
@ -135,7 +145,6 @@
|
||||
- [mattn/etcdenv](https://github.com/mattn/etcdenv) - "env" shebang with etcd integration
|
||||
- [kelseyhightower/confd](https://github.com/kelseyhightower/confd) - Manage local app config files using templates and data from etcd
|
||||
- [configdb](https://git.autistici.org/ai/configdb/tree/master) - A REST relational abstraction on top of arbitrary database backends, aimed at storing configs and inventories.
|
||||
- [scrz](https://github.com/scrz/scrz) - Container manager, stores configuration in etcd.
|
||||
- [fleet](https://github.com/coreos/fleet) - Distributed init system
|
||||
- [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes) - Container cluster manager introduced by Google.
|
||||
- [mailgun/vulcand](https://github.com/mailgun/vulcand) - HTTP proxy that uses etcd as a configuration backend.
|
||||
@ -146,3 +155,4 @@
|
||||
- [lytics/metafora](https://github.com/lytics/metafora) - Go distributed task library
|
||||
- [ryandoyle/nss-etcd](https://github.com/ryandoyle/nss-etcd) - A GNU libc NSS module for resolving names from etcd.
|
||||
- [Gru](https://github.com/dnaeon/gru) - Orchestration made easy with Go
|
||||
- [Vitess](http://vitess.io/) - Vitess is a database clustering system for horizontal scaling of MySQL.
|
@ -1,10 +1,35 @@
|
||||
# etcd3 API
|
||||
|
||||
NOTE: this doc is not finished!
|
||||
This document is meant to give an overview of the etcd3 API's central design. It is by no means all encompassing, but intended to focus on the basic ideas needed to understand etcd without the distraction of less common API calls. All etcd3 API's are defined in [gRPC services][grpc-service], which categorize remote procedure calls (RPCs) understood by the etcd server. A full listing of all etcd RPCs are documented in markdown in the [gRPC API listing][grpc-api].
|
||||
|
||||
## Response header
|
||||
## gRPC Services
|
||||
|
||||
All Responses from etcd API have a [response header][response_header] attached. The response header includes the metadata of the response.
|
||||
Every API request sent to an etcd server is a gRPC remote procedure call. RPCs in etcd3 are categorized based on functionality into services.
|
||||
|
||||
Services important for dealing with etcd's key space include:
|
||||
* KV - Creates, updates, fetches, and deletes key-value pairs.
|
||||
* Watch - Monitors changes to keys.
|
||||
* Lease - Primitives for consuming client keep-alive messages.
|
||||
|
||||
Services which manage the cluster itself include:
|
||||
* Auth - Role based authentication mechanism for authenticating users.
|
||||
* Cluster - Provides membership information and configuration facilities.
|
||||
* Maintenance - Takes recovery snapshots, defragments the store, and returns per-member status information.
|
||||
|
||||
### Requests and Responses
|
||||
|
||||
All RPCs in etcd3 follow the same format. Each RPC has a function `Name` which takes `NameRequest` as an argument and returns `NameResponse` as a response. For example, here is the `Range` RPC description:
|
||||
|
||||
```protobuf
|
||||
service KV {
|
||||
Range(RangeRequest) returns (RangeResponse)
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Response header
|
||||
|
||||
All Responses from etcd API have an attached response header which includes cluster metadata for the response:
|
||||
|
||||
```proto
|
||||
message ResponseHeader {
|
||||
@ -15,10 +40,10 @@ message ResponseHeader {
|
||||
}
|
||||
```
|
||||
|
||||
* Cluster_ID - the ID of the cluster that generates the response
|
||||
* Member_ID - the ID of the member that generates the response
|
||||
* Revision - the revision of the key-value store when the response is generated
|
||||
* Raft_Term - the Raft term of the member when the response is generated
|
||||
* Cluster_ID - the ID of the cluster generating the response.
|
||||
* Member_ID - the ID of the member generating the response.
|
||||
* Revision - the revision of the key-value store when generating the response.
|
||||
* Raft_Term - the Raft term of the member when generating the response.
|
||||
|
||||
An application may read the Cluster_ID (Member_ID) field to ensure it is communicating with the intended cluster (member).
|
||||
|
||||
@ -28,11 +53,13 @@ Applications can use `Raft_Term` to detect when the cluster completes a new lead
|
||||
|
||||
## Key-Value API
|
||||
|
||||
Key-Value API is used to manipulate key-value pairs stored inside etcd. The key-value API is defined as a [gRPC service][kv-service]. The Key-Value pair is defined as structured data in [protobuf format][kv-proto].
|
||||
The Key-Value API manipulates key-value pairs stored inside etcd. The majority of requests made to etcd are usually key-value requests.
|
||||
|
||||
### System primitives
|
||||
|
||||
### Key-Value pair
|
||||
|
||||
A key-value pair is the smallest unit that the key-value API can manipulate. Each key-value pair has a number of fields:
|
||||
A key-value pair is the smallest unit that the key-value API can manipulate. Each key-value pair has a number of fields, defined in [protobuf format][kv-proto]:
|
||||
|
||||
```protobuf
|
||||
message KeyValue {
|
||||
@ -52,6 +79,403 @@ message KeyValue {
|
||||
* Mod_Revision - revision of the last modification on the key.
|
||||
* Lease - the ID of the lease attached to the key. If lease is 0, then no lease is attached to the key.
|
||||
|
||||
|
||||
In addition to just the key and value, etcd attaches additional revision metadata as part of the key message. This revision information orders keys by time of creation and modification, which is useful for managing concurrency for distributed synchronization. The etcd client's [distributed shared locks][locks] use the creation revision to wait for lock ownership. Similarly, the modification revision is used for detecting [software transactional memory][STM] read set conflicts and waiting on [leader election][elections] updates.
|
||||
|
||||
#### Revisions
|
||||
|
||||
etcd maintains a 64-bit cluster-wide counter, the store revision, that is incremented each time the key space is modified. The revision serves as a global logical clock, sequentially ordering all updates to the store. The change represented by a new revisions is incremental; the data associated with a revision is the data that changed the store. Internally, a new revision means writing the changes to the backend's B+tree, keyed by the incremented revision.
|
||||
|
||||
Revisions become more valuable when taking considering etcd3's [multi-version concurrency control][mvcc] backend. The MVCC model means that the key-value store can be viewed from past revisions since historical key revisions are retained. The retention policy for this history can be configured by cluster administrators for fine-grained storage management; usually etcd3 discards old revisions of keys on a timer. A typical etcd3 cluster retains superseded key data for hours. This also buys reliable handling for long client disconnection, not just transient network disruptions: watchers simply resume from the last observed historical revision. Similarly, to read from the store at a particular point-in-time, read requests can be tagged with a revision to return keys from a view of the key space at the point in time that revision was committed.
|
||||
|
||||
#### Key ranges
|
||||
|
||||
The etcd3 data model indexes all keys over a flat binary key space. This differs from other key-value store systems that use a hierarchical system of organizing keys into directories. Instead of listing keys by directory, keys are listed by key intervals `[a, b)`.
|
||||
|
||||
These intervals are often referred to as "ranges" in etcd3. Operations over ranges are more powerful than operations on directories. Like a hierarchical store, intervals support single key lookups via `[a, a+1)` (e.g., ['a', 'a\x00') looks up 'a') and directory lookups by encoding keys by directory depth. In addition to those operations, intervals can also encode prefixes; for example the interval `['a', 'b')` looks up all keys prefixed by the string 'a'.
|
||||
|
||||
By convention, ranges for a Request are denoted by the fields `key` and `range_end`. The `key` field is the first key of the range and should be non-empty. The `range_end` is the key following the last key of the range. If `range_end` is not given or empty, the range is defined to contain only the key argument. If `range_end` is `key` plus one (e.g., "aa"+1 == "ab", "a\xff"+1 == "b"), then the range represents all keys prefixed with key. If both `key` and `range_end` are '\0', then range represents all keys. If `range_end` is '\0', the range is all keys greater than or equal to the key argument.
|
||||
|
||||
### Range
|
||||
|
||||
Keys are fetched from the key-value store using the `Range` API call, which takes a `RangeRequest`:
|
||||
|
||||
```protobuf
|
||||
message RangeRequest {
|
||||
enum SortOrder {
|
||||
NONE = 0; // default, no sorting
|
||||
ASCEND = 1; // lowest target value first
|
||||
DESCEND = 2; // highest target value first
|
||||
}
|
||||
enum SortTarget {
|
||||
KEY = 0;
|
||||
VERSION = 1;
|
||||
CREATE = 2;
|
||||
MOD = 3;
|
||||
VALUE = 4;
|
||||
}
|
||||
|
||||
bytes key = 1;
|
||||
bytes range_end = 2;
|
||||
int64 limit = 3;
|
||||
int64 revision = 4;
|
||||
SortOrder sort_order = 5;
|
||||
SortTarget sort_target = 6;
|
||||
bool serializable = 7;
|
||||
bool keys_only = 8;
|
||||
bool count_only = 9;
|
||||
int64 min_mod_revision = 10;
|
||||
int64 max_mod_revision = 11;
|
||||
int64 min_create_revision = 12;
|
||||
int64 max_create_revision = 13;
|
||||
}
|
||||
```
|
||||
|
||||
* Key, Range_End - The key range to fetch.
|
||||
* Limit - the maximum number of keys returned for the request. When limit is set to 0, it is treated as no limit.
|
||||
* Revision - the point-in-time of the key-value store to use for the range. If revision is less or equal to zero, the range is over the latest key-value store If the revision is compacted, ErrCompacted is returned as a response.
|
||||
* Sort_Order - the ordering for sorted requests.
|
||||
* Sort_Target - the key-value field to sort.
|
||||
* Serializable - sets the range request to use serializable member-local reads. By default, Range is linearizable; it reflects the current consensus of the cluster. For better performance and availability, in exchange for possible stale reads, a serializable range request is served locally without needing to reach consensus with other nodes in the cluster.
|
||||
* Keys_Only - return only the keys and not the values.
|
||||
* Count_Only - return only the count of the keys in the range.
|
||||
* Min_Mod_Revision - the lower bound for key mod revisions; filters out lesser mod revisions.
|
||||
* Max_Mod_Revision - the upper bound for key mod revisions; filters out greater mod revisions.
|
||||
* Min_Create_Revision - the lower bound for key create revisions; filters out lesser create revisions.
|
||||
* Max_Create_Revision - the upper bound for key create revisions; filters out greater create revisions.
|
||||
|
||||
The client receives a `RangeResponse` message from the `Range` call:
|
||||
|
||||
```protobuf
|
||||
message RangeResponse {
|
||||
ResponseHeader header = 1;
|
||||
repeated mvccpb.KeyValue kvs = 2;
|
||||
bool more = 3;
|
||||
int64 count = 4;
|
||||
}
|
||||
```
|
||||
|
||||
* Kvs - the list of key-value pairs matched by the range request. When `Count_Only` is set, `Kvs` is empty.
|
||||
* More - indicates if there are more keys to return in the requested range if `limit` is set.
|
||||
* Count - the total number of keys satisfying the range request.
|
||||
|
||||
### Put
|
||||
|
||||
Keys are saved into the key-value store by issuing a `Put` call, which takes a `PutRequest`:
|
||||
|
||||
```protobuf
|
||||
message PutRequest {
|
||||
bytes key = 1;
|
||||
bytes value = 2;
|
||||
int64 lease = 3;
|
||||
bool prev_kv = 4;
|
||||
bool ignore_value = 5;
|
||||
bool ignore_lease = 6;
|
||||
}
|
||||
```
|
||||
|
||||
* Key - the name of the key to put into the key-value store.
|
||||
* Value - the value, in bytes, to associate with the key in the key-value store.
|
||||
* Lease - the lease ID to associate with the key in the key-value store. A lease value of 0 indicates no lease.
|
||||
* Prev_Kv - when set, responds with the key-value pair data before the update from this `Put` request.
|
||||
* Ignore_Value - when set, update the key without changing its current value. Returns an error if the key does not exist.
|
||||
* Ignore_Lease - when set, update the key without changing its current lease. Returns an error if the key does not exist.
|
||||
|
||||
The client receives a `PutResponse` message from the `Put` call:
|
||||
|
||||
```protobuf
|
||||
message PutResponse {
|
||||
ResponseHeader header = 1;
|
||||
mvccpb.KeyValue prev_kv = 2;
|
||||
}
|
||||
```
|
||||
|
||||
* Prev_Kv - the key-value pair overwritten by the `Put`, if `Prev_Kv` was set in the `PutRequest`.
|
||||
|
||||
### Delete Range
|
||||
|
||||
Ranges of keys are deleted using the `DeleteRange` call, which takes a `DeleteRangeRequest`:
|
||||
|
||||
```protobuf
|
||||
message DeleteRangeRequest {
|
||||
bytes key = 1;
|
||||
bytes range_end = 2;
|
||||
bool prev_kv = 3;
|
||||
}
|
||||
```
|
||||
|
||||
* Key, Range_End - The key range to delete.
|
||||
* Prev_Kv - when set, return the contents of the deleted key-value pairs.
|
||||
|
||||
The client receives a `DeleteRangeResponse` message from the `DeleteRange` call:
|
||||
|
||||
```protobuf
|
||||
message DeleteRangeResponse {
|
||||
ResponseHeader header = 1;
|
||||
int64 deleted = 2;
|
||||
repeated mvccpb.KeyValue prev_kvs = 3;
|
||||
}
|
||||
```
|
||||
|
||||
* Deleted - number of keys deleted.
|
||||
* Prev_Kv - a list of all key-value pairs deleted by the DeleteRange operation.
|
||||
|
||||
### Transaction
|
||||
|
||||
A transaction is an atomic If/Then/Else construct over the key-value store. It provides a primitive for grouping requests together in atomic blocks (i.e., then/else) whose execution is guarded (i.e., if) based on the contents of the key-value store. Transactions can be used for protecting keys from unintended concurrent updates, building compare-and-swap operations, and developing higher-level concurrency control.
|
||||
|
||||
A transaction can atomically process multiple requests in a single request. For modifications to the key-value store, this means the store's revision is incremented only once for the transaction and all events generated by the transaction will have the same revision. However, modifications to the same key multiple times within a single transaction are forbidden.
|
||||
|
||||
All transactions are guarded by a conjunction of comparisons, similar to an "If" statement. Each comparison checks a single key in the store. It may check for the absence or presence of a value, compare with a given value, or check a key's revision or version. Two different comparisons may apply to the same or different keys. All comparisons are applied atomically; if all comparisons are true, the transaction is said to succeed and etcd applies the transaction's then / `success` request block, otherwise it is said to fail and applies the else / `failure` request block.
|
||||
|
||||
Each comparison is encoded as a `Compare` message:
|
||||
|
||||
```protobuf
|
||||
message Compare {
|
||||
enum CompareResult {
|
||||
EQUAL = 0;
|
||||
GREATER = 1;
|
||||
LESS = 2;
|
||||
NOT_EQUAL = 3;
|
||||
}
|
||||
enum CompareTarget {
|
||||
VERSION = 0;
|
||||
CREATE = 1;
|
||||
MOD = 2;
|
||||
VALUE= 3;
|
||||
}
|
||||
CompareResult result = 1;
|
||||
// target is the key-value field to inspect for the comparison.
|
||||
CompareTarget target = 2;
|
||||
// key is the subject key for the comparison operation.
|
||||
bytes key = 3;
|
||||
oneof target_union {
|
||||
int64 version = 4;
|
||||
int64 create_revision = 5;
|
||||
int64 mod_revision = 6;
|
||||
bytes value = 7;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Result - the kind of logical comparison operation (e.g., equal, less than, etc).
|
||||
* Target - the key-value field to be compared. Either the key's version, create revision, modification revision, or value.
|
||||
* Key - the key for the comparison.
|
||||
* Target_Union - the user-specified data for the comparison.
|
||||
|
||||
After processing the comparison block, the transaction applies a block of requests. A block is a list of `RequestOp` messages:
|
||||
|
||||
```protobuf
|
||||
message RequestOp {
|
||||
// request is a union of request types accepted by a transaction.
|
||||
oneof request {
|
||||
RangeRequest request_range = 1;
|
||||
PutRequest request_put = 2;
|
||||
DeleteRangeRequest request_delete_range = 3;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Request_Range - a `RangeRequest`.
|
||||
* Request_Put - a `PutRequest`. The keys must be unique. It may not share keys with any other Puts or Deletes.
|
||||
* Request_Delete_Range - a `DeleteRangeRequest`. It may not share keys with any Puts or Deletes requests.
|
||||
|
||||
All together, a transaction is issued with a `Txn` API call, which takes a `TxnRequest`:
|
||||
|
||||
```protobuf
|
||||
message TxnRequest {
|
||||
repeated Compare compare = 1;
|
||||
repeated RequestOp success = 2;
|
||||
repeated RequestOp failure = 3;
|
||||
}
|
||||
```
|
||||
|
||||
* Compare - A list of predicates representing a conjunction of terms for guarding the transaction.
|
||||
* Success - A list of requests to process if all compare tests evaluate to true.
|
||||
* Failure - A list of requests to process if any compare test evaluates to false.
|
||||
|
||||
The client receives a `TxnResponse` message from the `Txn` call:
|
||||
|
||||
```protobuf
|
||||
message TxnResponse {
|
||||
ResponseHeader header = 1;
|
||||
bool succeeded = 2;
|
||||
repeated ResponseOp responses = 3;
|
||||
}
|
||||
```
|
||||
|
||||
* Succeeded - Whether `Compare` evaluated to true or false.
|
||||
* Responses - A list of responses corresponding to the results from applying the `Success` block if succeeded is true or the `Failure` if succeeded is false.
|
||||
|
||||
The `Responses` list corresponds to the results from the applied `RequestOp` list, with each response encoded as a `ResponseOp`:
|
||||
|
||||
```protobuf
|
||||
message ResponseOp {
|
||||
oneof response {
|
||||
RangeResponse response_range = 1;
|
||||
PutResponse response_put = 2;
|
||||
DeleteRangeResponse response_delete_range = 3;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Watch API
|
||||
|
||||
The Watch API provides an event-based interface for asynchronously monitoring changes to keys. An etcd3 watch waits for changes to keys by continuously watching from a given revision, either current or historical, and streams key updates back to the client.
|
||||
|
||||
### Events
|
||||
|
||||
Every change to every key is represented with `Event` messages. An `Event` message provides both the update's data and the type of update:
|
||||
|
||||
```protobuf
|
||||
message Event {
|
||||
enum EventType {
|
||||
PUT = 0;
|
||||
DELETE = 1;
|
||||
}
|
||||
EventType type = 1;
|
||||
KeyValue kv = 2;
|
||||
KeyValue prev_kv = 3;
|
||||
}
|
||||
```
|
||||
|
||||
* Type - The kind of event. A PUT type indicates new data has been stored to the key. A DELETE indicates the key was deleted.
|
||||
* KV - The KeyValue associated with the event. A PUT event contains current kv pair. A PUT event with kv.Version=1 indicates the creation of a key. A DELETE event contains the deleted key with its modification revision set to the revision of deletion.
|
||||
* Prev_KV - The key-value pair for the key from the revision immediately before the event. To save bandwidth, it is only filled out if the watch has explicitly enabled it.
|
||||
|
||||
### Watch streams
|
||||
|
||||
Watches are long-running requests and use gRPC streams to stream event data. A watch stream is bi-directional; the client writes to the stream to establish watches and reads to receive watch event. A single watch stream can multiplex many distinct watches by tagging events with per-watch identifiers. This multiplexing helps reducing the memory footprint and connection overhead on the core etcd cluster.
|
||||
|
||||
Watches make three guarantees about events:
|
||||
* Ordered - events are ordered by revision; an event will never appear on a watch if it precedes an event in time that has already been posted.
|
||||
* Reliable - a sequence of events will never drop any subsequence of events; if there are events ordered in time as a < b < c, then if the watch receives events a and c, it is guaranteed to receive b.
|
||||
* Atomic - a list of events is guaranteed to encompass complete revisions; updates in the same revision over multiple keys will not be split over several lists of events.
|
||||
|
||||
A client creates a watch by sending a `WatchCreateRequest` over a stream returned by `Watch`:
|
||||
|
||||
```protobuf
|
||||
message WatchCreateRequest {
|
||||
bytes key = 1;
|
||||
bytes range_end = 2;
|
||||
int64 start_revision = 3;
|
||||
bool progress_notify = 4;
|
||||
|
||||
enum FilterType {
|
||||
NOPUT = 0;
|
||||
NODELETE = 1;
|
||||
}
|
||||
repeated FilterType filters = 5;
|
||||
bool prev_kv = 6;
|
||||
}
|
||||
```
|
||||
|
||||
* Key, Range_End - The key range to watch.
|
||||
* Start_Revision - An optional revision for where to inclusively begin watching. If not given, it will stream events following the revision of the watch creation response header revision. The entire available event history can be watched starting from the last compaction revision.
|
||||
* Progress_Notify - When set, the watch will periodically receive a WatchResponse with no events, if there are no recent events. It is useful when clients wish to recover a disconnected watcher starting from a recent known revision. The etcd server decides how often to send notifications based on current server load.
|
||||
* Filters - A list of event types to filter away at server side.
|
||||
* Prev_Kv - When set, the watch receives the key-value data from before the event happens. This is useful for knowing what data has been overwritten.
|
||||
|
||||
In response to a `WatchCreateRequest` or if there is a new event for some established watch, the client receives a `WatchResponse`:
|
||||
|
||||
```protobuf
|
||||
message WatchResponse {
|
||||
ResponseHeader header = 1;
|
||||
int64 watch_id = 2;
|
||||
bool created = 3;
|
||||
bool canceled = 4;
|
||||
int64 compact_revision = 5;
|
||||
|
||||
repeated mvccpb.Event events = 11;
|
||||
}
|
||||
```
|
||||
|
||||
* Watch_ID - the ID of the watch that corresponds to the response.
|
||||
* Created - set to true if the response is for a create watch request. The client should record ID and expect to receive events for the watch on the stream. All events sent to the created watcher will have the same watch_id.
|
||||
* Canceled - set to true if the response is for a cancel watch request. No further events will be sent to the canceled watcher.
|
||||
* Compact_Revision - set to the minimum historical revision available to etcd if a watcher tries watching at a compacted revision. This happens when creating a watcher at a compacted revision or the watcher cannot catch up with the progress of the key-value store. The watcher will be canceled; creating new watches with the same start_revision will fail.
|
||||
* Events - a list of new events in sequence corresponding to the given watch ID.
|
||||
|
||||
If the client wishes to stop receiving events for a watch, it issues a `WatchCancelRequest`:
|
||||
|
||||
```protobuf
|
||||
message WatchCancelRequest {
|
||||
int64 watch_id = 1;
|
||||
}
|
||||
```
|
||||
|
||||
* Watch_ID - the ID of the watch to cancel so that no more events are transmitted.
|
||||
|
||||
## Lease API
|
||||
|
||||
Leases are a mechanism for detecting client liveness. The cluster grants leases with a time-to-live. A lease expires if the etcd cluster does not receive a keepAlive within a given TTL period.
|
||||
|
||||
To tie leases into the key-value store, each key may be attached to at most one lease. When a lease expires or is revoked, all keys attached to that lease will be deleted. Each expired key generates a delete event in the event history.
|
||||
|
||||
### Obtaining leases
|
||||
|
||||
Leases are obtained through the `LeaseGrant` API call, which takes a `LeaseGrantRequest`:
|
||||
|
||||
```protobuf
|
||||
message LeaseGrantRequest {
|
||||
int64 TTL = 1;
|
||||
int64 ID = 2;
|
||||
}
|
||||
```
|
||||
|
||||
* TTL - the advisory time-to-live, in seconds.
|
||||
* ID - the requested ID for the lease. If ID is set to 0, etcd will choose an ID.
|
||||
|
||||
The client receives a `LeaseGrantResponse` from the `LeaseGrant` call:
|
||||
|
||||
```protobuf
|
||||
message LeaseGrantResponse {
|
||||
ResponseHeader header = 1;
|
||||
int64 ID = 2;
|
||||
int64 TTL = 3;
|
||||
}
|
||||
```
|
||||
|
||||
* ID - the lease ID for the granted lease.
|
||||
* TTL - is the server selected time-to-live, in seconds, for the lease.
|
||||
|
||||
```protobuf
|
||||
message LeaseRevokeRequest {
|
||||
int64 ID = 1;
|
||||
}
|
||||
```
|
||||
|
||||
* ID - the lease ID to revoke. When the lease is revoked, all attached keys are deleted.
|
||||
|
||||
### Keep alives
|
||||
|
||||
Leases are refreshed using a bi-directional stream created with the `LeaseKeepAlive` API call. When the client wishes to refresh a lease, it sends a `LeaseGrantRequest` over the stream:
|
||||
|
||||
```protobuf
|
||||
message LeaseKeepAliveRequest {
|
||||
int64 ID = 1;
|
||||
}
|
||||
```
|
||||
|
||||
* ID - the lease ID for the lease to keep alive.
|
||||
|
||||
The keep alive stream responds with a `LeaseKeepAliveResponse`:
|
||||
|
||||
```protobuf
|
||||
message LeaseKeepAliveResponse {
|
||||
ResponseHeader header = 1;
|
||||
int64 ID = 2;
|
||||
int64 TTL = 3;
|
||||
}
|
||||
```
|
||||
|
||||
* ID - the lease that was refreshed with a new TTL.
|
||||
* TTL - the new time-to-live, in seconds, that the lease has remaining.
|
||||
|
||||
[elections]: https://github.com/coreos/etcd/blob/master/clientv3/concurrency/election.go
|
||||
[kv-proto]: https://github.com/coreos/etcd/blob/master/mvcc/mvccpb/kv.proto
|
||||
[kv-service]: https://github.com/coreos/etcd/blob/master/etcdserver/etcdserverpb/rpc.proto
|
||||
[response_header]: https://github.com/coreos/etcd/blob/master/etcdserver/etcdserverpb/rpc.proto
|
||||
[grpc-api]: ../dev-guide/api_reference_v3.md
|
||||
[grpc-service]: https://github.com/coreos/etcd/blob/master/etcdserver/etcdserverpb/rpc.proto
|
||||
[locks]: https://github.com/coreos/etcd/blob/master/clientv3/concurrency/mutex.go
|
||||
[mvcc]: https://en.wikipedia.org/wiki/Multiversion_concurrency_control
|
||||
[stm]: https://github.com/coreos/etcd/blob/master/clientv3/concurrency/stm.go
|
||||
|
@ -1,6 +1,6 @@
|
||||
# KV API guarantees
|
||||
|
||||
etcd is a consistent and durable key value store with mini-transaction(TODO: link to txn doc when we have it) support. The key value store is exposed through the KV APIs. etcd tries to ensure the strongest consistency and durability guarantees for a distributed system. This specification enumerates the KV API guarantees made by etcd.
|
||||
etcd is a consistent and durable key value store with [mini-transaction][txn] support. The key value store is exposed through the KV APIs. etcd tries to ensure the strongest consistency and durability guarantees for a distributed system. This specification enumerates the KV API guarantees made by etcd.
|
||||
|
||||
### APIs to consider
|
||||
|
||||
@ -21,7 +21,7 @@ An etcd operation is considered complete when it is committed through consensus,
|
||||
|
||||
#### Revision
|
||||
|
||||
An etcd operation that modifies the key value store is assigned with a single increasing revision. A transaction operation might modifies the key value store multiple times, but only one revision is assigned. The revision attribute of a key value pair that modified by the operation has the same value as the revision of the operation. The revision can be used as a logical clock for key value store. A key value pair that has a larger revision is modified after a key value pair with a smaller revision. Two key value pairs that have the same revision are modified by an operation "concurrently".
|
||||
An etcd operation that modifies the key value store is assigned a single increasing revision. A transaction operation might modify the key value store multiple times, but only one revision is assigned. The revision attribute of a key value pair that was modified by the operation has the same value as the revision of the operation. The revision can be used as a logical clock for key value store. A key value pair that has a larger revision is modified after a key value pair with a smaller revision. Two key value pairs that have the same revision are modified by an operation "concurrently".
|
||||
|
||||
### Guarantees provided
|
||||
|
||||
@ -61,3 +61,4 @@ etcd ensures linearizability for all other operations by default. Linearizabilit
|
||||
[strict_consistency]: https://en.wikipedia.org/wiki/Consistency_model#Strict_consistency
|
||||
[serializable_isolation]: https://en.wikipedia.org/wiki/Isolation_(database_systems)#Serializable
|
||||
[Linearizability]: #Linearizability
|
||||
[txn]: api.md#transactions
|
||||
|
77
Documentation/learning/auth_design.md
Normal file
77
Documentation/learning/auth_design.md
Normal file
@ -0,0 +1,77 @@
|
||||
# etcd v3 authentication design
|
||||
|
||||
## Why not reuse the v2 auth system?
|
||||
|
||||
The v3 protocol uses gRPC as its transport instead of a RESTful interface like v2. This new protocol provides an opportunity to iterate on and improve the v2 design. For example, v3 auth has connection based authentication, rather than v2's slower per-request authentication. Additionally, v2 auth's semantics tend to be unwieldy in practice with respect to reasoning about consistency, which will be described in the next sections. For v3, there is a well-defined description and implementation of the authentication mechanism which fixes the deficiencies in the v2 auth system.
|
||||
|
||||
### Functionality requirements
|
||||
|
||||
* Per connection authentication, not per request
|
||||
* User ID + password based authentication implemented for the gRPC API
|
||||
* Authentication must be refreshed after auth policy changes
|
||||
* Its functionality should be as simple and useful as v2
|
||||
* v3 provides a flat key space, unlike the directory structure of v2. Permission checking will be provided as interval matching.
|
||||
* It should have stronger consistency guarantees than v2 auth
|
||||
|
||||
### Main required changes
|
||||
|
||||
* A client must create a dedicated connection only for authentication before sending authenticated requests
|
||||
* Add permission information (user ID and authorized revision) to the Raft commands (`etcdserverpb.InternalRaftRequest`)
|
||||
* Every request is permission checked in the state machine layer, rather than API layer
|
||||
|
||||
### Permission metadata consistency
|
||||
|
||||
The metadata for auth should also be stored and managed in the storage controlled by etcd's Raft protocol like other data stored in etcd. It is required for not sacrificing availability and consistency of the entire etcd cluster. If reading or writing the metadata (e.g. permission information) needs an agreement of every node (more than quorum), single node failure can stop the entire cluster. Requiring all nodes to agree at once means that checking ordinary read/write requests cannot be completed if any cluster member is down, even if the cluster has an available quorum. This unanimous scheme ultimately degrades cluster availability; quorum based consensus from raft should suffice since agreement follows from consistent ordering.
|
||||
|
||||
The authentication mechanism in the etcd v2 protocol has a tricky part because the metadata consistency should work as in the above, but does not: each permission check is processed by the etcd member that receives the client request (etcdserver/api/v2http/client.go), including follower members. Therefore, it's possible the check may be based on stale metadata.
|
||||
|
||||
|
||||
This staleness means that auth configuration cannot be reflected as soon as operators execute etcdctl. Therefore there is no way to know how long the stale metadata is active. Practically, the configuration change is reflected immediately after the command execution. However, in some cases of heavy load, the inconsistent state can be prolonged and it might result in counter-intuitive situations for users and developers. It requires a workaround like this: https://github.com/coreos/etcd/pull/4317#issuecomment-179037582
|
||||
|
||||
### Inconsistent permissions are unsafe for linearized requests
|
||||
|
||||
Inconsistent authentication state is most serious for writes. Even if an operator disables write on a user, if the write is only ordered with respect to the key value store but not the authentication system, it's possible the write will complete successfully. Without ordering on both the auth store and the key-value store, the system will be susceptible to stale permission attacks.
|
||||
|
||||
Therefore, the permission checking logic should be added to the state machine of etcd. Each state machine should check the requests based on its permission information in the apply phase (so the auth information must not be stale).
|
||||
|
||||
## Design and implementation
|
||||
|
||||
### Authentication
|
||||
|
||||
At first, a client must create a gRPC connection only to authenticate its user ID and password. An etcd server will respond with an authentication reply. The reponse will be an authentication token on success or an error on failure. The client can use its authentication token to present its credentials to etcd when making API requests.
|
||||
|
||||
The client connection used to request the authentication token is typically thrown away; it cannot carry the new token's credentials. This is because gRPC doesn't provide a way for adding per RPC credential after creation of the connection (calling `grpc.Dial()`). Therefore, a client cannot assign a token to its connection that is obtained through the connection. The client needs a new connection for using the token.
|
||||
|
||||
#### Notes on the implementation of `Authenticate()` RPC
|
||||
|
||||
`Authenticate()` RPC generates an authentication token based on a given user name and password. etcd saves and checks a configured password and a given password using Go's `bcrypt` package. By design, `bcrypt`'s password checking mechanism is computationally expensive, taking nearly 100ms on an ordinary x64 server. Therefore, performing this check in the state machine apply phase would cause performance trouble: the entire etcd cluster can only serve almost 10 `Authenticate()` requests per second.
|
||||
|
||||
For good performance, the v3 auth mechanism checks passwords in etcd's API layer, where it can be parallelized outside of raft. However, this can lead to potential time-of-check/time-of-use (TOCTOU) permission lapses:
|
||||
1. client A sends a request `Authenticate()`
|
||||
1. the API layer processes the password checking part of `Authenticate()`
|
||||
1. another client B sends a request of `ChangePassword()` and the server completes it
|
||||
1. the state machine layer processes the part of getting a revision number for the `Authenticate()` from A
|
||||
1. the server returns a success to A
|
||||
1. now A is authenticated on an obsolete password
|
||||
|
||||
For avoiding such a situation, the API layer performs *version number validation* based on the revision number of the auth store. During password checking, the API layer saves the revision number of auth store. After successful password checking, the API layer compares the saved revision number and the latest revision number. If the numbers differ, it means someone else updated the auth metadata. So it retries the checking. With this mechanism, the successful password checking based on the obsolete password can be avoided.
|
||||
|
||||
### Resolving a token in the API layer
|
||||
|
||||
After authenticating with `Authenticate()`, a client can create a gRPC connection as it would without auth. In addition to the existing initialization process, the client must associate the token with the newly created connection. `grpc.WithPerRPCCredentials()` provides the functionality for this purpose.
|
||||
|
||||
Every authenticated request from the client has a token. The token can be obtained with `grpc.metadata.FromContext()` in the server side. The server can obtain who is issuing the request and when the user was authorized. The information will be filled by the API layer in the header (`etcdserverpb.RequestHeader.Username` and `etcdserverpb.RequestHeader.AuthRevision`) of a raft log entry (`etcdserverpb.InternalRaftRequest`).
|
||||
|
||||
### Checking permission in the state machine
|
||||
|
||||
The auth info in `etcdserverpb.RequestHeader` is checked in the apply phase of the state machine. This step checks the user is granted permission to requested keys on the latest revision of auth store.
|
||||
|
||||
### Two types of tokens: simple and JWT
|
||||
|
||||
There are two kinds of token types: simple and JWT. The simple token isn't designed for production use cases. Its tokens aren't cryptographically signed and servers must statefully track token-user correspondence; it is meant for development testing. JWT tokens should be used for production deployments since it is cryptographically signed and verified. From the implementation perspective, JWT is stateless. Its token can include metadata including username and revision, so servers don't need to remember correspondence between tokens and the metadata.
|
||||
|
||||
## Notes on the difference between KVS models and file system models
|
||||
|
||||
etcd v3 is a KVS, not a file system. So the permissions can be granted to the users in form of an exact key name or a key range like `["start key", "end key")`. It means that granting a permission of a nonexistent key is possible. Users should care about unintended permission granting. In a case of file system like system (e.g. Chubby or ZooKeeper), an inode like data structure can include the permission information. So granting permission to a nonexist key won't be possible (except the case of sticky bits).
|
||||
|
||||
The etcd v3 model requires multiple lookup of the metadata unlike the file system like systems. The worst case lookup cost will be sum the user's total granted keys and intervals. The cost cannot be avoided because v3's flat key space is completely different from Unix's file system model (every inode includes permission metadata). Practically the cost won’t be a serious problem because the metadata is small enough to benefit from caching.
|
@ -2,19 +2,114 @@
|
||||
|
||||
The name "etcd" originated from two ideas, the unix "/etc" folder and "d"istibuted systems. The "/etc" folder is a place to store configuration data for a single system whereas etcd stores configuration information for large scale distributed systems. Hence, a "d"istributed "/etc" is "etcd".
|
||||
|
||||
etcd stores metadata in a consistent and fault-tolerant way. Distributed systems use etcd as a consistent key-value store for configuration management, service discovery, and coordinating distributed work. Common distributed patterns using etcd include leader election, [distributed locks][etcd-concurrency], and monitoring machine liveness.
|
||||
etcd stores metadata in a consistent and fault-tolerant way. Distributed systems use etcd as a consistent key-value store for configuration management, service discovery, and coordinating distributed work. Common distributed patterns using etcd include [leader election][etcd-etcdctl-elect], [distributed locks][etcd-etcdctl-lock], and monitoring machine liveness.
|
||||
|
||||
## Use cases
|
||||
|
||||
- Container Linux by CoreOS: Application running on [Container Linux][container-linux] gets automatic, zero-downtime Linux kernel updates. Container Linux uses [locksmith] to coordinate updates. locksmith implements a distributed semaphore over etcd to ensure only a subset of a cluster is rebooting at any given time.
|
||||
- [Kubernetes][kubernetes] stores configuration data into etcd for service discovery and cluster management; etcd's consistency is crucial for correctly scheduling and operating services. The Kubernetes API server persists cluster state into etcd. It uses etcd's watch API to monitor the cluster and roll out critical configuration changes.
|
||||
|
||||
## etcd versus other key-value stores
|
||||
|
||||
## Features and system comparisons
|
||||
When deciding whether to use etcd as a key-value store, it’s worth keeping in mind etcd’s main goal. Namely, etcd is designed as a general substrate for large scale distributed systems. These are systems that will never tolerate split-brain operation and are willing to sacrifice availability to achieve this end. An etcd cluster is meant to provide consistent key-value storage with best of class stability, reliability, scalability and performance. The upshot of this focus is many [organizations][production-users] already use etcd to implement production systems such as container schedulers, service discovery services, distributed data storage, and more.
|
||||
|
||||
TODO
|
||||
Perhaps etcd already seems like a good fit, but as with all technological decisions, proceed with caution. Please note this documentation is written by the etcd team. Although the ideal is a disinterested comparison of technology and features, the authors’ expertise and biases obviously favor etcd. Use only as directed.
|
||||
|
||||
[etcd-concurrency]: https://godoc.org/github.com/coreos/etcd/clientv3/concurrency
|
||||
The table below is a handy quick reference for spotting the differences among etcd and its most popular alternatives at a glance. Further commentary and details for each column are in the sections following the table.
|
||||
|
||||
| | etcd | ZooKeeper | Consul | NewSQL (Cloud Spanner, CockroachDB, TiDB) |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Concurrency Primitives | [Lock RPCs][etcd-v3lock], [Election RPCs][etcd-v3election], [command line locks][etcd-etcdctl-lock], [command line elections][etcd-etcdctl-elect], [recipes][etcd-recipe] in go | External [curator recipes][curator] in Java | [Native lock API][consul-lock] | [Rare][newsql-leader], if any |
|
||||
| Linearizable Reads | [Yes][etcd-linread] | No | [Yes][consul-linread] | Sometimes |
|
||||
| Multi-version Concurrency Control | [Yes][etcd-mvcc] | No | No | Sometimes |
|
||||
| Transactions | [Field compares, Read, Write][etcd-txn] | [Version checks, Write][zk-txn] | [Field compare, Lock, Read, Write][consul-txn] | SQL-style |
|
||||
| Change Notification | [Historical and current key intervals][etcd-watch] | [Current keys and directories][zk-watch] | [Current keys and prefixes][consul-watch] | Triggers (sometimes) |
|
||||
| User permissions | [Role based][etcd-rbac] | [ACLs][zk-acl] | [ACLs][consul-acl] | Varies (per-table [GRANT][cockroach-grant], per-database [roles][spanner-roles]) |
|
||||
| HTTP/JSON API | [Yes][etcd-json] | No | [Yes][consul-json] | Rarely |
|
||||
| Membership Reconfiguration | [Yes][etcd-reconfig] | [>3.5.0][zk-reconfig] | [Yes][consul-reconfig] | Yes |
|
||||
| Maximum reliable database size | Several gigabytes | Hundreds of megabytes (sometimes several gigabytes) | Hundreds of MBs | Terabytes+ |
|
||||
| Minimum read linearization latency | Network RTT | No read linearization | RTT + fsync | Clock barriers (atomic, NTP) |
|
||||
|
||||
### ZooKeeper
|
||||
|
||||
ZooKeeper solves the same problem as etcd: distributed system coordination and metadata storage. However, etcd has the luxury of hindsight taken from engineering and operational experience with ZooKeeper’s design and implementation. The lessons learned from Zookeeper certainly informed etcd’s design, helping it support large scale systems like Kubernetes. The improvements etcd made over Zookeeper include:
|
||||
|
||||
* Dynamic cluster membership reconfiguration
|
||||
* Stable read/write under high load
|
||||
* A multi-version concurrency control data model
|
||||
* Reliable key monitoring which never silently drop events
|
||||
* Lease primitives decoupling connections from sessions
|
||||
* APIs for safe distributed shared locks
|
||||
|
||||
Furthermore, etcd supports a wide range of languages and frameworks out of the box. Whereas Zookeeper has its own custom Jute RPC protocol, which is totally unique to Zookeeper and limits its [supported language bindings][zk-bindings], etcd’s client protocol is built from [gRPC][grpc], a popular RPC framework with language bindings for go, C++, Java, and more. Likewise, gRPC can be serialized into JSON over HTTP, so even general command line utilities like `curl` can talk to it. Since systems can select from a variety of choices, they are built on etcd with native tooling rather than around etcd with a single fixed set of technologies.
|
||||
|
||||
When considering features, support, and stability, new applications planning to use Zookeeper for a consistent key value store would do well to choose etcd instead.
|
||||
|
||||
### Consul
|
||||
|
||||
Consul bills itself as an end-to-end service discovery framework. To wit, it includes services such as health checking, failure detection, and DNS. Incidentally, Consul also exposes a key value store with mediocre performance and an intricate API. As it stands in Consul 0.7, the storage system does not scales well; systems requiring millions of keys will suffer from high latencies and memory pressure. The key value API is missing, most notably, multi-version keys, conditional transactions, and reliable streaming watches.
|
||||
|
||||
etcd and Consul solve different problems. If looking for a distributed consistent key value store, etcd is a better choice over Consul. If looking for end-to-end cluster service discovery, etcd will not have enough features; choose Kubernetes, Consul, or SmartStack.
|
||||
|
||||
### NewSQL (Cloud Spanner, CockroachDB, TiDB)
|
||||
|
||||
Both etcd and NewSQL databases (e.g., [Cockroach][cockroach], [TiDB][tidb], [Google Spanner][spanner]) provide strong data consistency guarantees with high availability. However, the significantly different system design parameters lead to significantly different client APIs and performance characteristics.
|
||||
|
||||
NewSQL databases are meant to horizontally scale across data centers. These systems typically partition data across multiple consistent replication groups (shards), potentially distant, storing data sets on the order of terabytes and above. This sort of scaling makes them poor candidates for distributed coordination as they have long latencies from waiting on clocks and expect updates with mostly localized dependency graphs. The data is organized into tables, including SQL-style query facilities with richer semantics than etcd, but at the cost of additional complexity for processing, planning, and optimizing queries.
|
||||
|
||||
In short, choose etcd for storing metadata or coordinating distributed applications. If storing more than a few GB of data or if full SQL queries are needed, choose a NewSQL database.
|
||||
|
||||
## Using etcd for metadata
|
||||
|
||||
etcd replicates all data within a single consistent replication group. For storing up to a few GB of data with consistent ordering, this is the most efficient approach. Each modification of cluster state, which may change multiple keys, is assigned a global unique ID, called a revision in etcd, from a monotonically increasing counter for reasoning over ordering. Since there’s only a single replication group, the modification request only needs to go through the raft protocol to commit. By limiting consensus to one replication group, etcd gets distributed consistency with a simple protocol while achieving low latency and high throughput.
|
||||
|
||||
The replication behind etcd cannot horizontally scale because it lacks data sharding. In contrast, NewSQL databases usually shard data across multiple consistent replication groups, storing data sets on the order of terabytes and above. However, to assign each modification a global unique and increasing ID, each request must go through an additional coordination protocol among replication groups. This extra coordination step may potentially conflict on the global ID, forcing ordered requests to retry. The result is a more complicated approach with typically worse performance than etcd for strict ordering.
|
||||
|
||||
If an application reasons primarily about metadata or metadata ordering, such as to coordinate processes, choose etcd. If the application needs a large data store spanning multiple data centers and does not heavily depend on strong global ordering properties, choose a NewSQL database.
|
||||
|
||||
## Using etcd for distributed coordination
|
||||
|
||||
etcd has distributed coordination primitives such as event watches, leases, elections, and distributed shared locks out of the box. These primitives are both maintained and supported by the etcd developers; leaving these primitives to external libraries shirks the responsibility of developing foundational distributed software, essentially leaving the system incomplete. NewSQL databases usually expect these distributed coordination primitives to be authored by third parties. Likewise, ZooKeeper famously has a separate and independent [library][curator] of coordination recipes. Consul, which provides a native locking API, goes so far as to apologize that it’s “[not a bulletproof method][consul-bulletproof]”.
|
||||
|
||||
In theory, it’s possible to build these primitives atop any storage systems providing strong consistency. However, the algorithms tend to be subtle; it is easy to develop a locking algorithm that appears to work, only to suddenly break due to thundering herd and timing skew. Furthermore, other primitives supported by etcd, such as transactional memory depend on etcd’s MVCC data model; simple strong consistency is not enough.
|
||||
|
||||
For distributed coordination, choosing etcd can help prevent operational headaches and save engineering effort.
|
||||
|
||||
[production-users]: ../production-users.md
|
||||
[grpc]: http://www.grpc.io
|
||||
[consul-bulletproof]: https://www.consul.io/docs/internals/sessions.html
|
||||
[curator]: http://curator.apache.org/
|
||||
[cockroach]: https://github.com/cockroachdb/cockroach
|
||||
[spanner]: https://cloud.google.com/spanner/
|
||||
[tidb]: https://github.com/pingcap/tidb
|
||||
[etcd-v3lock]: https://godoc.org/github.com/coreos/etcd/etcdserver/api/v3lock/v3lockpb
|
||||
[etcd-v3election]: https://godoc.org/github.com/coreos/etcd/etcdserver/api/v3election/v3electionpb
|
||||
[etcd-etcdctl-lock]: ../../etcdctl/README.md#lock-lockname
|
||||
[etcd-etcdctl-elect]: ../../etcdctl/README.md#elect-options-election-name-proposal
|
||||
[etcd-mvcc]: data_model.md
|
||||
[etcd-recipe]: https://godoc.org/github.com/coreos/etcd/contrib/recipes
|
||||
[consul-lock]: https://www.consul.io/docs/commands/lock.html
|
||||
[newsql-leader]: http://dl.acm.org/citation.cfm?id=2960999
|
||||
[etcd-reconfig]: ../op-guide/runtime-configuration.md
|
||||
[zk-reconfig]: https://zookeeper.apache.org/doc/trunk/zookeeperReconfig.html
|
||||
[consul-reconfig]: https://www.consul.io/docs/guides/servers.html
|
||||
[etcd-linread]: api_guarantees.md#linearizability
|
||||
[consul-linread]: https://www.consul.io/docs/agent/http.html#consistency
|
||||
[etcd-json]: ../dev-guide/api_grpc_gateway.md
|
||||
[consul-json]: https://www.consul.io/docs/agent/http.html#formatted-json-output
|
||||
[etcd-txn]: api.md#transaction
|
||||
[zk-txn]: https://zookeeper.apache.org/doc/r3.4.3/api/org/apache/zookeeper/ZooKeeper.html#multi(java.lang.Iterable)
|
||||
[consul-txn]: https://www.consul.io/docs/agent/http/kv.html#txn
|
||||
[etcd-watch]: api.md#watch-streams
|
||||
[zk-watch]: https://zookeeper.apache.org/doc/trunk/zookeeperProgrammers.html#ch_zkWatches
|
||||
[consul-watch]: https://www.consul.io/docs/agent/watches.html
|
||||
[etcd-commonname]: ../op-guide/authentication.md#using-tls-common-name
|
||||
[etcd-rbac]: ../op-guide/authentication.md#working-with-roles
|
||||
[zk-acl]: https://zookeeper.apache.org/doc/r3.1.2/zookeeperProgrammers.html#sc_ZooKeeperAccessControl
|
||||
[consul-acl]: https://www.consul.io/docs/internals/acl.html
|
||||
[cockroach-grant]: https://www.cockroachlabs.com/docs/grant.html
|
||||
[spanner-roles]: https://cloud.google.com/spanner/docs/iam#roles
|
||||
[zk-bindings]: https://zookeeper.apache.org/doc/r3.1.2/zookeeperProgrammers.html#ch_bindings
|
||||
[container-linux]: https://coreos.com/why
|
||||
[locksmith]: https://github.com/coreos/locksmith
|
||||
[kubernetes]: http://kubernetes.io/docs/whatisk8s
|
||||
|
164
Documentation/op-guide/authentication.md
Normal file
164
Documentation/op-guide/authentication.md
Normal file
@ -0,0 +1,164 @@
|
||||
# Authentication Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Authentication was added in etcd 2.1. The etcd v3 API slightly modified the authentication feature's API and user interface to better fit the new data model. This guide is intended to help users set up basic authentication in etcd v3.
|
||||
|
||||
## Special users and roles
|
||||
|
||||
There is one special user, `root`, and one special role, `root`.
|
||||
|
||||
### User `root`
|
||||
|
||||
The `root` user, which has full access to etcd, must be created before activating authentication. The idea behind the `root` user is for administrative purposes: managing roles and ordinary users. The `root` user must have the `root` role and is allowed to change anything inside etcd.
|
||||
|
||||
### Role `root`
|
||||
|
||||
The role `root` may be granted to any user, in addition to the root user. A user with the `root` role has both global read-write access and permission to update the cluster's authentication configuration. Furthermore, the `root` role grants privileges for general cluster maintenance, including modifying cluster membership, defragmenting the store, and taking snapshots.
|
||||
|
||||
## Working with users
|
||||
|
||||
The `user` subcommand for `etcdctl` handles all things having to do with user accounts.
|
||||
|
||||
A listing of users can be found with:
|
||||
|
||||
```
|
||||
$ etcdctl user list
|
||||
```
|
||||
|
||||
Creating a user is as easy as
|
||||
|
||||
```
|
||||
$ etcdctl user add myusername
|
||||
```
|
||||
|
||||
Creating a new user will prompt for a new password. The password can be supplied from standard input when an option `--interactive=false` is given.
|
||||
|
||||
Roles can be granted and revoked for a user with:
|
||||
|
||||
```
|
||||
$ etcdctl user grant-role myusername foo
|
||||
$ etcdctl user revoke-role myusername bar
|
||||
```
|
||||
|
||||
The user's settings can be inspected with:
|
||||
|
||||
```
|
||||
$ etcdctl user get myusername
|
||||
```
|
||||
|
||||
And the password for a user can be changed with
|
||||
|
||||
```
|
||||
$ etcdctl user passwd myusername
|
||||
```
|
||||
|
||||
Changing the password will prompt again for a new password. The password can be supplied from standard input when an option `--interactive=false` is given.
|
||||
|
||||
Delete an account with:
|
||||
```
|
||||
$ etcdctl user delete myusername
|
||||
```
|
||||
|
||||
|
||||
## Working with roles
|
||||
|
||||
The `role` subcommand for `etcdctl` handles all things having to do with access controls for particular roles, as were granted to individual users.
|
||||
|
||||
List roles with:
|
||||
|
||||
```
|
||||
$ etcdctl role list
|
||||
```
|
||||
|
||||
Create a new role with:
|
||||
|
||||
```
|
||||
$ etcdctl role add myrolename
|
||||
```
|
||||
|
||||
A role has no password; it merely defines a new set of access rights.
|
||||
|
||||
Roles are granted access to a single key or a range of keys.
|
||||
|
||||
The range can be specified as an interval [start-key, end-key) where start-key should be lexically less than end-key in an alphabetical manner.
|
||||
|
||||
Access can be granted as either read, write, or both, as in the following examples:
|
||||
|
||||
```
|
||||
# Give read access to a key /foo
|
||||
$ etcdctl role grant-permission myrolename read /foo
|
||||
|
||||
# Give read access to keys with a prefix /foo/. The prefix is equal to the range [/foo/, /foo0)
|
||||
$ etcdctl role grant-permission myrolename --prefix=true read /foo/
|
||||
|
||||
# Give write-only access to the key at /foo/bar
|
||||
$ etcdctl role grant-permission myrolename write /foo/bar
|
||||
|
||||
# Give full access to keys in a range of [key1, key5)
|
||||
$ etcdctl role grant-permission myrolename readwrite key1 key5
|
||||
|
||||
# Give full access to keys with a prefix /pub/
|
||||
$ etcdctl role grant-permission myrolename --prefix=true readwrite /pub/
|
||||
```
|
||||
|
||||
To see what's granted, we can look at the role at any time:
|
||||
|
||||
```
|
||||
$ etcdctl role get myrolename
|
||||
```
|
||||
|
||||
Revocation of permissions is done the same logical way:
|
||||
|
||||
```
|
||||
$ etcdctl role revoke-permission myrolename /foo/bar
|
||||
```
|
||||
|
||||
As is removing a role entirely:
|
||||
|
||||
```
|
||||
$ etcdctl role remove myrolename
|
||||
```
|
||||
|
||||
## Enabling authentication
|
||||
|
||||
The minimal steps to enabling auth are as follows. The administrator can set up users and roles before or after enabling authentication, as a matter of preference.
|
||||
|
||||
Make sure the root user is created:
|
||||
|
||||
```
|
||||
$ etcdctl user add root
|
||||
Password of root:
|
||||
```
|
||||
|
||||
Enable authentication:
|
||||
|
||||
```
|
||||
$ etcdctl auth enable
|
||||
```
|
||||
|
||||
After this, etcd is running with authentication enabled. To disable it for any reason, use the reciprocal command:
|
||||
|
||||
```
|
||||
$ etcdctl --user root:rootpw auth disable
|
||||
```
|
||||
|
||||
## Using `etcdctl` to authenticate
|
||||
|
||||
`etcdctl` supports a similar flag as `curl` for authentication.
|
||||
|
||||
```
|
||||
$ etcdctl --user user:password get foo
|
||||
```
|
||||
|
||||
The password can be taken from a prompt:
|
||||
|
||||
```
|
||||
$ etcdctl --user user get foo
|
||||
```
|
||||
|
||||
Otherwise, all `etcdctl` commands remain the same. Users and roles can still be created and modified, but require authentication by a user with the root role.
|
||||
|
||||
## Using TLS Common Name
|
||||
|
||||
If an etcd server is launched with the option `--client-cert-auth=true`, the field of Common Name (CN) in the client's TLS cert will be used as an etcd user. In this case, the common name authenticates the user and the client does not need a password.
|
@ -458,7 +458,7 @@ $ etcd --name infra2 \
|
||||
|
||||
### Gateway
|
||||
|
||||
etcd gateway is a simple TCP proxy that forwards network data to the etcd cluster. Please read [gateway guide] for more information.
|
||||
etcd gateway is a simple TCP proxy that forwards network data to the etcd cluster. Please read [gateway guide][gateway] for more information.
|
||||
|
||||
### Proxy
|
||||
|
||||
@ -475,5 +475,5 @@ To setup an etcd cluster with proxies of v2 API, please read the the [clustering
|
||||
[proxy]: https://github.com/coreos/etcd/blob/release-2.3/Documentation/proxy.md
|
||||
[clustering_etcd2]: https://github.com/coreos/etcd/blob/release-2.3/Documentation/clustering.md
|
||||
[security-guide]: security.md
|
||||
[tls-setup]: /hack/tls-setup
|
||||
[tls-setup]: ../../hack/tls-setup
|
||||
[gateway]: gateway.md
|
||||
|
@ -28,7 +28,7 @@ To start etcd automatically using custom settings at startup in Linux, using a [
|
||||
|
||||
### --snapshot-count
|
||||
+ Number of committed transactions to trigger a snapshot to disk.
|
||||
+ default: "10000"
|
||||
+ default: "100000"
|
||||
+ env variable: ETCD_SNAPSHOT_COUNT
|
||||
|
||||
### --heartbeat-interval
|
||||
@ -140,6 +140,12 @@ To start etcd automatically using custom settings at startup in Linux, using a [
|
||||
+ default: 0
|
||||
+ env variable: ETCD_AUTO_COMPACTION_RETENTION
|
||||
|
||||
|
||||
### --enable-v2
|
||||
+ Accept etcd V2 client requests
|
||||
+ default: true
|
||||
+ env variable: ETCD_ENABLE_V2
|
||||
|
||||
## Proxy flags
|
||||
|
||||
`--proxy` prefix flags configures etcd to run in [proxy mode][proxy]. "proxy" supports v2 API only.
|
||||
@ -179,7 +185,10 @@ To start etcd automatically using custom settings at startup in Linux, using a [
|
||||
|
||||
The security flags help to [build a secure etcd cluster][security].
|
||||
|
||||
### --ca-file [DEPRECATED]
|
||||
### --ca-file
|
||||
|
||||
**DEPRECATED**
|
||||
|
||||
+ Path to the client server TLS CA file. `--ca-file ca.crt` could be replaced by `--trusted-ca-file ca.crt --client-cert-auth` and etcd will perform the same.
|
||||
+ default: none
|
||||
+ env variable: ETCD_CA_FILE
|
||||
@ -209,7 +218,10 @@ The security flags help to [build a secure etcd cluster][security].
|
||||
+ default: false
|
||||
+ env variable: ETCD_AUTO_TLS
|
||||
|
||||
### --peer-ca-file [DEPRECATED]
|
||||
### --peer-ca-file
|
||||
|
||||
**DEPRECATED**
|
||||
|
||||
+ Path to the peer server TLS CA file. `--peer-ca-file ca.crt` could be replaced by `--peer-trusted-ca-file ca.crt --peer-client-cert-auth` and etcd will perform the same.
|
||||
+ default: none
|
||||
+ env variable: ETCD_PEER_CA_FILE
|
||||
@ -283,10 +295,17 @@ Follow the instructions when using these flags.
|
||||
+ Set level of detail for exported metrics, specify 'extensive' to include histogram metrics.
|
||||
+ default: basic
|
||||
|
||||
## Auth flags
|
||||
|
||||
### --auth-token
|
||||
+ Specify a token type and token specific options, especially for JWT. Its format is "type,var1=val1,var2=val2,...". Possible type is 'simple' or 'jwt'. Possible variables are 'sign-method' for specifying a sign method of jwt (its possible values are 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'PS256', 'PS384', or 'PS512'), 'pub-key' for specifying a path to a public key for verifying jwt, and 'priv-key' for specifying a path to a private key for signing jwt.
|
||||
+ Example option of JWT: '--auth-token jwt,pub-key=app.rsa.pub,priv-key=app.rsa,sign-method=RS512'
|
||||
+ default: "simple"
|
||||
|
||||
[build-cluster]: clustering.md#static
|
||||
[reconfig]: runtime-configuration.md
|
||||
[discovery]: clustering.md#discovery
|
||||
[iana-ports]: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=etcd
|
||||
[iana-ports]: http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.txt
|
||||
[proxy]: ../v2/proxy.md
|
||||
[restore]: ../v2/admin_guide.md#restoring-a-backup
|
||||
[security]: security.md
|
||||
|
@ -21,10 +21,10 @@ sudo rkt trust --prefix coreos.com/etcd
|
||||
# gpg key fingerprint is: 18AD 5014 C99E F7E3 BA5F 6CE9 50BD D3E0 FC8A 365E
|
||||
```
|
||||
|
||||
Run the `v3.0.6` version of etcd or specify another release version.
|
||||
Run the `v3.1.2` version of etcd or specify another release version.
|
||||
|
||||
```
|
||||
sudo rkt run --net=default:IP=${NODE1} coreos.com/etcd:v3.0.6 -- -name=node1 -advertise-client-urls=http://${NODE1}:2379 -initial-advertise-peer-urls=http://${NODE1}:2380 -listen-client-urls=http://0.0.0.0:2379 -listen-peer-urls=http://${NODE1}:2380 -initial-cluster=node1=http://${NODE1}:2380
|
||||
sudo rkt run --net=default:IP=${NODE1} coreos.com/etcd:v3.1.2 -- -name=node1 -advertise-client-urls=http://${NODE1}:2379 -initial-advertise-peer-urls=http://${NODE1}:2380 -listen-client-urls=http://0.0.0.0:2379 -listen-peer-urls=http://${NODE1}:2380 -initial-cluster=node1=http://${NODE1}:2380
|
||||
```
|
||||
|
||||
List the cluster member.
|
||||
@ -45,19 +45,19 @@ export NODE3=172.16.28.23
|
||||
|
||||
```
|
||||
# node 1
|
||||
sudo rkt run --net=default:IP=${NODE1} coreos.com/etcd:v3.0.6 -- -name=node1 -advertise-client-urls=http://${NODE1}:2379 -initial-advertise-peer-urls=http://${NODE1}:2380 -listen-client-urls=http://0.0.0.0:2379 -listen-peer-urls=http://${NODE1}:2380 -initial-cluster=node1=http://${NODE1}:2380,node2=http://${NODE2}:2380,node3=http://${NODE3}:2380
|
||||
sudo rkt run --net=default:IP=${NODE1} coreos.com/etcd:v3.1.2 -- -name=node1 -advertise-client-urls=http://${NODE1}:2379 -initial-advertise-peer-urls=http://${NODE1}:2380 -listen-client-urls=http://0.0.0.0:2379 -listen-peer-urls=http://${NODE1}:2380 -initial-cluster=node1=http://${NODE1}:2380,node2=http://${NODE2}:2380,node3=http://${NODE3}:2380
|
||||
|
||||
# node 2
|
||||
sudo rkt run --net=default:IP=${NODE2} coreos.com/etcd:v3.0.6 -- -name=node2 -advertise-client-urls=http://${NODE2}:2379 -initial-advertise-peer-urls=http://${NODE2}:2380 -listen-client-urls=http://0.0.0.0:2379 -listen-peer-urls=http://${NODE2}:2380 -initial-cluster=node1=http://${NODE1}:2380,node2=http://${NODE2}:2380,node3=http://${NODE3}:2380
|
||||
sudo rkt run --net=default:IP=${NODE2} coreos.com/etcd:v3.1.2 -- -name=node2 -advertise-client-urls=http://${NODE2}:2379 -initial-advertise-peer-urls=http://${NODE2}:2380 -listen-client-urls=http://0.0.0.0:2379 -listen-peer-urls=http://${NODE2}:2380 -initial-cluster=node1=http://${NODE1}:2380,node2=http://${NODE2}:2380,node3=http://${NODE3}:2380
|
||||
|
||||
# node 3
|
||||
sudo rkt run --net=default:IP=${NODE3} coreos.com/etcd:v3.0.6 -- -name=node3 -advertise-client-urls=http://${NODE3}:2379 -initial-advertise-peer-urls=http://${NODE3}:2380 -listen-client-urls=http://0.0.0.0:2379 -listen-peer-urls=http://${NODE3}:2380 -initial-cluster=node1=http://${NODE1}:2380,node2=http://${NODE2}:2380,node3=http://${NODE3}:2380
|
||||
sudo rkt run --net=default:IP=${NODE3} coreos.com/etcd:v3.1.2 -- -name=node3 -advertise-client-urls=http://${NODE3}:2379 -initial-advertise-peer-urls=http://${NODE3}:2380 -listen-client-urls=http://0.0.0.0:2379 -listen-peer-urls=http://${NODE3}:2380 -initial-cluster=node1=http://${NODE1}:2380,node2=http://${NODE2}:2380,node3=http://${NODE3}:2380
|
||||
```
|
||||
|
||||
Verify the cluster is healthy and can be reached.
|
||||
|
||||
```
|
||||
ETCDCTL_API=3 etcdctl --endpoints=http://172.16.28.21:2379,http://172.16.28.22:2379,http://172.16.28.23:2379 endpoint-health
|
||||
ETCDCTL_API=3 etcdctl --endpoints=http://172.16.28.21:2379,http://172.16.28.22:2379,http://172.16.28.23:2379 endpoint health
|
||||
```
|
||||
|
||||
### DNS
|
||||
@ -68,9 +68,40 @@ Production clusters which refer to peers by DNS name known to the local resolver
|
||||
|
||||
In order to expose the etcd API to clients outside of Docker host, use the host IP address of the container. Please see [`docker inspect`](https://docs.docker.com/engine/reference/commandline/inspect) for more detail on how to get the IP address. Alternatively, specify `--net=host` flag to `docker run` command to skip placing the container inside of a separate network stack.
|
||||
|
||||
### Running a single node etcd
|
||||
|
||||
Use the host IP address when configuring etcd:
|
||||
|
||||
```
|
||||
export NODE1=192.168.1.21
|
||||
```
|
||||
|
||||
Run the latest version of etcd:
|
||||
|
||||
```
|
||||
docker run \
|
||||
-p 2379:2379 \
|
||||
-p 2380:2380 \
|
||||
--volume=${DATA_DIR}:/etcd-data \
|
||||
--name etcd quay.io/coreos/etcd:latest \
|
||||
/usr/local/bin/etcd \
|
||||
--data-dir=/etcd-data --name node1 \
|
||||
--initial-advertise-peer-urls http://${NODE1}:2380 --listen-peer-urls http://${NODE1}:2380 \
|
||||
--advertise-client-urls http://${NODE1}:2379 --listen-client-urls http://${NODE1}:2379 \
|
||||
--initial-cluster node1=http://${NODE1}:2380
|
||||
```
|
||||
|
||||
List the cluster member:
|
||||
|
||||
```
|
||||
etcdctl --endpoints=http://${NODE1}:2379 member list
|
||||
```
|
||||
|
||||
### Running a 3 node etcd cluster
|
||||
|
||||
```
|
||||
# For each machine
|
||||
ETCD_VERSION=v3.0.0
|
||||
ETCD_VERSION=latest
|
||||
TOKEN=my-etcd-token
|
||||
CLUSTER_STATE=new
|
||||
NAME_1=etcd-node-0
|
||||
@ -80,39 +111,52 @@ HOST_1=10.20.30.1
|
||||
HOST_2=10.20.30.2
|
||||
HOST_3=10.20.30.3
|
||||
CLUSTER=${NAME_1}=http://${HOST_1}:2380,${NAME_2}=http://${HOST_2}:2380,${NAME_3}=http://${HOST_3}:2380
|
||||
DATA_DIR=/var/lib/etcd
|
||||
|
||||
# For node 1
|
||||
THIS_NAME=${NAME_1}
|
||||
THIS_IP=${HOST_1}
|
||||
sudo docker run --net=host --name etcd quay.io/coreos/etcd:${ETCD_VERSION} \
|
||||
/usr/local/bin/etcd \
|
||||
--data-dir=data.etcd --name ${THIS_NAME} \
|
||||
--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \
|
||||
--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \
|
||||
--initial-cluster ${CLUSTER} \
|
||||
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
|
||||
docker run \
|
||||
-p 2379:2379 \
|
||||
-p 2380:2380 \
|
||||
--volume=${DATA_DIR}:/etcd-data \
|
||||
--name etcd quay.io/coreos/etcd:${ETCD_VERSION} \
|
||||
/usr/local/bin/etcd \
|
||||
--data-dir=/etcd-data --name ${THIS_NAME} \
|
||||
--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \
|
||||
--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \
|
||||
--initial-cluster ${CLUSTER} \
|
||||
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
|
||||
|
||||
# For node 2
|
||||
THIS_NAME=${NAME_2}
|
||||
THIS_IP=${HOST_2}
|
||||
sudo docker run --net=host --name etcd quay.io/coreos/etcd:${ETCD_VERSION} \
|
||||
/usr/local/bin/etcd \
|
||||
--data-dir=data.etcd --name ${THIS_NAME} \
|
||||
--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \
|
||||
--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \
|
||||
--initial-cluster ${CLUSTER} \
|
||||
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
|
||||
docker run \
|
||||
-p 2379:2379 \
|
||||
-p 2380:2380 \
|
||||
--volume=${DATA_DIR}:/etcd-data \
|
||||
--name etcd quay.io/coreos/etcd:${ETCD_VERSION} \
|
||||
/usr/local/bin/etcd \
|
||||
--data-dir=/etcd-data --name ${THIS_NAME} \
|
||||
--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \
|
||||
--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \
|
||||
--initial-cluster ${CLUSTER} \
|
||||
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
|
||||
|
||||
# For node 3
|
||||
THIS_NAME=${NAME_3}
|
||||
THIS_IP=${HOST_3}
|
||||
sudo docker run --net=host --name etcd quay.io/coreos/etcd:${ETCD_VERSION} \
|
||||
/usr/local/bin/etcd \
|
||||
--data-dir=data.etcd --name ${THIS_NAME} \
|
||||
--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \
|
||||
--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \
|
||||
--initial-cluster ${CLUSTER} \
|
||||
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
|
||||
docker run \
|
||||
-p 2379:2379 \
|
||||
-p 2380:2380 \
|
||||
--volume=${DATA_DIR}:/etcd-data \
|
||||
--name etcd quay.io/coreos/etcd:${ETCD_VERSION} \
|
||||
/usr/local/bin/etcd \
|
||||
--data-dir=/etcd-data --name ${THIS_NAME} \
|
||||
--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \
|
||||
--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \
|
||||
--initial-cluster ${CLUSTER} \
|
||||
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
|
||||
```
|
||||
|
||||
To run `etcdctl` using API version 3:
|
||||
@ -123,5 +167,30 @@ docker exec etcd /bin/sh -c "export ETCDCTL_API=3 && /usr/local/bin/etcdctl put
|
||||
|
||||
## Bare Metal
|
||||
|
||||
To provision a 3 node etcd cluster on bare-metal, you might find the examples in the [baremetal repo](https://github.com/coreos/coreos-baremetal/tree/master/examples) useful.
|
||||
To provision a 3 node etcd cluster on bare-metal, the examples in the [baremetal repo](https://github.com/coreos/coreos-baremetal/tree/master/examples) may be useful.
|
||||
|
||||
## Mounting a certificate volume
|
||||
|
||||
The etcd release container does not include default root certificates. To use HTTPS with certificates trusted by a root authority (e.g., for discovery), mount a certificate directory into the etcd container:
|
||||
|
||||
```
|
||||
rkt run \
|
||||
--volume etcd-ssl-certs-bundle,kind=host,source=/etc/ssl/certs/ca-certificates.crt \
|
||||
--mount volume=etcd-ssl-certs-bundle,target=/etc/ssl/certs/ca-certificates.crt \
|
||||
quay.io/coreos/etcd:latest -- --name my-name \
|
||||
--initial-advertise-peer-urls http://localhost:2380 --listen-peer-urls http://localhost:2380 \
|
||||
--advertise-client-urls http://localhost:2379 --listen-client-urls http://localhost:2379 \
|
||||
--discovery https://discovery.etcd.io/c11fbcdc16972e45253491a24fcf45e1
|
||||
```
|
||||
|
||||
```
|
||||
docker run \
|
||||
-p 2379:2379 \
|
||||
-p 2380:2380 \
|
||||
--volume=/etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt \
|
||||
quay.io/coreos/etcd:latest \
|
||||
/usr/local/bin/etcd --name my-name \
|
||||
--initial-advertise-peer-urls http://localhost:2380 --listen-peer-urls http://localhost:2380 \
|
||||
--advertise-client-urls http://localhost:2379 --listen-client-urls http://localhost:2379 \
|
||||
--discovery https://discovery.etcd.io/86a9ff6c8cb8b4c4544c1a2f88f8b801
|
||||
```
|
||||
|
206
Documentation/op-guide/etcd3_alert.rules
Normal file
206
Documentation/op-guide/etcd3_alert.rules
Normal file
@ -0,0 +1,206 @@
|
||||
# general cluster availability
|
||||
|
||||
# alert if another failed member will result in an unavailable cluster
|
||||
ALERT InsufficientMembers
|
||||
IF count(up{job="etcd"} == 0) > (count(up{job="etcd"}) / 2 - 1)
|
||||
FOR 3m
|
||||
LABELS {
|
||||
severity = "critical"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "etcd cluster insufficient members",
|
||||
description = "If one more etcd member goes down the cluster will be unavailable",
|
||||
}
|
||||
|
||||
# etcd leader alerts
|
||||
# ==================
|
||||
|
||||
# alert if any etcd instance has no leader
|
||||
ALERT NoLeader
|
||||
IF etcd_server_has_leader{job="etcd"} == 0
|
||||
FOR 1m
|
||||
LABELS {
|
||||
severity = "critical"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "etcd member has no leader",
|
||||
description = "etcd member {{ $labels.instance }} has no leader",
|
||||
}
|
||||
|
||||
# alert if there are lots of leader changes
|
||||
ALERT HighNumberOfLeaderChanges
|
||||
IF increase(etcd_server_leader_changes_seen_total{job="etcd"}[1h]) > 3
|
||||
LABELS {
|
||||
severity = "warning"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "a high number of leader changes within the etcd cluster are happening",
|
||||
description = "etcd instance {{ $labels.instance }} has seen {{ $value }} leader changes within the last hour",
|
||||
}
|
||||
|
||||
# gRPC request alerts
|
||||
# ===================
|
||||
|
||||
# alert if more than 1% of gRPC method calls have failed within the last 5 minutes
|
||||
ALERT HighNumberOfFailedGRPCRequests
|
||||
IF sum by(grpc_method) (rate(etcd_grpc_requests_failed_total{job="etcd"}[5m]))
|
||||
/ sum by(grpc_method) (rate(etcd_grpc_total{job="etcd"}[5m])) > 0.01
|
||||
FOR 10m
|
||||
LABELS {
|
||||
severity = "warning"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "a high number of gRPC requests are failing",
|
||||
description = "{{ $value }}% of requests for {{ $labels.grpc_method }} failed on etcd instance {{ $labels.instance }}",
|
||||
}
|
||||
|
||||
# alert if more than 5% of gRPC method calls have failed within the last 5 minutes
|
||||
ALERT HighNumberOfFailedGRPCRequests
|
||||
IF sum by(grpc_method) (rate(etcd_grpc_requests_failed_total{job="etcd"}[5m]))
|
||||
/ sum by(grpc_method) (rate(etcd_grpc_total{job="etcd"}[5m])) > 0.05
|
||||
FOR 5m
|
||||
LABELS {
|
||||
severity = "critical"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "a high number of gRPC requests are failing",
|
||||
description = "{{ $value }}% of requests for {{ $labels.grpc_method }} failed on etcd instance {{ $labels.instance }}",
|
||||
}
|
||||
|
||||
# alert if the 99th percentile of gRPC method calls take more than 150ms
|
||||
ALERT GRPCRequestsSlow
|
||||
IF histogram_quantile(0.99, rate(etcd_grpc_unary_requests_duration_seconds_bucket[5m])) > 0.15
|
||||
FOR 10m
|
||||
LABELS {
|
||||
severity = "critical"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "slow gRPC requests",
|
||||
description = "on etcd instance {{ $labels.instance }} gRPC requests to {{ $label.grpc_method }} are slow",
|
||||
}
|
||||
|
||||
# HTTP requests alerts
|
||||
# ====================
|
||||
|
||||
# alert if more than 1% of requests to an HTTP endpoint have failed within the last 5 minutes
|
||||
ALERT HighNumberOfFailedHTTPRequests
|
||||
IF sum by(method) (rate(etcd_http_failed_total{job="etcd"}[5m]))
|
||||
/ sum by(method) (rate(etcd_http_received_total{job="etcd"}[5m])) > 0.01
|
||||
FOR 10m
|
||||
LABELS {
|
||||
severity = "warning"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "a high number of HTTP requests are failing",
|
||||
description = "{{ $value }}% of requests for {{ $labels.method }} failed on etcd instance {{ $labels.instance }}",
|
||||
}
|
||||
|
||||
# alert if more than 5% of requests to an HTTP endpoint have failed within the last 5 minutes
|
||||
ALERT HighNumberOfFailedHTTPRequests
|
||||
IF sum by(method) (rate(etcd_http_failed_total{job="etcd"}[5m]))
|
||||
/ sum by(method) (rate(etcd_http_received_total{job="etcd"}[5m])) > 0.05
|
||||
FOR 5m
|
||||
LABELS {
|
||||
severity = "critical"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "a high number of HTTP requests are failing",
|
||||
description = "{{ $value }}% of requests for {{ $labels.method }} failed on etcd instance {{ $labels.instance }}",
|
||||
}
|
||||
|
||||
# alert if the 99th percentile of HTTP requests take more than 150ms
|
||||
ALERT HTTPRequestsSlow
|
||||
IF histogram_quantile(0.99, rate(etcd_http_successful_duration_seconds_bucket[5m])) > 0.15
|
||||
FOR 10m
|
||||
LABELS {
|
||||
severity = "warning"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "slow HTTP requests",
|
||||
description = "on etcd instance {{ $labels.instance }} HTTP requests to {{ $label.method }} are slow",
|
||||
}
|
||||
|
||||
# file descriptor alerts
|
||||
# ======================
|
||||
|
||||
instance:fd_utilization = process_open_fds / process_max_fds
|
||||
|
||||
# alert if file descriptors are likely to exhaust within the next 4 hours
|
||||
ALERT FdExhaustionClose
|
||||
IF predict_linear(instance:fd_utilization[1h], 3600 * 4) > 1
|
||||
FOR 10m
|
||||
LABELS {
|
||||
severity = "warning"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "file descriptors soon exhausted",
|
||||
description = "{{ $labels.job }} instance {{ $labels.instance }} will exhaust its file descriptors soon",
|
||||
}
|
||||
|
||||
# alert if file descriptors are likely to exhaust within the next hour
|
||||
ALERT FdExhaustionClose
|
||||
IF predict_linear(instance:fd_utilization[10m], 3600) > 1
|
||||
FOR 10m
|
||||
LABELS {
|
||||
severity = "critical"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "file descriptors soon exhausted",
|
||||
description = "{{ $labels.job }} instance {{ $labels.instance }} will exhaust its file descriptors soon",
|
||||
}
|
||||
|
||||
# etcd member communication alerts
|
||||
# ================================
|
||||
|
||||
# alert if 99th percentile of round trips take 150ms
|
||||
ALERT EtcdMemberCommunicationSlow
|
||||
IF histogram_quantile(0.99, rate(etcd_network_member_round_trip_time_seconds_bucket[5m])) > 0.15
|
||||
FOR 10m
|
||||
LABELS {
|
||||
severity = "warning"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "etcd member communication is slow",
|
||||
description = "etcd instance {{ $labels.instance }} member communication with {{ $label.To }} is slow",
|
||||
}
|
||||
|
||||
# etcd proposal alerts
|
||||
# ====================
|
||||
|
||||
# alert if there are several failed proposals within an hour
|
||||
ALERT HighNumberOfFailedProposals
|
||||
IF increase(etcd_server_proposals_failed_total{job="etcd"}[1h]) > 5
|
||||
LABELS {
|
||||
severity = "warning"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "a high number of proposals within the etcd cluster are failing",
|
||||
description = "etcd instance {{ $labels.instance }} has seen {{ $value }} proposal failures within the last hour",
|
||||
}
|
||||
|
||||
# etcd disk io latency alerts
|
||||
# ===========================
|
||||
|
||||
# alert if 99th percentile of fsync durations is higher than 500ms
|
||||
ALERT HighFsyncDurations
|
||||
IF histogram_quantile(0.99, rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m])) > 0.5
|
||||
FOR 10m
|
||||
LABELS {
|
||||
severity = "warning"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "high fsync durations",
|
||||
description = "etcd instance {{ $labels.instance }} fync durations are high",
|
||||
}
|
||||
|
||||
# alert if 99th percentile of commit durations is higher than 250ms
|
||||
ALERT HighCommitDurations
|
||||
IF histogram_quantile(0.99, rate(etcd_disk_backend_commit_duration_seconds_bucket[5m])) > 0.25
|
||||
FOR 10m
|
||||
LABELS {
|
||||
severity = "warning"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "high commit durations",
|
||||
description = "etcd instance {{ $labels.instance }} commit durations are high",
|
||||
}
|
@ -4,16 +4,15 @@
|
||||
|
||||
etcd gateway is a simple TCP proxy that forwards network data to the etcd cluster. The gateway is stateless and transparent; it neither inspects client requests nor interferes with cluster responses.
|
||||
|
||||
The gateway supports multiple etcd server endpoints. When the gateway starts, it randomly picks one etcd server endpoint and forwards all requests to that endpoint. This endpoint serves all requests until the gateway detects a network failure. If the gateway detects an endpoint failure, it will switch to a different endpoint, if available, to hide failures from its clients. Other retry policies, such as weighted round-robin, may be supported in the future.
|
||||
The gateway supports multiple etcd server endpoints and works on a simple round-robin policy. It only routes to available enpoints and hides failures from its clients. Other retry policies, such as weighted round-robin, may be supported in the future.
|
||||
|
||||
## When to use etcd gateway
|
||||
|
||||
Every application that accesses etcd must first have the address of an etcd cluster client endpoint. If multiple applications on the same server access the same etcd cluster, every application still needs to know the advertised client endpoints of the etcd cluster. If the etcd cluster is reconfigured to have different endpoints, every application may also need to update its endpoint list. This wide-scale reconfiguration is both tedious and error prone.
|
||||
|
||||
etcd gateway solves this problem by serving as a stable local endpoint. A typical etcd gateway configuration has
|
||||
each machine running a gateway listening on a local address and every etcd application connecting to its local gateway. The upshot is only the gateway needs to update its endpoints instead of updating each and every application.
|
||||
etcd gateway solves this problem by serving as a stable local endpoint. A typical etcd gateway configuration has each machine running a gateway listening on a local address and every etcd application connecting to its local gateway. The upshot is only the gateway needs to update its endpoints instead of updating each and every application.
|
||||
|
||||
In summary, to automatically propagate cluster endpoint changes, the etcd gateway runs on every machine serving multiple applications accessing same etcd cluster.
|
||||
In summary, to automatically propagate cluster endpoint changes, the etcd gateway runs on every machine serving multiple applications accessing the same etcd cluster.
|
||||
|
||||
## When not to use etcd gateway
|
||||
|
||||
@ -63,4 +62,44 @@ Start the etcd gateway to fetch the endpoints from the DNS SRV entries with the
|
||||
```bash
|
||||
$ etcd gateway --discovery-srv=example.com
|
||||
2016-08-16 11:21:18.867350 I | tcpproxy: ready to proxy client requests to [...]
|
||||
```
|
||||
```
|
||||
|
||||
## Configuration flags
|
||||
|
||||
### etcd cluster
|
||||
|
||||
#### --endpoints
|
||||
|
||||
* Comma-separated list of etcd server targets for forwarding client connections.
|
||||
* Default: `127.0.0.1:2379`
|
||||
* Invalid example: `https://127.0.0.1:2379` (gateway does not terminate TLS)
|
||||
|
||||
#### --discovery-srv
|
||||
|
||||
* DNS domain used to bootstrap cluster endpoints through SRV recrods.
|
||||
* Default: (not set)
|
||||
|
||||
### Network
|
||||
|
||||
#### --listen-addr
|
||||
|
||||
* Interface and port to bind for accepting client requests.
|
||||
* Default: `127.0.0.1:23790`
|
||||
|
||||
#### --retry-delay
|
||||
|
||||
* Duration of delay before retrying to connect to failed endpoints.
|
||||
* Default: 1m0s
|
||||
* Invalid example: "123" (expects time unit in format)
|
||||
|
||||
### Security
|
||||
|
||||
#### --insecure-discovery
|
||||
|
||||
* Accept SRV records that are insecure or susceptible to man-in-the-middle attacks.
|
||||
* Default: `false`
|
||||
|
||||
#### --trusted-ca-file
|
||||
|
||||
* Path to the client TLS CA file for the etcd cluster. Used to authenticate endpoints.
|
||||
* Default: (not set)
|
||||
|
@ -115,7 +115,7 @@
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [{
|
||||
"expr": "sum(rate({grpc_type=\"unary\",grpc_code!=\"OK\"} [1m]))",
|
||||
"expr": "sum(rate(grpc_server_started_total{grpc_type=\"unary\"} [1m]))",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "{{instance}} RPC Rate",
|
||||
"metric": "grpc_server_started_total",
|
||||
@ -123,7 +123,7 @@
|
||||
"step": 2
|
||||
},
|
||||
{
|
||||
"expr": "sum(rate(grpc_server_started_total{grpc_type=\"unary\",grpc_code!=\"OK\"} [1m])) - sum(rate(grpc_server_handled_total{grpc_type=\"unary\"} [1m]))",
|
||||
"expr": "sum(rate(grpc_server_started_total{grpc_type=\"unary\"} [1m])) - sum(rate(grpc_server_handled_total{grpc_type=\"unary\",grpc_code!=\"OK\"} [1m]))",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "{{instance}} RPC Failed Rate",
|
||||
"metric": "grpc_server_handled_total",
|
||||
@ -197,7 +197,7 @@
|
||||
"stack": true,
|
||||
"steppedLine": false,
|
||||
"targets": [{
|
||||
"expr": "sum(grpc_server_started_total {grpc_service=\"etcdserverpb.Watch\",grpc_type=\"bidi_stream\",grpc_code!=\"OK\"}) - sum(grpc_server_handled_total {grpc_service=\"etcdserverpb.Watch\",grpc_type=\"bidi_stream\"})",
|
||||
"expr": "sum(grpc_server_started_total{grpc_service=\"etcdserverpb.Watch\",grpc_type=\"bidi_stream\"}) - sum(grpc_server_handled_total{grpc_service=\"etcdserverpb.Watch\",grpc_type=\"bidi_stream\"})",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "Watch Streams",
|
||||
"metric": "grpc_server_handled_total",
|
||||
@ -205,7 +205,7 @@
|
||||
"step": 4
|
||||
},
|
||||
{
|
||||
"expr": "sum(grpc_server_started_total {grpc_service=\"etcdserverpb.Lease\",grpc_type=\"bidi_stream\"}) - sum(grpc_server_handled_total {grpc_service=\"etcdserverpb.Lease\",grpc_type=\"bidi_stream\"})",
|
||||
"expr": "sum(grpc_server_started_total{grpc_service=\"etcdserverpb.Lease\",grpc_type=\"bidi_stream\"}) - sum(grpc_server_handled_total{grpc_service=\"etcdserverpb.Lease\",grpc_type=\"bidi_stream\"})",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "Lease Streams",
|
||||
"metric": "grpc_server_handled_total",
|
||||
|
@ -1,7 +1,5 @@
|
||||
# gRPC proxy
|
||||
|
||||
*This is a pre-alpha feature, we are looking for early feedback.*
|
||||
|
||||
The gRPC proxy is a stateless etcd reverse proxy operating at the gRPC layer (L7). The proxy is designed to reduce the total processing load on the core etcd cluster. For horizontal scalability, it coalesces watch and lease API requests. To protect the cluster against abusive clients, it caches key range requests.
|
||||
|
||||
The gRPC proxy supports multiple etcd server endpoints. When the proxy starts, it randomly picks one etcd server endpoint to use. This endpoint serves all requests until the proxy detects an endpoint failure. If the gRPC proxy detects an endpoint failure, it switches to a different endpoint, if available, to hide failures from its clients. Other retry policies, such as weighted round-robin, may be supported in the future.
|
||||
@ -36,13 +34,36 @@ watch key A ^ ^ watch key A |
|
||||
|
||||
To effectively coalesce multiple client watchers into a single watcher, the gRPC proxy coalesces new `c-watchers` into an existing `s-watcher` when possible. This coalesced `s-watcher` may be out of sync with the etcd server due to network delays or buffered undelivered events. When the watch revision is unspecified, the gRPC proxy will not guarantee the `c-watcher` will start watching from the most recent store revision. For example, if a client watches from an etcd server with revision 1000, that watcher will begin at revision 1000. If a client watches from the gRPC proxy, may begin watching from revision 990.
|
||||
|
||||
Similar limitations apply to cancellation. When the watcher is cancelled, the etcd server’s revision may be greater than the cancellation response revision.
|
||||
Similar limitations apply to cancellation. When the watcher is cancelled, the etcd server’s revision may be greater than the cancellation response revision.
|
||||
|
||||
These two limitations should not cause problems for most use cases. In the future, there may be additional options to force the watcher to bypass the gRPC proxy for more accurate revision responses.
|
||||
These two limitations should not cause problems for most use cases. In the future, there may be additional options to force the watcher to bypass the gRPC proxy for more accurate revision responses.
|
||||
|
||||
## Scalable lease API
|
||||
|
||||
TODO
|
||||
To keep its leases alive, a client must establish at least one gRPC stream to an etcd server for sending periodic heartbeats. If an etcd workload involves heavy lease activity spread over many clients, these streams may contribute to excessive CPU utilization. To reduce the total number of streams on the core cluster, the proxy supports lease stream coalescing.
|
||||
|
||||
Assuming N clients are updating leases, a single gRPC proxy reduces the stream load on the etcd server from N to 1. Deployments may have additional gRPC proxies to further distribute streams across multiple proxies.
|
||||
|
||||
In the following example, three clients update three independent leases (`L1`, `L2`, and `L3`). The gRPC proxy coalesces the three client lease streams (`c-streams`) into a single lease keep alive stream (`s-stream`) attached to an etcd server. The proxy forwards client-side lease heartbeats from the c-streams to the s-stream, then returns the responses to the corresponding c-streams.
|
||||
|
||||
```
|
||||
+-------------+
|
||||
| etcd server |
|
||||
+------+------+
|
||||
^
|
||||
| heartbeat L1, L2, L3
|
||||
| (s-stream)
|
||||
v
|
||||
+-------+-----+
|
||||
| gRPC proxy +<-----------+
|
||||
+---+------+--+ | heartbeat L3
|
||||
^ ^ | (c-stream)
|
||||
heartbeat L1 | | heartbeat L2 |
|
||||
(c-stream) v v (c-stream) v
|
||||
+------+-+ +-+------+ +-----+--+
|
||||
| client | | client | | client |
|
||||
+--------+ +--------+ +--------+
|
||||
```
|
||||
|
||||
## Abusive clients protection
|
||||
|
||||
@ -75,3 +96,98 @@ $ ETCDCTL_API=3 ./etcdctl --endpoints=127.0.0.1:2379 get foo
|
||||
foo
|
||||
bar
|
||||
```
|
||||
|
||||
## Client endpoint synchronization and name resolution
|
||||
|
||||
The proxy supports registering its endpoints for discovery by writing to a user-defined endpoint. This serves two purposes. First, it allows clients to synchronize their endpoints against a set of proxy endpoints for high availability. Second, it is an endpoint provider for etcd [gRPC naming](../dev-guide/grpc_naming.md).
|
||||
|
||||
Register proxy(s) by providing a user-defined prefix:
|
||||
|
||||
```bash
|
||||
$ etcd grpc-proxy start --endpoints=localhost:2379 \
|
||||
--listen-addr=127.0.0.1:23790 \
|
||||
--advertise-client-url=127.0.0.1:23790 \
|
||||
--resolver-prefix="___grpc_proxy_endpoint" \
|
||||
--resolver-ttl=60
|
||||
|
||||
$ etcd grpc-proxy start --endpoints=localhost:2379 \
|
||||
--listen-addr=127.0.0.1:23791 \
|
||||
--advertise-client-url=127.0.0.1:23791 \
|
||||
--resolver-prefix="___grpc_proxy_endpoint" \
|
||||
--resolver-ttl=60
|
||||
```
|
||||
|
||||
The proxy will list all its members for member list:
|
||||
|
||||
```bash
|
||||
ETCDCTL_API=3 ./bin/etcdctl --endpoints=http://localhost:23790 member list --write-out table
|
||||
|
||||
+----+---------+--------------------------------+------------+-----------------+
|
||||
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS |
|
||||
+----+---------+--------------------------------+------------+-----------------+
|
||||
| 0 | started | Gyu-Hos-MBP.sfo.coreos.systems | | 127.0.0.1:23791 |
|
||||
| 0 | started | Gyu-Hos-MBP.sfo.coreos.systems | | 127.0.0.1:23790 |
|
||||
+----+---------+--------------------------------+------------+-----------------+
|
||||
```
|
||||
|
||||
This lets clients automatically discover proxy endpoints through Sync:
|
||||
|
||||
```go
|
||||
cli, err := clientv3.New(clientv3.Config{
|
||||
Endpoints: []string{"http://localhost:23790"},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
// fetch registered grpc-proxy endpoints
|
||||
if err := cli.Sync(context.Background()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
Note that if a proxy is configured without a resolver prefix,
|
||||
|
||||
```bash
|
||||
$ etcd grpc-proxy start --endpoints=localhost:2379 \
|
||||
--listen-addr=127.0.0.1:23792 \
|
||||
--advertise-client-url=127.0.0.1:23792
|
||||
```
|
||||
|
||||
the member list API to the grpc-proxy returns its own `advertise-client-url`:
|
||||
|
||||
```bash
|
||||
ETCDCTL_API=3 ./bin/etcdctl --endpoints=http://localhost:23792 member list --write-out table
|
||||
|
||||
+----+---------+--------------------------------+------------+-----------------+
|
||||
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS |
|
||||
+----+---------+--------------------------------+------------+-----------------+
|
||||
| 0 | started | Gyu-Hos-MBP.sfo.coreos.systems | | 127.0.0.1:23792 |
|
||||
+----+---------+--------------------------------+------------+-----------------+
|
||||
```
|
||||
|
||||
## Namespacing
|
||||
|
||||
Suppose an application expects full control over the entire key space, but the etcd cluster is shared with other applications. To let all appications run without interfering with each other, the proxy can partition the etcd keyspace so clients appear to have access to the complete keyspace. When the proxy is given the flag `--namespace`, all client requests going into the proxy are translated to have a user-defined prefix on the keys. Accesses to the etcd cluster will be under the prefix and responses from the proxy will strip away the prefix; to the client, it appears as if there is no prefix at all.
|
||||
|
||||
To namespace a proxy, start it with `--namespace`:
|
||||
|
||||
```bash
|
||||
$ etcd grpc-proxy start --endpoints=localhost:2379 \
|
||||
--listen-addr=127.0.0.1:23790 \
|
||||
--namespace=my-prefix/
|
||||
```
|
||||
|
||||
Accesses to the proxy are now transparently prefixed on the etcd cluster:
|
||||
|
||||
```bash
|
||||
$ ETCDCTL_API=3 ./bin/etcdctl --endpoints=localhost:23790 put my-key abc
|
||||
# OK
|
||||
$ ETCDCTL_API=3 ./bin/etcdctl --endpoints=localhost:23790 get my-key
|
||||
# my-key
|
||||
# abc
|
||||
$ ETCDCTL_API=3 ./bin/etcdctl --endpoints=localhost:2379 get my-prefix/my-key
|
||||
# my-prefix/my-key
|
||||
# abc
|
||||
```
|
||||
|
@ -56,6 +56,12 @@ nohup /tmp/prometheus \
|
||||
Now Prometheus will scrape etcd metrics every 10 seconds.
|
||||
|
||||
|
||||
## Alerting
|
||||
|
||||
There is a [set of default alerts for etcd v3 clusters](./etcd3_alert.rules).
|
||||
|
||||
> Note: `job` labels may need to be adjusted to fit a particular need. The rules were written to apply to a single cluster so it is recommended to choose labels unique to a cluster.
|
||||
|
||||
## Grafana
|
||||
|
||||
[Grafana][grafana] has built-in Prometheus support; just add a Prometheus data source:
|
||||
|
@ -17,58 +17,54 @@ For some baseline performance numbers, we consider a three member etcd cluster w
|
||||
- Google Cloud Compute Engine
|
||||
- 3 machines of 8 vCPUs + 16GB Memory + 50GB SSD
|
||||
- 1 machine(client) of 16 vCPUs + 30GB Memory + 50GB SSD
|
||||
- Ubuntu 15.10
|
||||
- etcd v3 master branch (commit SHA d8f325d), Go 1.6.2
|
||||
- Ubuntu 17.04
|
||||
- etcd 3.2.0, go 1.8.3
|
||||
|
||||
With this configuration, etcd can approximately write:
|
||||
|
||||
| Number of keys | Key size in bytes | Value size in bytes | Number of connections | Number of clients | Target etcd server | Average write QPS | Average latency per request | Memory |
|
||||
|----------------|-------------------|---------------------|-----------------------|-------------------|--------------------|-------------------|-----------------------------|--------|
|
||||
| 10,000 | 8 | 256 | 1 | 1 | leader only | 525 | 2ms | 35 MB |
|
||||
| 100,000 | 8 | 256 | 100 | 1000 | leader only | 25,000 | 30ms | 35 MB |
|
||||
| 100,000 | 8 | 256 | 100 | 1000 | all members | 33,000 | 25ms | 35 MB |
|
||||
| Number of keys | Key size in bytes | Value size in bytes | Number of connections | Number of clients | Target etcd server | Average write QPS | Average latency per request | Average server RSS |
|
||||
|---------------:|------------------:|--------------------:|----------------------:|------------------:|--------------------|------------------:|----------------------------:|-------------------:|
|
||||
| 10,000 | 8 | 256 | 1 | 1 | leader only | 583 | 1.6ms | 48 MB |
|
||||
| 100,000 | 8 | 256 | 100 | 1000 | leader only | 44,341 | 22ms | 124MB |
|
||||
| 100,000 | 8 | 256 | 100 | 1000 | all members | 50,104 | 20ms | 126MB |
|
||||
|
||||
Sample commands are:
|
||||
|
||||
```
|
||||
# assuming IP_1 is leader, write requests to the leader
|
||||
benchmark --endpoints={IP_1} --conns=1 --clients=1 \
|
||||
```sh
|
||||
# write to leader
|
||||
benchmark --endpoints=${HOST_1} --target-leader --conns=1 --clients=1 \
|
||||
put --key-size=8 --sequential-keys --total=10000 --val-size=256
|
||||
benchmark --endpoints={IP_1} --conns=100 --clients=1000 \
|
||||
benchmark --endpoints=${HOST_1} --target-leader --conns=100 --clients=1000 \
|
||||
put --key-size=8 --sequential-keys --total=100000 --val-size=256
|
||||
|
||||
# write to all members
|
||||
benchmark --endpoints={IP_1},{IP_2},{IP_3} --conns=100 --clients=1000 \
|
||||
benchmark --endpoints=${HOST_1},${HOST_2},${HOST_3} --conns=100 --clients=1000 \
|
||||
put --key-size=8 --sequential-keys --total=100000 --val-size=256
|
||||
```
|
||||
|
||||
Linearizable read requests go through a quorum of cluster members for consensus to fetch the most recent data. Serializable read requests are cheaper than linearizable reads since they are served by any single etcd member, instead of a quorum of members, in exchange for possibly serving stale data. etcd can read:
|
||||
|
||||
| Number of requests | Key size in bytes | Value size in bytes | Number of connections | Number of clients | Consistency | Average latency per request | Average read QPS |
|
||||
|--------------------|-------------------|---------------------|-----------------------|-------------------|-------------|-----------------------------|------------------|
|
||||
| 10,000 | 8 | 256 | 1 | 1 | Linearizable | 2ms | 560 |
|
||||
| 10,000 | 8 | 256 | 1 | 1 | Serializable | 0.4ms | 7,500 |
|
||||
| 100,000 | 8 | 256 | 100 | 1000 | Linearizable | 15ms | 43,000 |
|
||||
| 100,000 | 8 | 256 | 100 | 1000 | Serializable | 9ms | 93,000 |
|
||||
| Number of requests | Key size in bytes | Value size in bytes | Number of connections | Number of clients | Consistency | Average read QPS | Average latency per request |
|
||||
|-------------------:|------------------:|--------------------:|----------------------:|------------------:|-------------|-----------------:|----------------------------:|
|
||||
| 10,000 | 8 | 256 | 1 | 1 | Linearizable | 1,353 | 0.7ms |
|
||||
| 10,000 | 8 | 256 | 1 | 1 | Serializable | 2,909 | 0.3ms |
|
||||
| 100,000 | 8 | 256 | 100 | 1000 | Linearizable | 141,578 | 5.5ms |
|
||||
| 100,000 | 8 | 256 | 100 | 1000 | Serializable | 185,758 | 2.2ms |
|
||||
|
||||
Sample commands are:
|
||||
|
||||
```
|
||||
# Linearizable read requests
|
||||
benchmark --endpoints={IP_1},{IP_2},{IP_3} --conns=1 --clients=1 \
|
||||
```sh
|
||||
# Single connection read requests
|
||||
benchmark --endpoints=${HOST_1},${HOST_2},${HOST_3} --conns=1 --clients=1 \
|
||||
range YOUR_KEY --consistency=l --total=10000
|
||||
benchmark --endpoints={IP_1},{IP_2},{IP_3} --conns=100 --clients=1000 \
|
||||
range YOUR_KEY --consistency=l --total=100000
|
||||
benchmark --endpoints=${HOST_1},${HOST_2},${HOST_3} --conns=1 --clients=1 \
|
||||
range YOUR_KEY --consistency=s --total=10000
|
||||
|
||||
# Serializable read requests for each member and sum up the numbers
|
||||
for endpoint in {IP_1} {IP_2} {IP_3}; do
|
||||
benchmark --endpoints=$endpoint --conns=1 --clients=1 \
|
||||
range YOUR_KEY --consistency=s --total=10000
|
||||
done
|
||||
for endpoint in {IP_1} {IP_2} {IP_3}; do
|
||||
benchmark --endpoints=$endpoint --conns=100 --clients=1000 \
|
||||
range YOUR_KEY --consistency=s --total=100000
|
||||
done
|
||||
# Many concurrent read requests
|
||||
benchmark --endpoints=${HOST_1},${HOST_2},${HOST_3} --conns=100 --clients=1000 \
|
||||
range YOUR_KEY --consistency=l --total=100000
|
||||
benchmark --endpoints=${HOST_1},${HOST_2},${HOST_3} --conns=100 --clients=1000 \
|
||||
range YOUR_KEY --consistency=s --total=100000
|
||||
```
|
||||
|
||||
We encourage running the benchmark test when setting up an etcd cluster for the first time in a new environment to ensure the cluster achieves adequate performance; cluster latency and throughput can be sensitive to minor environment differences.
|
||||
We encourage running the benchmark test when setting up an etcd cluster for the first time in a new environment to ensure the cluster achieves adequate performance; cluster latency and throughput can be sensitive to minor environment differences.
|
||||
|
@ -2,23 +2,23 @@
|
||||
|
||||
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 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].
|
||||
Reconfiguration requests can only be processed when a majority of 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].
|
||||
|
||||
To better understand the design behind runtime reconfiguration, we suggest reading [the runtime reconfiguration document][runtime-reconf].
|
||||
To better understand the design behind runtime reconfiguration, please read [the runtime reconfiguration document][runtime-reconf].
|
||||
|
||||
## Reconfiguration use cases
|
||||
|
||||
Let's 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-reconf].
|
||||
This section will walk through some common reasons for reconfiguring a cluster. Most of these reasons just involve combinations of adding or removing a member, which are explained below under [Cluster Reconfiguration Operations][cluster-reconf].
|
||||
|
||||
### Cycle or upgrade multiple machines
|
||||
|
||||
If multiple cluster members need to move 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 the cluster holds more than 50MB, it is recommended to [migrate the member's data directory][member migration].
|
||||
It is safe to remove the leader, however there is a brief period of downtime while the election process takes place. If the cluster holds more than 50MB of v2 data, it is recommended to [migrate the member's data directory][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.
|
||||
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 serialized 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.
|
||||
|
||||
@ -30,42 +30,34 @@ To replace the machine, follow the instructions for [removing the member][remove
|
||||
|
||||
### Restart cluster from majority failure
|
||||
|
||||
If the majority of the cluster is lost or all of the nodes have changed IP addresses, then manual action is necessary 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.
|
||||
If the majority of the cluster is lost or all of the nodes have changed IP addresses, then manual action is necessary 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.
|
||||
|
||||
## Cluster reconfiguration operations
|
||||
|
||||
Now that we have the use cases in mind, let us lay out the operations involved in each.
|
||||
With these use cases in mind, the involved operations can be described for 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.
|
||||
Before making any change, a simple majority (quorum) of etcd members must be available. This is essentially the same requirement for any kind of write to etcd.
|
||||
|
||||
All changes to the cluster are done one at a time:
|
||||
All changes to the cluster must be done sequentially:
|
||||
|
||||
* To update a single member peerURLs, make an update operation
|
||||
* To replace a single member, make an add then a remove operation
|
||||
* To increase from 3 to 5 members, make two add operations
|
||||
* To decrease from 5 to 3, make two remove operations
|
||||
* To update a single member peerURLs, issue an update operation
|
||||
* To replace a healthy single member, add a new member then remove the old member
|
||||
* To increase from 3 to 5 members, issue two add operations
|
||||
* To decrease from 5 to 3, issue two remove operations
|
||||
|
||||
All of these examples will use the `etcdctl` command line tool that ships with etcd.
|
||||
To change membership without `etcdctl`, use the [v2 HTTP members API][member-api] or the [v3 gRPC members API][member-api-grpc].
|
||||
All of these examples use the `etcdctl` command line tool that ships with etcd. To change membership without `etcdctl`, use the [v2 HTTP members API][member-api] or the [v3 gRPC members API][member-api-grpc].
|
||||
|
||||
### Update a member
|
||||
|
||||
#### Update advertise client URLs
|
||||
|
||||
To update the advertise client URLs of a member, simply restart
|
||||
that member with updated client urls flag (`--advertise-client-urls`) or environment variable
|
||||
(`ETCD_ADVERTISE_CLIENT_URLS`). The restarted member will self publish the updated URLs.
|
||||
A wrongly updated client URL will not affect the health of the etcd cluster.
|
||||
To update the advertise client URLs of a member, simply restart that member with updated client urls flag (`--advertise-client-urls`) or environment variable (`ETCD_ADVERTISE_CLIENT_URLS`). The restarted member will self publish the updated URLs. A wrongly updated client URL will not affect the health of the etcd cluster.
|
||||
|
||||
#### Update advertise peer URLs
|
||||
|
||||
To update the advertise peer URLs of a member, first update
|
||||
it explicitly via member command and then restart the member. The additional action is required
|
||||
since updating peer URLs changes the cluster wide configuration and can affect the health of the etcd cluster.
|
||||
To update the advertise peer URLs of a member, first update it explicitly via member command and then restart the member. The additional action is required since updating peer URLs changes the cluster wide configuration and can affect the health of the etcd cluster.
|
||||
|
||||
To update the peer URLs, first, we need to find the target member's ID. To list all members with `etcdctl`:
|
||||
To update the peer URLs, first find the target member's ID. To list all members with `etcdctl`:
|
||||
|
||||
```sh
|
||||
$ etcdctl member list
|
||||
@ -74,7 +66,7 @@ $ etcdctl member list
|
||||
a8266ecf031671f3: name=node1 peerURLs=http://localhost:23801 clientURLs=http://127.0.0.1:23791
|
||||
```
|
||||
|
||||
In this example let's `update` a8266ecf031671f3 member ID and change its peerURLs value to http://10.0.1.10:2380
|
||||
This example will `update` a8266ecf031671f3 member ID and change its peerURLs value to `http://10.0.1.10:2380`:
|
||||
|
||||
```sh
|
||||
$ etcdctl member update a8266ecf031671f3 http://10.0.1.10:2380
|
||||
@ -83,8 +75,7 @@ Updated member with ID a8266ecf031671f3 in cluster
|
||||
|
||||
### Remove a member
|
||||
|
||||
Let us say the member ID we want to remove is a8266ecf031671f3.
|
||||
We then use the `remove` command to perform the removal:
|
||||
Suppose the member ID to remove is a8266ecf031671f3. Use the `remove` command to perform the removal:
|
||||
|
||||
```sh
|
||||
$ etcdctl member remove a8266ecf031671f3
|
||||
@ -106,7 +97,7 @@ Adding a member is a two step process:
|
||||
* Add the new member to the cluster via the [HTTP members API][member-api], the [gRPC members API][member-api-grpc], 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 specifying its [name][conf-name] and [advertised peer URLs][conf-adv-peer]:
|
||||
`etcdctl` adds a new member to the cluster by specifying the member's [name][conf-name] and [advertised peer URLs][conf-adv-peer]:
|
||||
|
||||
```sh
|
||||
$ etcdctl member add infra3 http://10.0.1.13:2380
|
||||
@ -117,8 +108,7 @@ ETCD_INITIAL_CLUSTER="infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11: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:
|
||||
`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:
|
||||
|
||||
```sh
|
||||
$ export ETCD_NAME="infra3"
|
||||
@ -129,13 +119,11 @@ $ etcd --listen-client-urls http://10.0.1.13:2379 --advertise-client-urls http:/
|
||||
|
||||
The new member will run as a part of the cluster and immediately begin catching up with the rest of the cluster.
|
||||
|
||||
If 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 adding 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. This behavior only happens 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.
|
||||
If 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 adding 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. This behavior only happens 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 when adding members
|
||||
|
||||
In the following case we have not included our new host in the list of enumerated nodes.
|
||||
If this is a new cluster, the node must be added to the list of initial cluster members.
|
||||
In the following case a new host is not included in the list of enumerated nodes. If this is a new cluster, the node must be added to the list of initial cluster members.
|
||||
|
||||
```sh
|
||||
$ etcd --name infra3 \
|
||||
@ -145,7 +133,7 @@ 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).
|
||||
In this case, give a different address (10.0.1.14:2380) from the one used to join the cluster (10.0.1.13:2380):
|
||||
|
||||
```sh
|
||||
$ etcd --name infra4 \
|
||||
@ -155,7 +143,7 @@ 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 active member in the cluster:
|
||||
If etcd starts using the data directory of a removed member, etcd automatically exits if it connects to any active member in the cluster:
|
||||
|
||||
```sh
|
||||
$ etcd
|
||||
|
@ -16,7 +16,7 @@ etcd takes several certificate related configuration options, either through com
|
||||
|
||||
`--key-file=<path>`: Key for the certificate. Must be unencrypted.
|
||||
|
||||
`--client-cert-auth`: When this is set etcd will check all incoming HTTPS requests for a client certificate signed by the trusted CA, requests that don't supply a valid client certificate will fail.
|
||||
`--client-cert-auth`: When this is set etcd will check all incoming HTTPS requests for a client certificate signed by the trusted CA, requests that don't supply a valid client certificate will fail. If [authentication][auth] is enabled, the certificate provides credentials for the user name given by the Common Name field.
|
||||
|
||||
`--trusted-ca-file=<path>`: Trusted certificate authority.
|
||||
|
||||
@ -219,6 +219,7 @@ Make sure to sign the certificates with a Subject Name the member's public IP ad
|
||||
The certificate needs to be signed for the member's FQDN in its Subject Name, use Subject Alternative Names (short IP SANs) to add the IP address. The `etcd-ca` tool provides `--domain=` option for its `new-cert` command, and openssl can make [it][alt-name] too.
|
||||
|
||||
[cfssl]: https://github.com/cloudflare/cfssl
|
||||
[tls-setup]: /hack/tls-setup
|
||||
[tls-setup]: ../../hack/tls-setup
|
||||
[tls-guide]: https://github.com/coreos/docs/blob/master/os/generate-self-signed-certificates.md
|
||||
[alt-name]: http://wiki.cacert.org/FAQ/subjectAltName
|
||||
[auth]: authentication.md
|
||||
|
@ -4,14 +4,15 @@
|
||||
|
||||
The following table lists etcd support status for common architectures and operating systems,
|
||||
|
||||
| Architecture | Operating System | Status | Maintainers |
|
||||
| ------------ | ---------------- | ------------ | ---------------- |
|
||||
| amd64 | Darwin | Experimental | etcd maintainers |
|
||||
| amd64 | Linux | Stable | etcd maintainers |
|
||||
| amd64 | Windows | Experimental | |
|
||||
| arm64 | Linux | Experimental | @glevand |
|
||||
| arm | Linux | Unstable | |
|
||||
| 386 | Linux | Unstable | |
|
||||
| Architecture | Operating System | Status | Maintainers |
|
||||
| ------------ | ---------------- | ------------ | --------------------------- |
|
||||
| amd64 | Darwin | Experimental | etcd maintainers |
|
||||
| amd64 | Linux | Stable | etcd maintainers |
|
||||
| amd64 | Windows | Experimental | |
|
||||
| arm64 | Linux | Experimental | @glevand |
|
||||
| arm | Linux | Unstable | |
|
||||
| 386 | Linux | Unstable | |
|
||||
| ppc64le | Linux | Stable | etcd maintainers, @mkumatag |
|
||||
|
||||
* etcd-maintainers are listed in https://github.com/coreos/etcd/blob/master/MAINTAINERS.
|
||||
|
||||
@ -33,7 +34,7 @@ etcd has known issues on 32-bit systems due to a bug in the Go runtime. See the
|
||||
|
||||
To avoid inadvertently running a possibly unstable etcd server, `etcd` on unstable or unsupported architectures will print a warning message and immediately exit if the environment variable `ETCD_UNSUPPORTED_ARCH` is not set to the target architecture.
|
||||
|
||||
Currently only the amd64 architecture is officially supported by `etcd`.
|
||||
Currently amd64 and ppc64le architectures are officially supported by `etcd`.
|
||||
|
||||
[go-issue]: https://github.com/golang/go/issues/599
|
||||
[go-atomic]: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
|
||||
|
@ -22,6 +22,12 @@ There are some notable differences between API v2 and API v3:
|
||||
|
||||
Application data can be migrated either offline or online. Offline migration is much simpler than online migration and is recommended.
|
||||
|
||||
Sometimes an etcd cluster will possibly have v3 data which should not be overwritten. In this case, the migration process may want to confirm no v3 data is committed before proceeding. One way to check the cluster has no v3 keys is to issue the following `etcdctl` command, which scans the entire v3 keyspace for any key, expecting `0` as output:
|
||||
|
||||
```sh
|
||||
ETCDCTL_API=3 etcdctl get "" --from-key --keys-only --limit 1 | wc -l
|
||||
```
|
||||
|
||||
### Offline migration
|
||||
|
||||
Offline migration is very simple but requires etcd downtime. If an etcd downtime window spanning from seconds to minutes is acceptable, offline migration is a good choice and is easy to automate.
|
||||
|
77
Documentation/platforms/aws.md
Normal file
77
Documentation/platforms/aws.md
Normal file
@ -0,0 +1,77 @@
|
||||
## Introduction
|
||||
|
||||
This guide assumes operational knowledge of Amazon Web Services (AWS), specifically Amazon Elastic Compute Cloud (EC2). This guide provides an introduction to design considerations when designing an etcd deployment on AWS EC2 and how AWS specific features may be utilized in that context.
|
||||
|
||||
## Capacity planning
|
||||
|
||||
As a critical building block for distributed systems it is crucial to perform adequate capacity planning in order to support the intended cluster workload. As a highly available and strongly consistent data store increasing the number of nodes in an etcd cluster will generally affect performance adversely. This makes sense intuitively, as more nodes means more members for the leader to coordinate state across. The most direct way to increase throughput and decrease latency of an etcd cluster is allocate more disk I/O, network I/O, CPU, and memory to cluster members. In the event it is impossible to temporarily divert incoming requests to the cluster, scaling the EC2 instances which comprise the etcd cluster members one at a time may improve performance. It is, however, best to avoid bottlenecks through capacity planning.
|
||||
|
||||
The etcd team has produced a [hardware recommendation guide]( ../op-guide/hardware.md) which is very useful for “ballparking” how many nodes and what instance type are necessary for a cluster.
|
||||
|
||||
AWS provides a service for creating groups of EC2 instances which are dynamically sized to match load on the instances. Using an Auto Scaling Group ([ASG](http://docs.aws.amazon.com/autoscaling/latest/userguide/AutoScalingGroup.html)) to dynamically scale an etcd cluster is not recommended for several reasons including:
|
||||
|
||||
* etcd performance is generally inversely proportional to the number of members in a cluster due to the synchronous replication which provides strong consistency of data stored in etcd
|
||||
* the operational complexity of adding [lifecycle hooks](http://docs.aws.amazon.com/autoscaling/latest/userguide/lifecycle-hooks.html) to properly add and remove members from an etcd cluster by modifying the [runtime configuration](../op-guide/runtime-configuration.md)
|
||||
|
||||
Auto Scaling Groups do provide a number of benefits besides cluster scaling which include:
|
||||
|
||||
* distribution of EC2 instances across Availability Zones (AZs)
|
||||
* EC2 instance fail over across AZs
|
||||
* consolidated monitoring and life cycle control of instances within an ASG
|
||||
|
||||
The use of an ASG to create a [self healing etcd cluster](#self-healing) is one of the design considerations when deploying an etcd cluster to AWS.
|
||||
|
||||
## Cluster design
|
||||
|
||||
The purpose of this section is to provide foundational guidance for deploying etcd on AWS. The discussion will be framed by the following three critical design criteria about the etcd cluster itself:
|
||||
|
||||
* block device provider: limited to the tradeoffs between EBS or instance storage (InstanceStore)
|
||||
* cluster topology: how many nodes should make up an etcd cluster; should these nodes be distributed over multiple AZs
|
||||
* managing etcd members: creating a static cluster of EC2 instances or using an ASG.
|
||||
|
||||
The intended cluster workload should dictate the cluster design. A configuration store for microservices may require different design considerations than a distributed lock service, a secrets store, or a Kubernetes control plane. Cluster design tradeoffs include considerations such as:
|
||||
|
||||
* availability
|
||||
* data durability after member failure
|
||||
* performance/throughput
|
||||
* self healing
|
||||
|
||||
### Availability
|
||||
|
||||
Instance availability on AWS is ultimately determined by the Amazon EC2 Region Service Level Agreement ([SLA](https://aws.amazon.com/ec2/sla/)) which is the policy by which Amazon describes their precise definition of a regional outage.
|
||||
|
||||
In the context of an etcd cluster this means a cluster must contain a minimum of three members where EC2 instances are spread across at least two AZs in order for an etcd cluster to be considered highly available at a Regional level.
|
||||
|
||||
For most use cases the additional latency associated with a cluster spanning across Availability Zones will introduce a negligible performance impact.
|
||||
|
||||
Availability considerations apply to all components of an application; if the application which accesses the etcd cluster will only be deployed to a single Availability Zone it may not make sense to make the etcd cluster highly available across zones.
|
||||
|
||||
### Data durability after member failure
|
||||
|
||||
A highly available etcd cluster is resilient to member loss, however, it is important to consider data durability in the event of disaster when designing an etcd deployment. Deploying etcd on AWS supports multiple mechanisms for data durability.
|
||||
|
||||
* replication: etcd replicates all data to all members of the etcd cluster. Therefore, given more members in the cluster and more independent failure domains, the less likely that data stored in an etcd cluster will be permanently lost in the event of disaster.
|
||||
* Point in time etcd snapshotting: the etcd v3 API introduced support for snapshotting clusters. The operation is cheap enough (completing in the order of minutes) to run quite frequently and the resulting archives can be archived in a storage service like Amazon Simple Storage Service (S3).
|
||||
* Amazon Elastic Block Storage (EBS): an EBS volume is a replicated network attached block device which have stronger storage safety guarantees than InstanceStore which has a life cycle associated with the life cycle of the attached EC2 instance. The life cycle of an EBS volume is not necessarily tied to an EC2 instance and can be detached and snapshotted independently which means that a single node etcd cluster backed by an EBS volume can provide a fairly reasonable level of data durability.
|
||||
|
||||
### Performance/Throughput
|
||||
|
||||
The performance of an etcd cluster is roughly quantifiable through latency and throughput metrics which are primarily affected by disk and network performance. Detailed performance planning information is provided in the [performance section](../op-guide/performance.md) of the etcd operations guide.
|
||||
|
||||
#### Network
|
||||
|
||||
AWS offers EC2 Placement Groups which allow the collocation of EC2 instances within a single Availability Zone which can be utilized in order to minimize network latency between etcd members in the cluster. It is important to remember that collocation of etcd nodes within a single AZ will provide weaker fault tolerance than distributing members across multiple AZs. [Enhanced networking for EC2 instances](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enhanced-networking.html) may also improve network performance of individual EC2 instances.
|
||||
|
||||
#### Disk
|
||||
|
||||
AWS provides two basic types of block storage: [EBS volumes](https://aws.amazon.com/ebs/) and [EC2 Instance Store](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html). As mentioned, an EBS volume is a network attached block device while instance storage is directly attached to the hypervisor of the EC2 host. EBS volumes will generally have higher latency, lower throughput, and greater performance variance than Instance Store volumes. If performance, rather than data safety, is the primary concern it is highly recommended that instance storage on the EC2 instances be utilized. Remember that the amount of available instance storage varies by EC2 [instance types](https://aws.amazon.com/ec2/instance-types/) which may impose additional performance considerations.
|
||||
|
||||
Inconsistent EBS volume performance can introduce etcd cluster instability. [Provisioned IOPS](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html#EBSVolumeTypes_piops) can provide more consistent performance than general purpose SSD EBS volumes. More information about EBS volume performance is available [from AWS](https://aws.amazon.com/ebs/details/) and Datadog has shared their experience with [getting optimal performance with AWS EBS Provisioned IOPS](https://www.datadoghq.com/blog/aws-ebs-provisioned-iops-getting-optimal-performance/) in their engineering blog.
|
||||
|
||||
### Self healing
|
||||
|
||||
While using an ASG to scale the size of an etcd cluster is not recommended, an ASG can be used effectively to maintain the desired number of nodes in the event of node failure. The maintenance of a stable number of etcd nodes will provide the etcd cluster with a measure of self healing.
|
||||
|
||||
### Next steps
|
||||
|
||||
The operational life cycle of an etcd cluster can be greatly simplified through the use of the etcd-operator. The open source etcd operator is a Kubernetes control plane operator which deploys and manages etcd clusters atop Kubernetes. While still in its early stages the etcd-operator already offers periodic backups to S3, detection and replacement of failed nodes, and automated disaster recovery from backups in the event of permanent quorum loss.
|
203
Documentation/platforms/container-linux-systemd.md
Normal file
203
Documentation/platforms/container-linux-systemd.md
Normal file
@ -0,0 +1,203 @@
|
||||
# Run etcd on Container Linux with systemd
|
||||
|
||||
The following guide shows how to run etcd with [systemd][systemd-docs] under [Container Linux][container-linux-docs].
|
||||
|
||||
## Provisioning an etcd cluster
|
||||
|
||||
Cluster bootstrapping in Container Linux is simplest with [Ignition][container-linux-ignition]; `coreos-metadata.service` dynamically fetches the machine's IP for discovery. Note that etcd's discovery service protocol is only meant for bootstrapping, and cannot be used with runtime reconfiguration or cluster monitoring.
|
||||
|
||||
The [Container Linux Config Transpiler][container-linux-ct] compiles etcd configuration files into Ignition configuration files:
|
||||
|
||||
```yaml container-linux-config:norender
|
||||
etcd:
|
||||
version: 3.2.0
|
||||
name: s1
|
||||
data_dir: /var/lib/etcd
|
||||
advertise_client_urls: http://{PUBLIC_IPV4}:2379
|
||||
initial_advertise_peer_urls: http://{PRIVATE_IPV4}:2380
|
||||
listen_client_urls: http://0.0.0.0:2379
|
||||
listen_peer_urls: http://{PRIVATE_IPV4}:2380
|
||||
discovery: https://discovery.etcd.io/<token>
|
||||
```
|
||||
|
||||
`ct` would produce the following Ignition Config:
|
||||
|
||||
```
|
||||
$ ct --platform=gce --in-file /tmp/ct-etcd.cnf
|
||||
{"ignition":{"version":"2.0.0","config"...
|
||||
```
|
||||
|
||||
```json ignition-config
|
||||
{
|
||||
"ignition":{"version":"2.0.0","config":{}},
|
||||
"storage":{},
|
||||
"systemd":{
|
||||
"units":[{
|
||||
"name":"etcd-member.service",
|
||||
"enable":true,
|
||||
"dropins":[{
|
||||
"name":"20-clct-etcd-member.conf",
|
||||
"contents":"[Unit]\nRequires=coreos-metadata.service\nAfter=coreos-metadata.service\n\n[Service]\nEnvironmentFile=/run/metadata/coreos\nEnvironment=\"ETCD_IMAGE_TAG=v3.1.8\"\nExecStart=\nExecStart=/usr/lib/coreos/etcd-wrapper $ETCD_OPTS \\\n --name=\"s1\" \\\n --data-dir=\"/var/lib/etcd\" \\\n --listen-peer-urls=\"http://${COREOS_GCE_IP_LOCAL_0}:2380\" \\\n --listen-client-urls=\"http://0.0.0.0:2379\" \\\n --initial-advertise-peer-urls=\"http://${COREOS_GCE_IP_LOCAL_0}:2380\" \\\n --advertise-client-urls=\"http://${COREOS_GCE_IP_EXTERNAL_0}:2379\" \\\n --discovery=\"https://discovery.etcd.io/\u003ctoken\u003e\""}]}]},
|
||||
"networkd":{},
|
||||
"passwd":{}}
|
||||
```
|
||||
|
||||
To avoid accidental misconfiguration, the transpiler helpfully verifies etcd configurations when generating Ignition files:
|
||||
|
||||
```yaml container-linux-config:norender
|
||||
etcd:
|
||||
version: 3.2.0
|
||||
name: s1
|
||||
data_dir_x: /var/lib/etcd
|
||||
advertise_client_urls: http://{PUBLIC_IPV4}:2379
|
||||
initial_advertise_peer_urls: http://{PRIVATE_IPV4}:2380
|
||||
listen_client_urls: http://0.0.0.0:2379
|
||||
listen_peer_urls: http://{PRIVATE_IPV4}:2380
|
||||
discovery: https://discovery.etcd.io/<token>
|
||||
```
|
||||
|
||||
```
|
||||
$ ct --platform=gce --in-file /tmp/ct-etcd.cnf
|
||||
warning at line 3, column 2
|
||||
Config has unrecognized key: data_dir_x
|
||||
```
|
||||
|
||||
See [Container Linux Provisioning][container-linux-provision] for more details.
|
||||
|
||||
## etcd 3.x service
|
||||
|
||||
[Container Linux][container-linux-docs] does not include etcd 3.x binaries by default. Different versions of etcd 3.x can be fetched via `etcd-member.service`.
|
||||
|
||||
Confirm unit file exists:
|
||||
|
||||
```
|
||||
systemctl cat etcd-member.service
|
||||
```
|
||||
|
||||
Check if the etcd service is running:
|
||||
|
||||
```
|
||||
systemctl status etcd-member.service
|
||||
```
|
||||
|
||||
Example systemd drop-in unit to override the default service settings:
|
||||
|
||||
```bash
|
||||
cat > /tmp/20-cl-etcd-member.conf <<EOF
|
||||
[Service]
|
||||
Environment="ETCD_IMAGE_TAG=v3.2.0"
|
||||
Environment="ETCD_DATA_DIR=/var/lib/etcd"
|
||||
Environment="ETCD_SSL_DIR=/etc/ssl/certs"
|
||||
Environment="ETCD_OPTS=--name s1 \
|
||||
--listen-client-urls https://10.240.0.1:2379 \
|
||||
--advertise-client-urls https://10.240.0.1:2379 \
|
||||
--listen-peer-urls https://10.240.0.1:2380 \
|
||||
--initial-advertise-peer-urls https://10.240.0.1:2380 \
|
||||
--initial-cluster s1=https://10.240.0.1:2380,s2=https://10.240.0.2:2380,s3=https://10.240.0.3:2380 \
|
||||
--initial-cluster-token mytoken \
|
||||
--initial-cluster-state new \
|
||||
--client-cert-auth \
|
||||
--trusted-ca-file /etc/ssl/certs/etcd-root-ca.pem \
|
||||
--cert-file /etc/ssl/certs/s1.pem \
|
||||
--key-file /etc/ssl/certs/s1-key.pem \
|
||||
--peer-client-cert-auth \
|
||||
--peer-trusted-ca-file /etc/ssl/certs/etcd-root-ca.pem \
|
||||
--peer-cert-file /etc/ssl/certs/s1.pem \
|
||||
--peer-key-file /etc/ssl/certs/s1-key.pem \
|
||||
--auto-compaction-retention 1"
|
||||
EOF
|
||||
mv /tmp/20-cl-etcd-member.conf /etc/systemd/system/etcd-member.service.d/20-cl-etcd-member.conf
|
||||
```
|
||||
|
||||
Or use a Container Linux Config:
|
||||
|
||||
```yaml container-linux-config:norender
|
||||
systemd:
|
||||
units:
|
||||
- name: etcd-member.service
|
||||
dropins:
|
||||
- name: conf1.conf
|
||||
contents: |
|
||||
[Service]
|
||||
Environment="ETCD_SSL_DIR=/etc/ssl/certs"
|
||||
|
||||
etcd:
|
||||
version: 3.2.0
|
||||
name: s1
|
||||
data_dir: /var/lib/etcd
|
||||
listen_client_urls: https://0.0.0.0:2379
|
||||
advertise_client_urls: https://{PUBLIC_IPV4}:2379
|
||||
listen_peer_urls: https://{PRIVATE_IPV4}:2380
|
||||
initial_advertise_peer_urls: https://{PRIVATE_IPV4}:2380
|
||||
initial_cluster: s1=https://{PRIVATE_IPV4}:2380,s2=https://10.240.0.2:2380,s3=https://10.240.0.3:2380
|
||||
initial_cluster_token: mytoken
|
||||
initial_cluster_state: new
|
||||
client_cert_auth: true
|
||||
trusted_ca_file: /etc/ssl/certs/etcd-root-ca.pem
|
||||
cert-file: /etc/ssl/certs/s1.pem
|
||||
key-file: /etc/ssl/certs/s1-key.pem
|
||||
peer-client-cert-auth: true
|
||||
peer-trusted-ca-file: /etc/ssl/certs/etcd-root-ca.pem
|
||||
peer-cert-file: /etc/ssl/certs/s1.pem
|
||||
peer-key-file: /etc/ssl/certs/s1-key.pem
|
||||
auto-compaction-retention: 1
|
||||
```
|
||||
|
||||
```
|
||||
$ ct --platform=gce --in-file /tmp/ct-etcd.cnf
|
||||
{"ignition":{"version":"2.0.0","config"...
|
||||
```
|
||||
|
||||
To see all runtime drop-in changes for system units:
|
||||
|
||||
```
|
||||
systemd-delta --type=extended
|
||||
```
|
||||
|
||||
To enable and start:
|
||||
|
||||
```
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now etcd-member.service
|
||||
```
|
||||
|
||||
To see the logs:
|
||||
|
||||
```
|
||||
journalctl --unit etcd-member.service --lines 10
|
||||
```
|
||||
|
||||
To stop and disable the service:
|
||||
|
||||
```
|
||||
systemctl disable --now etcd-member.service
|
||||
```
|
||||
|
||||
## etcd 2.x service
|
||||
|
||||
[Container Linux][container-linux-docs] includes a unit file `etcd2.service` for etcd 2.x, which will be removed in the near future. See [Container Linux FAQ][container-linux-faq] for more details.
|
||||
|
||||
Confirm unit file is installed:
|
||||
|
||||
```
|
||||
systemctl cat etcd2.service
|
||||
```
|
||||
|
||||
Check if the etcd service is running:
|
||||
|
||||
```
|
||||
systemctl status etcd2.service
|
||||
```
|
||||
|
||||
To stop and disable:
|
||||
|
||||
```
|
||||
systemctl disable --now etcd2.service
|
||||
```
|
||||
|
||||
[systemd-docs]: https://github.com/systemd/systemd
|
||||
[container-linux-docs]: https://coreos.com/os/docs/latest
|
||||
[container-linux-faq]: https://github.com/coreos/docs/blob/master/etcd/os-faq.md
|
||||
[container-linux-provision]: https://github.com/coreos/docs/blob/master/os/provisioning.md
|
||||
[container-linux-ignition]: https://github.com/coreos/docs/blob/master/ignition/what-is-ignition.md
|
||||
[container-linux-ct]: https://github.com/coreos/container-linux-config-transpiler
|
@ -1,23 +1,18 @@
|
||||
# FreeBSD
|
||||
|
||||
Starting with version 0.1.2 both etcd and etcdctl have been ported to FreeBSD and can
|
||||
be installed either via packages or ports system. Their versions have been recently
|
||||
updated to 0.2.0 so now you can enjoy using etcd and etcdctl on FreeBSD 10.0 (RC4 as
|
||||
of now) and 9.x where they have been tested. They might also work when installed from
|
||||
ports on earlier versions of FreeBSD, but your mileage may vary.
|
||||
Starting with version 0.1.2 both etcd and etcdctl have been ported to FreeBSD and can be installed either via packages or ports system. Their versions have been recently updated to 0.2.0 so now etcd and etcdctl can be enjoyed on FreeBSD 10.0 (RC4 as of now) and 9.x, where they have been tested. They might also work when installed from ports on earlier versions of FreeBSD, but it is untested; caveat emptor.
|
||||
|
||||
## Installation
|
||||
|
||||
### Using pkgng package system
|
||||
|
||||
1. If you do not have pkgng installed, install it with command `pkg` and answering 'Y'
|
||||
when asked
|
||||
1. If pkgng is not installed, install it with command `pkg` and answering 'Y' when asked.
|
||||
|
||||
2. Update your repository data with `pkg update`
|
||||
2. Update the repository data with `pkg update`.
|
||||
|
||||
3. Install etcd with `pkg install coreos-etcd coreos-etcdctl`
|
||||
3. Install etcd with `pkg install coreos-etcd coreos-etcdctl`.
|
||||
|
||||
4. Verify successful installation with `pkg info | grep etcd` and you should get:
|
||||
4. Verify successful installation by confirming `pkg info | grep etcd` matches:
|
||||
|
||||
```
|
||||
r@fbsd10:/ # pkg info | grep etcd
|
||||
@ -26,21 +21,17 @@ coreosetcdctl0.2.0 Simple commandline client for et
|
||||
r@fbsd10:/ #
|
||||
```
|
||||
|
||||
5. You’re ready to use etcd and etcdctl! For more information about using pkgng, please
|
||||
see: http://www.freebsd.org/doc/handbook/pkgngintro.html
|
||||
5. etcd and etcdctl are ready to use! For more information about using pkgng, please see: http://www.freebsd.org/doc/handbook/pkgngintro.html
|
||||
|
||||
### Using ports system
|
||||
|
||||
1. If you do not have ports installed, install with with `portsnap fetch extract` (it
|
||||
may take some time depending on your hardware and network connection)
|
||||
1. If ports is not installed, install with `portsnap fetch extract` (it may take some time depending on hardware and network connection).
|
||||
|
||||
2. Build etcd with `cd /usr/ports/devel/etcd && make install clean`, you
|
||||
will get an option to build and install documentation and etcdctl with it.
|
||||
2. Build etcd with `cd /usr/ports/devel/etcd && make install clean`. There will be an option to build and install documentation and etcdctl with it.
|
||||
|
||||
3. If you haven't installed it with etcdctl, and you would like to install it later, you can build it
|
||||
with `cd /usr/ports/devel/etcdctl && make install clean`
|
||||
3. If etcd wasn't installed with etcdctl, it can be built later with `cd /usr/ports/devel/etcdctl && make install clean`.
|
||||
|
||||
4. Verify successful installation with `pkg info | grep etcd` and you should get:
|
||||
4. Verify successful installation by confirming `pkg info | grep etcd` matches:
|
||||
|
||||
|
||||
```
|
||||
@ -50,13 +41,8 @@ coreosetcdctl0.2.0 Simple commandline client for et
|
||||
r@fbsd10:/ #
|
||||
```
|
||||
|
||||
5. You’re ready to use etcd and etcdctl! For more information about using ports system,
|
||||
please see: https://www.freebsd.org/doc/handbook/portsusing.html
|
||||
5. etcd and etcdctl are ready to use! For more information about using ports system, please see: https://www.freebsd.org/doc/handbook/portsusing.html
|
||||
|
||||
## Issues
|
||||
|
||||
If you find any issues with the build/install procedure or you've found a problem that
|
||||
you've verified is local to FreeBSD version only (for example, by not being able to
|
||||
reproduce it on any other platform, like OSX or Linux), please sent a
|
||||
problem report using this page for more
|
||||
information: http://www.freebsd.org/sendpr.html
|
||||
If there are any issues with the build/install procedure or there's a problem that is local to FreeBSD only (for example, by not being able to reproduce it on any other platform, like OSX or Linux), please send a problem report using this page for more information: http://www.freebsd.org/sendpr.html
|
||||
|
@ -2,6 +2,15 @@
|
||||
|
||||
This document tracks people and use cases for etcd in production. By creating a list of production use cases we hope to build a community of advisors that we can reach out to with experience using various etcd applications, operation environments, and cluster sizes. The etcd development team may reach out periodically to check-in on how etcd is working in the field and update this list.
|
||||
|
||||
## All Kubernetes Users
|
||||
|
||||
- *Application*: https://kubernetes.io/
|
||||
- *Environments*: AWS, OpenStack, Azure, Google Cloud, Huawei Cloud, Bare Metal, etc
|
||||
|
||||
**This is a meta user; please feel free to document specific Kubernetes clusters!**
|
||||
|
||||
All Kubernetes clusters use etcd as their primary data store. This means etcd's users include such companies as [Niantic, Inc Pokemon Go](https://cloudplatform.googleblog.com/2016/09/bringing-Pokemon-GO-to-life-on-Google-Cloud.html), [Box](https://blog.box.com/blog/kubernetes-box-microservices-maximum-velocity/), [CoreOS](https://coreos.com/tectonic), [Ticketmaster](https://www.youtube.com/watch?v=wqXVKneP0Hg), [Salesforce](https://www.salesforce.com) and many many more.
|
||||
|
||||
## discovery.etcd.io
|
||||
|
||||
- *Application*: https://github.com/coreos/discovery.etcd.io
|
||||
@ -50,7 +59,7 @@ Radius Intelligence uses Kubernetes running CoreOS to containerize and scale int
|
||||
|
||||
## Vonage
|
||||
|
||||
- *Application*: system configuration for microservices, scheduling, locks (future - service discovery)
|
||||
- *Application*: kubernetes, vault backend, system configuration for microservices, scheduling, locks (future - service discovery)
|
||||
- *Launched*: August 2015
|
||||
- *Cluster Size*: 2 clusters of 5 members in 2 DCs, n local proxies 1-to-1 with microservice, (ssl and SRV look up)
|
||||
- *Order of Data Size*: kilobytes
|
||||
@ -58,5 +67,173 @@ Radius Intelligence uses Kubernetes running CoreOS to containerize and scale int
|
||||
- *Environment*: VMWare, AWS
|
||||
- *Backups*: Daily snapshots on VMs. Backups done for upgrades.
|
||||
|
||||
## PD
|
||||
|
||||
- *Application*: embed etcd
|
||||
- *Launched*: Mar 2016
|
||||
- *Cluster Size*: 3 or 5 members
|
||||
- *Order of Data Size*: megabytes
|
||||
- *Operator*: PingCAP, Inc.
|
||||
- *Environment*: Bare Metal, AWS, etc.
|
||||
- *Backups*: None.
|
||||
|
||||
PD(Placement Driver) is the central controller in the TiDB cluster. It saves the cluster meta information, schedule the data, allocate the global unique timestamp for the distributed transaction, etc. It embeds etcd to supply high availability and auto failover.
|
||||
|
||||
## Canal
|
||||
|
||||
- *Application*: system configuration for overlay network
|
||||
- *Launched*: June 2016
|
||||
- *Cluster Size*: 3 members for each cluster
|
||||
- *Order of Data Size*: kilobytes
|
||||
- *Operator*: Huawei Euler Department
|
||||
- *Environment*: [Huawei Cloud](http://www.hwclouds.com/product/cce.html)
|
||||
- *Backups*: None, all data can be recreated if necessary.
|
||||
|
||||
[teamcity]: https://www.jetbrains.com/teamcity/
|
||||
[raoofm]:https://github.com/raoofm
|
||||
|
||||
## Qiniu Cloud
|
||||
|
||||
- *Application*: system configuration for microservices, distributed locks
|
||||
- *Launched*: Jan. 2016
|
||||
- *Cluster Size*: 3 members each with several clusters
|
||||
- *Order of Data Size*: kilobytes
|
||||
- *Operator*: Pandora, chenchao@qiniu.com
|
||||
- *Environment*: Baremetal
|
||||
- *Backups*: None, all data can be recreated if necessary
|
||||
|
||||
## QingCloud
|
||||
|
||||
- *Application*: [QingCloud][qingcloud] appcenter cluster for service discovery as [metad][metad] backend.
|
||||
- *Launched*: December 2016
|
||||
- *Cluster Size*: 1 cluster of 3 members per user.
|
||||
- *Order of Data Size*: kilobytes
|
||||
- *Operator*: [yunify][yunify]
|
||||
- *Environment*: QingCloud IaaS
|
||||
- *Backups*: None, all data can be recreated if necessary.
|
||||
|
||||
[metad]:https://github.com/yunify/metad
|
||||
[yunify]:https://github.com/yunify
|
||||
[qingcloud]:https://qingcloud.com/
|
||||
|
||||
|
||||
## Yandex
|
||||
|
||||
- *Application*: system configuration for services, service discovery
|
||||
- *Launched*: March 2016
|
||||
- *Cluster Size*: 3 clusters of 5 members
|
||||
- *Order of Data Size*: several gigabytes
|
||||
- *Operator*: Yandex; [nekto0n][nekto0n]
|
||||
- *Environment*: Bare Metal
|
||||
- *Backups*: None
|
||||
|
||||
[nekto0n]:https://github.com/nekto0n
|
||||
|
||||
## Tencent Games
|
||||
|
||||
- *Application*: Meta data and configuration data for service discovery, Kubernetes, etc.
|
||||
- *Launched*: Jan. 2015
|
||||
- *Cluster Size*: 3 members each with 10s of clusters
|
||||
- *Order of Data Size*: 10s of Megabytes
|
||||
- *Operator*: Tencent Game Operations Department
|
||||
- *Environment*: Baremetal
|
||||
- *Backups*: Periodic sync to backup server
|
||||
|
||||
In Tencent games, we use Docker and Kubernetes to deploy and run our applications, and use etcd to save meta data for service discovery, Kubernetes, etc.
|
||||
|
||||
## Hyper.sh
|
||||
|
||||
- *Application*: Kubernetes, distributed locks, etc.
|
||||
- *Launched*: April 2016
|
||||
- *Cluster Size*: 1 cluster of 3 members
|
||||
- *Order of Data Size*: 10s of MB
|
||||
- *Operator*: Hyper.sh
|
||||
- *Environment*: Baremetal
|
||||
- *Backups*: None, all data can be recreated if necessary.
|
||||
|
||||
In [hyper.sh][hyper.sh], the container service is backed by [hypernetes][hypernetes], a multi-tenant kubernetes distro. Moreover, we use etcd to coordinate the multiple manage services and store global meta data.
|
||||
|
||||
[hypernetes]:https://github.com/hyperhq/hypernetes
|
||||
[Hyper.sh]:https://www.hyper.sh
|
||||
|
||||
## Meitu
|
||||
- *Application*: system configuration for services, service discovery, kubernetes in test environment
|
||||
- *Launched*: October 2015
|
||||
- *Cluster Size*: 1 cluster of 3 members
|
||||
- *Order of Data Size*: megabytes
|
||||
- *Operator*: Meitu, hxj@meitu.com, [shafreeck][shafreeck]
|
||||
- *Environment*: Bare Metal
|
||||
- *Backups*: None, all data can be recreated if necessary.
|
||||
|
||||
[shafreeck]:https://github.com/shafreeck
|
||||
|
||||
## Grab
|
||||
- *Application*: system configuration for services, service discovery
|
||||
- *Launched*: June 2016
|
||||
- *Cluster Size*: 1 cluster of 7 members
|
||||
- *Order of Data Size*: megabytes
|
||||
- *Operator*: Grab, [taxitan][taxitan], [reterVision][reterVision]
|
||||
- *Environment*: AWS
|
||||
- *Backups*: None, all data can be recreated if necessary.
|
||||
|
||||
[taxitan]:https://github.com/taxitan
|
||||
[reterVision]:https://github.com/reterVision
|
||||
|
||||
## DaoCloud.io
|
||||
|
||||
- *Application*: container management
|
||||
- *Launched*: Sep. 2015
|
||||
- *Cluster Size*: 1000+ deployments, each deployment contains a 3 node cluster.
|
||||
- *Order of Data Size*: 100s of Megabytes
|
||||
- *Operator*: daocloud.io
|
||||
- *Environment*: Baremetal and virtual machines
|
||||
- *Backups*: None, all data can be recreated if necessary.
|
||||
|
||||
In [DaoCloud][DaoCloud], we use Docker and Swarm to deploy and run our applications, and we use etcd to save metadata for service discovery.
|
||||
|
||||
[DaoCloud]:https://www.daocloud.io
|
||||
|
||||
## Branch.io
|
||||
|
||||
- *Application*: Kubernetes
|
||||
- *Launched*: April 2016
|
||||
- *Cluster Size*: Multiple clusters, multiple sizes
|
||||
- *Order of Data Size*: 100s of Megabytes
|
||||
- *Operator*: branch.io
|
||||
- *Environment*: AWS, Kubernetes
|
||||
- *Backups*: EBS volume backups
|
||||
|
||||
At [Branch][branch], we use kubernetes heavily as our core microservice platform for staging and production.
|
||||
|
||||
[branch]: https://branch.io
|
||||
|
||||
## Baidu Waimai
|
||||
|
||||
- *Application*: SkyDNS, Kubernetes, UDC, CMDB and other distributed systems
|
||||
- *Launched*: April. 2016
|
||||
- *Cluster Size*: 3 clusters of 5 members
|
||||
- *Order of Data Size*: several gigabytes
|
||||
- *Operator*: Baidu Waimai Operations Department
|
||||
- *Environment*: CentOS 6.5
|
||||
- *Backups*: backup scripts
|
||||
|
||||
## Salesforce.com
|
||||
|
||||
- *Application*: Kubernetes
|
||||
- *Launched*: Jan 2017
|
||||
- *Cluster Size*: Multiple clusters of 3 members
|
||||
- *Order of Data Size*: 100s of Megabytes
|
||||
- *Operator*: Salesforce.com (krmayankk@github)
|
||||
- *Environment*: BareMetal
|
||||
- *Backups*: None, all data can be recreated
|
||||
|
||||
## Hosted Graphite
|
||||
|
||||
- *Application*: Service discovery, locking, ephemeral application data
|
||||
- *Launched*: January 2017
|
||||
- *Cluster Size*: 2 clusters of 7 members
|
||||
- *Order of Data Size*: Megabytes
|
||||
- *Operator*: Hosted Graphite (sre@hostedgraphite.com)
|
||||
- *Environment*: Bare Metal
|
||||
- *Backups*: None, all data is considered ephemeral.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Reporting bugs
|
||||
|
||||
If any part of the etcd project has bugs or documentation mistakes, please let us know by [opening an issue][issue]. We treat bugs and mistakes very seriously and believe no issue is too small. Before creating a bug report, please check that an issue reporting the same problem does not already exist.
|
||||
If any part of the etcd project has bugs or documentation mistakes, please let us know by [opening an issue][etcd-issue]. We treat bugs and mistakes very seriously and believe no issue is too small. Before creating a bug report, please check that an issue reporting the same problem does not already exist.
|
||||
|
||||
To make the bug report accurate and easy to understand, please try to create bug reports that are:
|
||||
|
||||
|
@ -6,33 +6,16 @@ The network isn't the only source of latency. Each request and response may be i
|
||||
|
||||
## Time parameters
|
||||
|
||||
The underlying distributed consensus protocol relies on two separate time parameters to ensure that nodes can handoff leadership if one stalls or goes offline.
|
||||
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.
|
||||
For best practices, the parameter should be set around round-trip time between members.
|
||||
By default, etcd uses a `100ms` heartbeat interval.
|
||||
The underlying distributed consensus protocol relies on two separate time parameters to ensure that nodes can handoff leadership if one stalls or goes offline. 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.
|
||||
For best practices, the parameter should be set around round-trip time between members. 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 `1000ms` election timeout.
|
||||
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 `1000ms` election timeout.
|
||||
|
||||
Adjusting these values is a trade off.
|
||||
The value of heartbeat interval is recommended to be around the maximum of average round-trip time (RTT) between members, normally around 0.5-1.5x the round-trip time.
|
||||
If heartbeat interval is too low, etcd will send unnecessary messages that increase the usage of CPU and network resources.
|
||||
On the other side, a too high heartbeat interval leads to high election timeout. Higher election timeout takes longer time to detect a leader failure.
|
||||
The easiest way to measure round-trip time (RTT) is to use [PING utility][ping].
|
||||
Adjusting these values is a trade off. The value of heartbeat interval is recommended to be around the maximum of average round-trip time (RTT) between members, normally around 0.5-1.5x the round-trip time. If heartbeat interval is too low, etcd will send unnecessary messages that increase the usage of CPU and network resources. On the other side, a too high heartbeat interval leads to high election timeout. Higher election timeout takes longer time to detect a leader failure. The easiest way to measure round-trip time (RTT) is to use [PING utility][ping].
|
||||
|
||||
The election timeout should be set based on the heartbeat interval and average round-trip time between members.
|
||||
Election timeouts must be at least 10 times the round-trip time so it can account for variance in the network.
|
||||
For example, if the round-trip time between members is 10ms then the election timeout should be at least 100ms.
|
||||
The election timeout should be set based on the heartbeat interval and average round-trip time between members. Election timeouts must be at least 10 times the round-trip time so it can account for variance in the network. For example, if the round-trip time between members is 10ms then the election timeout should be at least 100ms.
|
||||
|
||||
The election timeout should be set to at least 5 to 10 times the heartbeat interval to account for variance in leader replication.
|
||||
For a heartbeat interval of 50ms, set the election timeout to at least 250ms - 500ms.
|
||||
|
||||
The upper limit of election timeout is 50000ms (50s), which should only be used when deploying a globally-distributed etcd cluster.
|
||||
A reasonable round-trip time for the continental United States is 130ms, and the time between US and Japan is around 350-400ms.
|
||||
If the network has uneven performance or regular packet delays/loss then it is possible that a couple of retries may be necessary to successfully send a packet. So 5s is a safe upper limit of global round-trip time.
|
||||
As the election timeout should be an order of magnitude bigger than broadcast time, in the case of ~5s for a globally distributed cluster, then 50 seconds becomes a reasonable maximum.
|
||||
The upper limit of election timeout is 50000ms (50s), which should only be used when deploying a globally-distributed etcd cluster. A reasonable round-trip time for the continental United States is 130ms, and the time between US and Japan is around 350-400ms. If the network has uneven performance or regular packet delays/loss then it is possible that a couple of retries may be necessary to successfully send a packet. So 5s is a safe upper limit of global round-trip time. As the election timeout should be an order of magnitude bigger than broadcast time, in the case of ~5s for a globally distributed cluster, then 50 seconds becomes a reasonable maximum.
|
||||
|
||||
The heartbeat interval and election timeout value should be the same for all members in one cluster. Setting different values for etcd members may disrupt cluster stability.
|
||||
|
||||
@ -50,18 +33,13 @@ The values are specified in milliseconds.
|
||||
|
||||
## Snapshots
|
||||
|
||||
etcd appends all key changes to a log file.
|
||||
This log grows forever and is a complete linear history of every change made to the keys.
|
||||
A complete history works well for lightly used clusters but clusters that are heavily used would carry around a large log.
|
||||
etcd appends all key changes to a log file. This log grows forever and is a complete linear history of every change made to the keys. A complete history works well for lightly used clusters but clusters that are heavily used would carry around a large log.
|
||||
|
||||
To avoid having a huge log etcd makes periodic snapshots.
|
||||
These snapshots provide a way for etcd to compact the log by saving the current state of the system and removing old logs.
|
||||
To avoid having a huge log etcd makes periodic snapshots. These snapshots provide a way for etcd to compact the log by saving the current state of the system and removing old logs.
|
||||
|
||||
### Snapshot tuning
|
||||
|
||||
Creating snapshots can be expensive so they're only created after a given number of changes to etcd.
|
||||
By default, snapshots will be made after every 10,000 changes.
|
||||
If etcd's memory usage and disk usage are too high, try lowering the snapshot threshold by setting the following on the command line:
|
||||
Creating snapshots with the V2 backend can be expensive, so snapshots are only created after a given number of changes to etcd. By default, snapshots will be made after every 10,000 changes. If etcd's memory usage and disk usage are too high, try lowering the snapshot threshold by setting the following on the command line:
|
||||
|
||||
```sh
|
||||
# Command line arguments:
|
||||
@ -71,6 +49,17 @@ $ etcd --snapshot-count=5000
|
||||
$ ETCD_SNAPSHOT_COUNT=5000 etcd
|
||||
```
|
||||
|
||||
## Disk
|
||||
|
||||
An etcd cluster is very sensitive to disk latencies. Since etcd must persist proposals to its log, disk activity from other processes may cause long `fsync` latencies. The upshot is etcd may miss heartbeats, causing request timeouts and temporary leader loss. An etcd server can sometimes stably run alongside these processes when given a high disk priority.
|
||||
|
||||
On Linux, etcd's disk priority can be configured with `ionice`:
|
||||
|
||||
```sh
|
||||
# best effort, highest priority
|
||||
$ sudo ionice -c2 -n0 -p `pgrep etcd`
|
||||
```
|
||||
|
||||
## Network
|
||||
|
||||
If the etcd leader serves a large number of concurrent client requests, it may delay processing follower peer requests due to network congestion. This manifests as send buffer error messages on the follower nodes:
|
||||
|
@ -6,27 +6,27 @@ In the general case, upgrading from etcd 2.3 to 3.0 can be a zero-downtime, roll
|
||||
|
||||
Before [starting an upgrade](#upgrade-procedure), read through the rest of this guide to prepare.
|
||||
|
||||
### Upgrade Checklists
|
||||
### Upgrade checklists
|
||||
|
||||
#### Upgrade Requirements
|
||||
#### Upgrade requirements
|
||||
|
||||
To upgrade an existing etcd deployment to 3.0, the running cluster must be 2.3 or greater. If it's before 2.3, please upgrade to [2.3](https://github.com/coreos/etcd/releases/tag/v2.3.0) before upgrading to 3.0.
|
||||
|
||||
Also, to ensure a smooth rolling upgrade, the running cluster must be healthy. You can check the health of the cluster by using the `etcdctl cluster-health` command.
|
||||
Also, to ensure a smooth rolling upgrade, the running cluster must be healthy. Check the health of the cluster by using the `etcdctl cluster-health` command before proceeding.
|
||||
|
||||
#### Preparation
|
||||
|
||||
Before upgrading etcd, always test the services relying on etcd in a staging environment before deploying the upgrade to the production environment.
|
||||
|
||||
Before beginning, [backup the etcd data directory](../v2/admin_guide.md#backing-up-the-datastore). Should something go wrong with the upgrade, it is possible to use this backup to [downgrade](#downgrade) back to existing etcd version.
|
||||
Before beginning, [backup the etcd data directory](../v2/admin_guide.md#backing-up-the-datastore). Should something go wrong with the upgrade, it is possible to use this backup to [downgrade](#downgrade) back to existing etcd version.
|
||||
|
||||
#### Mixed Versions
|
||||
#### Mixed versions
|
||||
|
||||
While upgrading, an etcd cluster supports mixed versions of etcd members, and operates with the protocol of the lowest common version. The cluster is only considered upgraded once all of its members are upgraded to version 3.0. Internally, etcd members negotiate with each other to determine the overall cluster version, which controls the reported version and the supported features.
|
||||
|
||||
#### Limitations
|
||||
|
||||
It might take up to 2 minutes for the newly upgraded member to catch up with the existing cluster when the total data size is larger than 50MB. Check the size of a recent snapshot to estimate the total data size. In other words, it is safest to wait for 2 minutes between upgrading each member.
|
||||
It might take up to 2 minutes for the newly upgraded member to catch up with the existing cluster when the total data size is larger than 50MB. Check the size of a recent snapshot to estimate the total data size. In other words, it is safest to wait for 2 minutes between upgrading each member.
|
||||
|
||||
For a much larger total data size, 100MB or more , this one-time process might take even more time. Administrators of very large etcd clusters of this magnitude can feel free to contact the [etcd team][etcd-contact] before upgrading, and we’ll be happy to provide advice on the procedure.
|
||||
|
||||
@ -36,13 +36,13 @@ If all members have been upgraded to v3.0, the cluster will be upgraded to v3.0,
|
||||
|
||||
Please [backup the data directory](../v2/admin_guide.md#backing-up-the-datastore) of all etcd members to make downgrading the cluster possible even after it has been completely upgraded.
|
||||
|
||||
### Upgrade Procedure
|
||||
### Upgrade procedure
|
||||
|
||||
This example details the upgrade of a three-member v2.3 ectd cluster running on a local machine.
|
||||
This example details the upgrade of a three-member v2.3 ectd cluster running on a local machine.
|
||||
|
||||
#### 1. Check upgrade requirements.
|
||||
|
||||
Is the the cluster healthy and running v.2.3.x?
|
||||
Is the cluster healthy and running v.2.3.x?
|
||||
|
||||
```
|
||||
$ etcdctl cluster-health
|
||||
@ -64,7 +64,7 @@ When each etcd process is stopped, expected errors will be logged by other clust
|
||||
2016-06-27 15:21:48.624175 I | rafthttp: the connection with 8211f1d0f64f3269 became inactive
|
||||
```
|
||||
|
||||
It’s a good idea at this point to [backup the etcd data directory](../v2/admin_guide.md#backing-up-the-datastore) to provide a downgrade path should any problems occur:
|
||||
It’s a good idea at this point to [backup the etcd data directory](../v2/admin_guide.md#backing-up-the-datastore) to provide a downgrade path should any problems occur:
|
||||
|
||||
```
|
||||
$ etcdctl backup \
|
||||
@ -102,7 +102,7 @@ Upgraded members will log warnings like the following until the entire cluster i
|
||||
|
||||
#### 5. Finish
|
||||
|
||||
When all members are upgraded, the cluster will report upgrading to 3.0 successfully:
|
||||
When all members are upgraded, the cluster will report upgrading to 3.0 successfully:
|
||||
|
||||
```
|
||||
2016-06-27 15:22:19.873751 N | membership: updated the cluster version from 2.3 to 3.0
|
||||
@ -116,4 +116,14 @@ $ ETCDCTL_API=3 etcdctl endpoint health
|
||||
127.0.0.1:22379 is healthy: successfully committed proposal: took = 18.513301ms
|
||||
```
|
||||
|
||||
## Further considerations
|
||||
|
||||
- etcdctl environment variables have been updated. If `ETCDCTL_API=2 etcdctl cluster-health` works properly but `ETCDCTL_API=3 etcdctl endpoints health` responds with `Error: grpc: timed out when dialing`, be sure to use the [new variable names](https://github.com/coreos/etcd/tree/master/etcdctl#etcdctl).
|
||||
|
||||
## Known Issues
|
||||
|
||||
- etcd < v3.1 does not work properly if built with Go > v1.7. See [Issue 6951](https://github.com/coreos/etcd/issues/6951) for additional information.
|
||||
- If an error such as `transport: http2Client.notifyError got notified that the client transport was broken unexpected EOF.` shows up in the etcd server logs, be sure etcd is a pre-built release or built with (etcd v3.1+ & go v1.7+) or (etcd <v3.1 & go v1.6.x).
|
||||
- Adding a v3 node to v2.3 cluster during upgrades is not supported and could trigger panics. See [Issue 7249](https://github.com/coreos/etcd/issues/7429) for additional information. Mixed versions of etcd members are only allowed during v3 migration. Finish upgrades before making any membership changes.
|
||||
|
||||
[etcd-contact]: https://groups.google.com/forum/#!forum/etcd-dev
|
||||
|
123
Documentation/upgrades/upgrade_3_1.md
Normal file
123
Documentation/upgrades/upgrade_3_1.md
Normal file
@ -0,0 +1,123 @@
|
||||
## Upgrade etcd from 3.0 to 3.1
|
||||
|
||||
In the general case, upgrading from etcd 3.0 to 3.1 can be a zero-downtime, rolling upgrade:
|
||||
- one by one, stop the etcd v3.0 processes and replace them with etcd v3.1 processes
|
||||
- after running all v3.1 processes, new features in v3.1 are available to the cluster
|
||||
|
||||
Before [starting an upgrade](#upgrade-procedure), read through the rest of this guide to prepare.
|
||||
|
||||
### Upgrade checklists
|
||||
|
||||
#### Upgrade requirements
|
||||
|
||||
To upgrade an existing etcd deployment to 3.1, the running cluster must be 3.0 or greater. If it's before 3.0, please upgrade to [3.0](https://github.com/coreos/etcd/releases/tag/v3.0.16) before upgrading to 3.1.
|
||||
|
||||
Also, to ensure a smooth rolling upgrade, the running cluster must be healthy. Check the health of the cluster by using the `etcdctl endpoint health` command before proceeding.
|
||||
|
||||
#### Preparation
|
||||
|
||||
Before upgrading etcd, always test the services relying on etcd in a staging environment before deploying the upgrade to the production environment.
|
||||
|
||||
Before beginning, [backup the etcd data](../op-guide/maintenance.md#snapshot-backup). Should something go wrong with the upgrade, it is possible to use this backup to [downgrade](#downgrade) back to existing etcd version. Please note that the `snapshot` command only backs up the v3 data. For v2 data, see [backing up v2 datastore](../v2/admin_guide.md#backing-up-the-datastore).
|
||||
|
||||
#### Mixed versions
|
||||
|
||||
While upgrading, an etcd cluster supports mixed versions of etcd members, and operates with the protocol of the lowest common version. The cluster is only considered upgraded once all of its members are upgraded to version 3.1. Internally, etcd members negotiate with each other to determine the overall cluster version, which controls the reported version and the supported features.
|
||||
|
||||
#### Limitations
|
||||
|
||||
Note: If the cluster only has v3 data and no v2 data, it is not subject to this limitation.
|
||||
|
||||
If the cluster is serving a v2 data set larger than 50MB, each newly upgraded member may take up to two minutes to catch up with the existing cluster. Check the size of a recent snapshot to estimate the total data size. In other words, it is safest to wait for 2 minutes between upgrading each member.
|
||||
|
||||
For a much larger total data size, 100MB or more , this one-time process might take even more time. Administrators of very large etcd clusters of this magnitude can feel free to contact the [etcd team][etcd-contact] before upgrading, and we'll be happy to provide advice on the procedure.
|
||||
|
||||
#### Downgrade
|
||||
|
||||
If all members have been upgraded to v3.1, the cluster will be upgraded to v3.1, and downgrade from this completed state is **not possible**. If any single member is still v3.0, however, the cluster and its operations remains "v3.0", and it is possible from this mixed cluster state to return to using a v3.0 etcd binary on all members.
|
||||
|
||||
Please [backup the data directory](../op-guide/maintenance.md#snapshot-backup) of all etcd members to make downgrading the cluster possible even after it has been completely upgraded.
|
||||
|
||||
### Upgrade procedure
|
||||
|
||||
This example shows how to upgrade a 3-member v3.0 ectd cluster running on a local machine.
|
||||
|
||||
#### 1. Check upgrade requirements
|
||||
|
||||
Is the cluster healthy and running v3.0.x?
|
||||
|
||||
```
|
||||
$ ETCDCTL_API=3 etcdctl endpoint health --endpoints=localhost:2379,localhost:22379,localhost:32379
|
||||
localhost:2379 is healthy: successfully committed proposal: took = 6.600684ms
|
||||
localhost:22379 is healthy: successfully committed proposal: took = 8.540064ms
|
||||
localhost:32379 is healthy: successfully committed proposal: took = 8.763432ms
|
||||
|
||||
$ curl http://localhost:2379/version
|
||||
{"etcdserver":"3.0.16","etcdcluster":"3.0.0"}
|
||||
```
|
||||
|
||||
#### 2. Stop the existing etcd process
|
||||
|
||||
When each etcd process is stopped, expected errors will be logged by other cluster members. This is normal since a cluster member connection has been (temporarily) broken:
|
||||
|
||||
```
|
||||
2017-01-17 09:34:18.352662 I | raft: raft.node: 1640829d9eea5cfb elected leader 1640829d9eea5cfb at term 5
|
||||
2017-01-17 09:34:18.359630 W | etcdserver: failed to reach the peerURL(http://localhost:2380) of member fd32987dcd0511e0 (Get http://localhost:2380/version: dial tcp 127.0.0.1:2380: getsockopt: connection refused)
|
||||
2017-01-17 09:34:18.359679 W | etcdserver: cannot get the version of member fd32987dcd0511e0 (Get http://localhost:2380/version: dial tcp 127.0.0.1:2380: getsockopt: connection refused)
|
||||
2017-01-17 09:34:18.548116 W | rafthttp: lost the TCP streaming connection with peer fd32987dcd0511e0 (stream Message writer)
|
||||
2017-01-17 09:34:19.147816 W | rafthttp: lost the TCP streaming connection with peer fd32987dcd0511e0 (stream MsgApp v2 writer)
|
||||
2017-01-17 09:34:34.364907 W | etcdserver: failed to reach the peerURL(http://localhost:2380) of member fd32987dcd0511e0 (Get http://localhost:2380/version: dial tcp 127.0.0.1:2380: getsockopt: connection refused)
|
||||
```
|
||||
|
||||
It's a good idea at this point to [backup the etcd data](../op-guide/maintenance.md#snapshot-backup) to provide a downgrade path should any problems occur:
|
||||
|
||||
```
|
||||
$ etcdctl snapshot save backup.db
|
||||
```
|
||||
|
||||
#### 3. Drop-in etcd v3.1 binary and start the new etcd process
|
||||
|
||||
The new v3.1 etcd will publish its information to the cluster:
|
||||
|
||||
```
|
||||
2017-01-17 09:36:00.996590 I | etcdserver: published {Name:my-etcd-1 ClientURLs:[http://localhost:2379]} to cluster 46bc3ce73049e678
|
||||
```
|
||||
|
||||
Verify that each member, and then the entire cluster, becomes healthy with the new v3.1 etcd binary:
|
||||
|
||||
```
|
||||
$ ETCDCTL_API=3 /etcdctl endpoint health --endpoints=localhost:2379,localhost:22379,localhost:32379
|
||||
localhost:22379 is healthy: successfully committed proposal: took = 5.540129ms
|
||||
localhost:32379 is healthy: successfully committed proposal: took = 7.321671ms
|
||||
localhost:2379 is healthy: successfully committed proposal: took = 10.629901ms
|
||||
```
|
||||
|
||||
Upgraded members will log warnings like the following until the entire cluster is upgraded. This is expected and will cease after all etcd cluster members are upgraded to v3.1:
|
||||
|
||||
```
|
||||
2017-01-17 09:36:38.406268 W | etcdserver: the local etcd version 3.0.16 is not up-to-date
|
||||
2017-01-17 09:36:38.406295 W | etcdserver: member fd32987dcd0511e0 has a higher version 3.1.0
|
||||
2017-01-17 09:36:42.407695 W | etcdserver: the local etcd version 3.0.16 is not up-to-date
|
||||
2017-01-17 09:36:42.407730 W | etcdserver: member fd32987dcd0511e0 has a higher version 3.1.0
|
||||
```
|
||||
|
||||
#### 4. Repeat step 2 to step 3 for all other members
|
||||
|
||||
#### 5. Finish
|
||||
|
||||
When all members are upgraded, the cluster will report upgrading to 3.1 successfully:
|
||||
|
||||
```
|
||||
2017-01-17 09:37:03.100015 I | etcdserver: updating the cluster version from 3.0 to 3.1
|
||||
2017-01-17 09:37:03.104263 N | etcdserver/membership: updated the cluster version from 3.0 to 3.1
|
||||
2017-01-17 09:37:03.104374 I | etcdserver/api: enabled capabilities for version 3.1
|
||||
```
|
||||
|
||||
```
|
||||
$ ETCDCTL_API=3 /etcdctl endpoint health --endpoints=localhost:2379,localhost:22379,localhost:32379
|
||||
localhost:2379 is healthy: successfully committed proposal: took = 2.312897ms
|
||||
localhost:22379 is healthy: successfully committed proposal: took = 2.553476ms
|
||||
localhost:32379 is healthy: successfully committed proposal: took = 2.516902ms
|
||||
```
|
||||
|
||||
[etcd-contact]: https://groups.google.com/forum/#!forum/etcd-dev
|
172
Documentation/upgrades/upgrade_3_2.md
Normal file
172
Documentation/upgrades/upgrade_3_2.md
Normal file
@ -0,0 +1,172 @@
|
||||
## Upgrade etcd from 3.1 to 3.2
|
||||
|
||||
In the general case, upgrading from etcd 3.1 to 3.2 can be a zero-downtime, rolling upgrade:
|
||||
- one by one, stop the etcd v3.1 processes and replace them with etcd v3.2 processes
|
||||
- after running all v3.2 processes, new features in v3.2 are available to the cluster
|
||||
|
||||
Before [starting an upgrade](#upgrade-procedure), read through the rest of this guide to prepare.
|
||||
|
||||
### Client upgrade checklists
|
||||
|
||||
3.2 introduces two breaking changes.
|
||||
|
||||
Previously, `clientv3.Lease.TimeToLive` API returned `lease.ErrLeaseNotFound` on non-existent lease ID. 3.2 instead returns TTL=-1 in its response and no error (see [#7305](https://github.com/coreos/etcd/pull/7305)).
|
||||
|
||||
Before
|
||||
|
||||
```go
|
||||
// when leaseID does not exist
|
||||
resp, err := TimeToLive(ctx, leaseID)
|
||||
resp == nil
|
||||
err == lease.ErrLeaseNotFound
|
||||
```
|
||||
|
||||
After
|
||||
|
||||
```go
|
||||
// when leaseID does not exist
|
||||
resp, err := TimeToLive(ctx, leaseID)
|
||||
resp.TTL == -1
|
||||
err == nil
|
||||
```
|
||||
|
||||
`clientv3.NewFromConfigFile` is moved to `yaml.NewConfig`.
|
||||
|
||||
Before
|
||||
|
||||
```go
|
||||
import "github.com/coreos/etcd/clientv3"
|
||||
clientv3.NewFromConfigFile
|
||||
```
|
||||
|
||||
After
|
||||
|
||||
```go
|
||||
import clientv3yaml "github.com/coreos/etcd/clientv3/yaml"
|
||||
clientv3yaml.NewConfig
|
||||
```
|
||||
|
||||
### Server upgrade checklists
|
||||
|
||||
#### Upgrade requirements
|
||||
|
||||
To upgrade an existing etcd deployment to 3.2, the running cluster must be 3.1 or greater. If it's before 3.1, please upgrade to [3.1](https://github.com/coreos/etcd/releases/tag/v3.1.7) before upgrading to 3.2.
|
||||
|
||||
Also, to ensure a smooth rolling upgrade, the running cluster must be healthy. Check the health of the cluster by using the `etcdctl endpoint health` command before proceeding.
|
||||
|
||||
#### Preparation
|
||||
|
||||
Before upgrading etcd, always test the services relying on etcd in a staging environment before deploying the upgrade to the production environment.
|
||||
|
||||
Before beginning, [backup the etcd data](../op-guide/maintenance.md#snapshot-backup). Should something go wrong with the upgrade, it is possible to use this backup to [downgrade](#downgrade) back to existing etcd version. Please note that the `snapshot` command only backs up the v3 data. For v2 data, see [backing up v2 datastore](../v2/admin_guide.md#backing-up-the-datastore).
|
||||
|
||||
#### Mixed versions
|
||||
|
||||
While upgrading, an etcd cluster supports mixed versions of etcd members, and operates with the protocol of the lowest common version. The cluster is only considered upgraded once all of its members are upgraded to version 3.2. Internally, etcd members negotiate with each other to determine the overall cluster version, which controls the reported version and the supported features.
|
||||
|
||||
#### Limitations
|
||||
|
||||
Note: If the cluster only has v3 data and no v2 data, it is not subject to this limitation.
|
||||
|
||||
If the cluster is serving a v2 data set larger than 50MB, each newly upgraded member may take up to two minutes to catch up with the existing cluster. Check the size of a recent snapshot to estimate the total data size. In other words, it is safest to wait for 2 minutes between upgrading each member.
|
||||
|
||||
For a much larger total data size, 100MB or more , this one-time process might take even more time. Administrators of very large etcd clusters of this magnitude can feel free to contact the [etcd team][etcd-contact] before upgrading, and we'll be happy to provide advice on the procedure.
|
||||
|
||||
#### Downgrade
|
||||
|
||||
If all members have been upgraded to v3.2, the cluster will be upgraded to v3.2, and downgrade from this completed state is **not possible**. If any single member is still v3.1, however, the cluster and its operations remains "v3.1", and it is possible from this mixed cluster state to return to using a v3.1 etcd binary on all members.
|
||||
|
||||
Please [backup the data directory](../op-guide/maintenance.md#snapshot-backup) of all etcd members to make downgrading the cluster possible even after it has been completely upgraded.
|
||||
|
||||
### Upgrade procedure
|
||||
|
||||
This example shows how to upgrade a 3-member v3.1 ectd cluster running on a local machine.
|
||||
|
||||
#### 1. Check upgrade requirements
|
||||
|
||||
Is the cluster healthy and running v3.1.x?
|
||||
|
||||
```
|
||||
$ ETCDCTL_API=3 etcdctl endpoint health --endpoints=localhost:2379,localhost:22379,localhost:32379
|
||||
localhost:2379 is healthy: successfully committed proposal: took = 6.600684ms
|
||||
localhost:22379 is healthy: successfully committed proposal: took = 8.540064ms
|
||||
localhost:32379 is healthy: successfully committed proposal: took = 8.763432ms
|
||||
|
||||
$ curl http://localhost:2379/version
|
||||
{"etcdserver":"3.1.7","etcdcluster":"3.1.0"}
|
||||
```
|
||||
|
||||
#### 2. Stop the existing etcd process
|
||||
|
||||
When each etcd process is stopped, expected errors will be logged by other cluster members. This is normal since a cluster member connection has been (temporarily) broken:
|
||||
|
||||
```
|
||||
2017-04-27 14:13:31.491746 I | raft: c89feb932daef420 [term 3] received MsgTimeoutNow from 6d4f535bae3ab960 and starts an election to get leadership.
|
||||
2017-04-27 14:13:31.491769 I | raft: c89feb932daef420 became candidate at term 4
|
||||
2017-04-27 14:13:31.491788 I | raft: c89feb932daef420 received MsgVoteResp from c89feb932daef420 at term 4
|
||||
2017-04-27 14:13:31.491797 I | raft: c89feb932daef420 [logterm: 3, index: 9] sent MsgVote request to 6d4f535bae3ab960 at term 4
|
||||
2017-04-27 14:13:31.491805 I | raft: c89feb932daef420 [logterm: 3, index: 9] sent MsgVote request to 9eda174c7df8a033 at term 4
|
||||
2017-04-27 14:13:31.491815 I | raft: raft.node: c89feb932daef420 lost leader 6d4f535bae3ab960 at term 4
|
||||
2017-04-27 14:13:31.524084 I | raft: c89feb932daef420 received MsgVoteResp from 6d4f535bae3ab960 at term 4
|
||||
2017-04-27 14:13:31.524108 I | raft: c89feb932daef420 [quorum:2] has received 2 MsgVoteResp votes and 0 vote rejections
|
||||
2017-04-27 14:13:31.524123 I | raft: c89feb932daef420 became leader at term 4
|
||||
2017-04-27 14:13:31.524136 I | raft: raft.node: c89feb932daef420 elected leader c89feb932daef420 at term 4
|
||||
2017-04-27 14:13:31.592650 W | rafthttp: lost the TCP streaming connection with peer 6d4f535bae3ab960 (stream MsgApp v2 reader)
|
||||
2017-04-27 14:13:31.592825 W | rafthttp: lost the TCP streaming connection with peer 6d4f535bae3ab960 (stream Message reader)
|
||||
2017-04-27 14:13:31.693275 E | rafthttp: failed to dial 6d4f535bae3ab960 on stream Message (dial tcp [::1]:2380: getsockopt: connection refused)
|
||||
2017-04-27 14:13:31.693289 I | rafthttp: peer 6d4f535bae3ab960 became inactive
|
||||
2017-04-27 14:13:31.936678 W | rafthttp: lost the TCP streaming connection with peer 6d4f535bae3ab960 (stream Message writer)
|
||||
```
|
||||
|
||||
It's a good idea at this point to [backup the etcd data](../op-guide/maintenance.md#snapshot-backup) to provide a downgrade path should any problems occur:
|
||||
|
||||
```
|
||||
$ etcdctl snapshot save backup.db
|
||||
```
|
||||
|
||||
#### 3. Drop-in etcd v3.2 binary and start the new etcd process
|
||||
|
||||
The new v3.2 etcd will publish its information to the cluster:
|
||||
|
||||
```
|
||||
2017-04-27 14:14:25.363225 I | etcdserver: published {Name:s1 ClientURLs:[http://localhost:2379]} to cluster a9ededbffcb1b1f1
|
||||
```
|
||||
|
||||
Verify that each member, and then the entire cluster, becomes healthy with the new v3.2 etcd binary:
|
||||
|
||||
```
|
||||
$ ETCDCTL_API=3 /etcdctl endpoint health --endpoints=localhost:2379,localhost:22379,localhost:32379
|
||||
localhost:22379 is healthy: successfully committed proposal: took = 5.540129ms
|
||||
localhost:32379 is healthy: successfully committed proposal: took = 7.321771ms
|
||||
localhost:2379 is healthy: successfully committed proposal: took = 10.629901ms
|
||||
```
|
||||
|
||||
Upgraded members will log warnings like the following until the entire cluster is upgraded. This is expected and will cease after all etcd cluster members are upgraded to v3.2:
|
||||
|
||||
```
|
||||
2017-04-27 14:15:17.071804 W | etcdserver: member c89feb932daef420 has a higher version 3.2.0
|
||||
2017-04-27 14:15:21.073110 W | etcdserver: the local etcd version 3.1.7 is not up-to-date
|
||||
2017-04-27 14:15:21.073142 W | etcdserver: member 6d4f535bae3ab960 has a higher version 3.2.0
|
||||
2017-04-27 14:15:21.073157 W | etcdserver: the local etcd version 3.1.7 is not up-to-date
|
||||
2017-04-27 14:15:21.073164 W | etcdserver: member c89feb932daef420 has a higher version 3.2.0
|
||||
```
|
||||
|
||||
#### 4. Repeat step 2 to step 3 for all other members
|
||||
|
||||
#### 5. Finish
|
||||
|
||||
When all members are upgraded, the cluster will report upgrading to 3.2 successfully:
|
||||
|
||||
```
|
||||
2017-04-27 14:15:54.536901 N | etcdserver/membership: updated the cluster version from 3.1 to 3.2
|
||||
2017-04-27 14:15:54.537035 I | etcdserver/api: enabled capabilities for version 3.2
|
||||
```
|
||||
|
||||
```
|
||||
$ ETCDCTL_API=3 /etcdctl endpoint health --endpoints=localhost:2379,localhost:22379,localhost:32379
|
||||
localhost:2379 is healthy: successfully committed proposal: took = 2.312897ms
|
||||
localhost:22379 is healthy: successfully committed proposal: took = 2.553476ms
|
||||
localhost:32379 is healthy: successfully committed proposal: took = 2.517902ms
|
||||
```
|
||||
|
||||
[etcd-contact]: https://groups.google.com/forum/#!forum/etcd-dev
|
@ -67,13 +67,13 @@ You have successfully started an etcd and written a key to the store.
|
||||
|
||||
The [official etcd ports][iana-ports] are 2379 for client requests, and 2380 for peer communication. To maintain compatibility, some etcd configuration and documentation continues to refer to the legacy ports 4001 and 7001, but all new etcd use and discussion should adopt the IANA-assigned ports. The legacy ports 4001 and 7001 will be fully deprecated, and support for their use removed, in future etcd releases.
|
||||
|
||||
[iana-ports]: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=etcd
|
||||
[iana-ports]: http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.txt
|
||||
|
||||
### Running local etcd cluster
|
||||
|
||||
First install [goreman](https://github.com/mattn/goreman), which manages Procfile-based applications.
|
||||
|
||||
Our [Procfile script](./Procfile) will set up a local example cluster. You can start it with:
|
||||
Our [Procfile script](../../V2Procfile) will set up a local example cluster. You can start it with:
|
||||
|
||||
```sh
|
||||
goreman start
|
||||
@ -162,4 +162,4 @@ Currently only the amd64 architecture is officially supported by `etcd`.
|
||||
|
||||
### License
|
||||
|
||||
etcd is under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
|
||||
etcd is under the Apache 2.0 license. See the [LICENSE](../../LICENSE) file for details.
|
||||
|
@ -18,7 +18,7 @@ A key’s lifetime spans a generation. Each key may have one or multiple generat
|
||||
|
||||
### Physical View
|
||||
|
||||
etcd stores the physical data as key-value pairs in a persistent [b+tree][b+tree]. Each revision of the store’s state only contains the delta from its previous revision to be efficient. A single revision may correspond to multiple keys in the tree.
|
||||
etcd stores the physical data as key-value pairs in a persistent [b+tree][b+tree]. Each revision of the store’s state only contains the delta from its previous revision to be efficient. A single revision may correspond to multiple keys in the tree.
|
||||
|
||||
The key of key-value pair is a 3-tuple (major, sub, type). Major is the store revision holding the key. Sub differentiates among keys within the same revision. Type is an optional suffix for special value (e.g., `t` if the value contains a tombstone). The value of the key-value pair contains the modification from previous revision, thus one delta from previous revision. The b+tree is ordered by key in lexical byte-order. Ranged lookups over revision deltas are fast; this enables quickly finding modifications from one specific revision to another. Compaction removes out-of-date keys-value pairs.
|
||||
|
||||
@ -47,7 +47,7 @@ An etcd operation is considered complete when it is committed through consensus,
|
||||
|
||||
#### revision
|
||||
|
||||
An etcd operation that modifies the key value store is assigned with a single increasing revision. A transaction operation might modifies the key value store multiple times, but only one revision is assigned. The revision attribute of a key value pair that modified by the operation has the same value as the revision of the operation. The revision can be used as a logical clock for key value store. A key value pair that has a larger revision is modified after a key value pair with a smaller revision. Two key value pairs that have the same revision are modified by an operation "concurrently".
|
||||
An etcd operation that modifies the key value store is assigned with a single increasing revision. A transaction operation might modify the key value store multiple times, but only one revision is assigned. The revision attribute of a key value pair that modified by the operation has the same value as the revision of the operation. The revision can be used as a logical clock for key value store. A key value pair that has a larger revision is modified after a key value pair with a smaller revision. Two key value pairs that have the same revision are modified by an operation "concurrently".
|
||||
|
||||
### Guarantees Provided
|
||||
|
||||
@ -73,7 +73,7 @@ Any completed operations are durable. All accessible data is also durable data.
|
||||
|
||||
#### Linearizability
|
||||
|
||||
Linearizability (also known as Atomic Consistency or External Consistency) is a consistency level between strict consistency and sequential consistency.
|
||||
Linearizability (also known as Atomic Consistency or External Consistency) is a consistency level between strict consistency and sequential consistency.
|
||||
|
||||
For linearizability, suppose each operation receives a timestamp from a loosely synchronized global clock. Operations are linearized if and only if they always complete as though they were executed in a sequential order and each operation appears to complete in the order specified by the program. Likewise, if an operation’s timestamp precedes another, that operation must also precede the other operation in the sequence.
|
||||
|
||||
@ -83,10 +83,10 @@ etcd does not ensure linearizability for watch operations. Users are expected to
|
||||
|
||||
etcd ensures linearizability for all other operations by default. Linearizability comes with a cost, however, because linearized requests must go through the Raft consensus process. To obtain lower latencies and higher throughput for read requests, clients can configure a request’s consistency mode to `serializable`, which may access stale data with respect to quorum, but removes the performance penalty of linearized accesses' reliance on live consensus.
|
||||
|
||||
[persistent-ds]: [https://en.wikipedia.org/wiki/Persistent_data_structure]
|
||||
[btree]: [https://en.wikipedia.org/wiki/B-tree]
|
||||
[b+tree]: [https://en.wikipedia.org/wiki/B%2B_tree]
|
||||
[seq_consistency]: [https://en.wikipedia.org/wiki/Consistency_model#Sequential_consistency]
|
||||
[strict_consistency]: [https://en.wikipedia.org/wiki/Consistency_model#Strict_consistency]
|
||||
[serializable_isolation]: [https://en.wikipedia.org/wiki/Isolation_(database_systems)#Serializable]
|
||||
[Linearizability]: [#Linearizability]
|
||||
[persistent-ds]: https://en.wikipedia.org/wiki/Persistent_data_structure
|
||||
[btree]: https://en.wikipedia.org/wiki/B-tree
|
||||
[b+tree]: https://en.wikipedia.org/wiki/B%2B_tree
|
||||
[seq_consistency]: https://en.wikipedia.org/wiki/Consistency_model#Sequential_consistency
|
||||
[strict_consistency]: https://en.wikipedia.org/wiki/Consistency_model#Strict_consistency
|
||||
[serializable_isolation]: https://en.wikipedia.org/wiki/Isolation_(database_systems)#Serializable
|
||||
[Linearizability]: #linearizability
|
||||
|
@ -32,7 +32,7 @@ The consistent flag for read operations is removed in etcd 2.0.0. The normal rea
|
||||
|
||||
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 an etcd server successfully, it should be able to get the value out of the server immediately.
|
||||
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 an 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.
|
||||
|
||||
@ -56,6 +56,7 @@ Proxy mode in 2.0 will provide similar functionality, and with improved control
|
||||
## Discovery Service
|
||||
|
||||
A size key needs to be provided inside a [discovery token][discoverytoken].
|
||||
|
||||
[discoverytoken]: clustering.md#custom-etcd-discovery-service
|
||||
|
||||
## HTTP Admin API
|
||||
|
@ -49,4 +49,4 @@ Bootstrap another machine and use the [boom HTTP benchmark tool][boom] to send r
|
||||
| 256 | 256 | all servers | 3061 | 119.3 |
|
||||
|
||||
[boom]: https://github.com/rakyll/boom
|
||||
[hack-benchmark]: /hack/benchmark/
|
||||
[hack-benchmark]: ../../../hack/benchmark/
|
||||
|
@ -24,7 +24,7 @@ Go OS/Arch: linux/amd64
|
||||
|
||||
## Testing
|
||||
|
||||
Bootstrap another machine, outside of the etcd cluster, and run the [`boom` HTTP benchmark tool](https://github.com/rakyll/boom) with a connection reuse patch to send requests to each etcd cluster member. See the [benchmark instructions](../../hack/benchmark/) for the patch and the steps to reproduce our procedures.
|
||||
Bootstrap another machine, outside of the etcd cluster, and run the [`boom` HTTP benchmark tool][boom] with a connection reuse patch to send requests to each etcd cluster member. See the [benchmark instructions][hack] for the patch and the steps to reproduce our procedures.
|
||||
|
||||
The performance is calulated through results of 100 benchmark rounds.
|
||||
|
||||
@ -66,4 +66,7 @@ The performance is calulated through results of 100 benchmark rounds.
|
||||
|
||||
- Write QPS to cluster leaders seems to be increased by a small margin. This is because the main loop and entry apply loops were decoupled in the etcd raft logic, eliminating several blocks between them.
|
||||
|
||||
- Write QPS to all members seems to be increased by a significant margin, because followers now receive the latest commit index sooner, and commit proposals more quickly.
|
||||
- Write QPS to all members seems to be increased by a significant margin, because followers now receive the latest commit index sooner, and commit proposals more quickly.
|
||||
|
||||
[boom]: https://github.com/rakyll/boom
|
||||
[hack]: ../../../hack/benchmark/
|
||||
|
@ -69,4 +69,4 @@ Bootstrap another machine and use the [boom HTTP benchmark tool][boom] to send r
|
||||
[boom]: https://github.com/rakyll/boom
|
||||
[c7146bd5]: https://github.com/coreos/etcd/commits/c7146bd5f2c73716091262edc638401bb8229144
|
||||
[etcd-2.1-benchmark]: etcd-2-1-0-alpha-benchmarks.md
|
||||
[hack-benchmark]: /hack/benchmark/
|
||||
[hack-benchmark]: ../../../hack/benchmark/
|
||||
|
@ -39,4 +39,4 @@ The performance is nearly the same as the one with empty server handler.
|
||||
The performance with empty server handler is not affected by one put. So the
|
||||
performance downgrade should be caused by storage package.
|
||||
|
||||
[etcd-v3-benchmark]: /tools/benchmark/
|
||||
[etcd-v3-benchmark]: ../../../tools/benchmark/
|
||||
|
@ -423,7 +423,7 @@ To make understanding this feature easier, we changed the naming of some flags,
|
||||
|-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.|
|
||||
|
||||
[client]: /client
|
||||
[client]: ../../client
|
||||
[client-discoverer]: https://godoc.org/github.com/coreos/etcd/client#Discoverer
|
||||
[conf-adv-client]: configuration.md#-advertise-client-urls
|
||||
[conf-listen-client]: configuration.md#-listen-client-urls
|
||||
|
@ -176,7 +176,10 @@ To start etcd automatically using custom settings at startup in Linux, using a [
|
||||
|
||||
The security flags help to [build a secure etcd cluster][security].
|
||||
|
||||
### --ca-file [DEPRECATED]
|
||||
### --ca-file
|
||||
|
||||
**DEPRECATED**
|
||||
|
||||
+ Path to the client server TLS CA file. `--ca-file ca.crt` could be replaced by `--trusted-ca-file ca.crt --client-cert-auth` and etcd will perform the same.
|
||||
+ default: none
|
||||
+ env variable: ETCD_CA_FILE
|
||||
@ -201,7 +204,10 @@ The security flags help to [build a secure etcd cluster][security].
|
||||
+ default: none
|
||||
+ env variable: ETCD_TRUSTED_CA_FILE
|
||||
|
||||
### --peer-ca-file [DEPRECATED]
|
||||
### --peer-ca-file
|
||||
|
||||
**DEPRECATED**
|
||||
|
||||
+ Path to the peer server TLS CA file. `--peer-ca-file ca.crt` could be replaced by `--peer-trusted-ca-file ca.crt --peer-client-cert-auth` and etcd will perform the same.
|
||||
+ default: none
|
||||
+ env variable: ETCD_PEER_CA_FILE
|
||||
@ -234,7 +240,7 @@ The security flags help to [build a secure etcd cluster][security].
|
||||
+ env variable: ETCD_DEBUG
|
||||
|
||||
### --log-package-levels
|
||||
+ Set individual etcd subpackages to specific log levels. An example being `etcdserver=WARNING,security=DEBUG`
|
||||
+ Set individual etcd subpackages to specific log levels. An example being `etcdserver=WARNING,security=DEBUG`
|
||||
+ default: none (INFO for all packages)
|
||||
+ env variable: ETCD_LOG_PACKAGE_LEVELS
|
||||
|
||||
@ -272,7 +278,7 @@ Follow the instructions when using these flags.
|
||||
[build-cluster]: clustering.md#static
|
||||
[reconfig]: runtime-configuration.md
|
||||
[discovery]: clustering.md#discovery
|
||||
[iana-ports]: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=etcd
|
||||
[iana-ports]: http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.txt
|
||||
[proxy]: proxy.md
|
||||
[reconfig]: runtime-configuration.md
|
||||
[restore]: admin_guide.md#restoring-a-backup
|
||||
|
@ -70,7 +70,7 @@ cd release
|
||||
# personal GPG is okay for now
|
||||
for i in etcd-*{.zip,.tar.gz}; do gpg --sign ${i}; done
|
||||
# use `CoreOS ACI Builder <release@coreos.com>` secret key
|
||||
gpg -u 88182190 -a --output etcd-${VERSION}-linux-amd64.aci.asc --detach-sig etcd-${VERSION}-linux-amd64.aci
|
||||
for aci in etcd-${VERSION}.*.aci; do gpg -u 88182190 -a --output ${aci}.asc --detach-sig ${aci}; done
|
||||
```
|
||||
|
||||
## Publish Release Page in GitHub
|
||||
@ -88,6 +88,7 @@ gpg -u 88182190 -a --output etcd-${VERSION}-linux-amd64.aci.asc --detach-sig etc
|
||||
```
|
||||
docker login quay.io
|
||||
docker push quay.io/coreos/etcd:${VERSION}
|
||||
docker push quay.io/coreos/etcd:${VERSION}-${arch}
|
||||
```
|
||||
|
||||
- Add `latest` tag to the new image on [quay.io](https://quay.io/repository/coreos/etcd?tag=latest&tab=tags) if this is a stable release.
|
||||
|
@ -16,7 +16,7 @@ This will run the latest release version of etcd. You can specify version if nee
|
||||
|
||||
```
|
||||
docker run -d -v /usr/share/ca-certificates/:/etc/ssl/certs -p 4001:4001 -p 2380:2380 -p 2379:2379 \
|
||||
--name etcd quay.io/coreos/etcd \
|
||||
--name etcd quay.io/coreos/etcd:v2.3.8 \
|
||||
-name etcd0 \
|
||||
-advertise-client-urls http://${HostIP}:2379,http://${HostIP}:4001 \
|
||||
-listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:4001 \
|
||||
@ -42,11 +42,13 @@ etcdctl -C http://192.168.12.50:4001 member list
|
||||
Using Docker to setup a multi-node cluster is very similar to the standalone mode configuration.
|
||||
The main difference being the value used for the `-initial-cluster` flag, which must contain the peer urls for each etcd member in the cluster.
|
||||
|
||||
**Although the following commands look very similar, note that `-name`, `-advertise-client-urls` and `-initial-advertise-peer-urls` differ for each cluster member**
|
||||
|
||||
### etcd0
|
||||
|
||||
```
|
||||
docker run -d -v /usr/share/ca-certificates/:/etc/ssl/certs -p 4001:4001 -p 2380:2380 -p 2379:2379 \
|
||||
--name etcd quay.io/coreos/etcd \
|
||||
--name etcd quay.io/coreos/etcd:v2.3.8 \
|
||||
-name etcd0 \
|
||||
-advertise-client-urls http://192.168.12.50:2379,http://192.168.12.50:4001 \
|
||||
-listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:4001 \
|
||||
@ -61,7 +63,7 @@ docker run -d -v /usr/share/ca-certificates/:/etc/ssl/certs -p 4001:4001 -p 2380
|
||||
|
||||
```
|
||||
docker run -d -v /usr/share/ca-certificates/:/etc/ssl/certs -p 4001:4001 -p 2380:2380 -p 2379:2379 \
|
||||
--name etcd quay.io/coreos/etcd \
|
||||
--name etcd quay.io/coreos/etcd:v2.3.8 \
|
||||
-name etcd1 \
|
||||
-advertise-client-urls http://192.168.12.51:2379,http://192.168.12.51:4001 \
|
||||
-listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:4001 \
|
||||
@ -76,7 +78,7 @@ docker run -d -v /usr/share/ca-certificates/:/etc/ssl/certs -p 4001:4001 -p 2380
|
||||
|
||||
```
|
||||
docker run -d -v /usr/share/ca-certificates/:/etc/ssl/certs -p 4001:4001 -p 2380:2380 -p 2379:2379 \
|
||||
--name etcd quay.io/coreos/etcd \
|
||||
--name etcd quay.io/coreos/etcd:v2.3.8 \
|
||||
-name etcd2 \
|
||||
-advertise-client-urls http://192.168.12.52:2379,http://192.168.12.52:4001 \
|
||||
-listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:4001 \
|
||||
|
121
Documentation/v2/etcd_alert.rules
Normal file
121
Documentation/v2/etcd_alert.rules
Normal file
@ -0,0 +1,121 @@
|
||||
### General cluster availability ###
|
||||
|
||||
# alert if another failed member will result in an unavailable cluster
|
||||
ALERT InsufficientMembers
|
||||
IF count(up{job="etcd"} == 0) > (count(up{job="etcd"}) / 2 - 1)
|
||||
FOR 3m
|
||||
LABELS {
|
||||
severity = "critical"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "etcd cluster insufficient members",
|
||||
description = "If one more etcd member goes down the cluster will be unavailable",
|
||||
}
|
||||
|
||||
### HTTP requests alerts ###
|
||||
|
||||
# alert if more than 1% of requests to an HTTP endpoint have failed with a non 4xx response
|
||||
ALERT HighNumberOfFailedHTTPRequests
|
||||
IF sum by(method) (rate(etcd_http_failed_total{job="etcd", code!~"4[0-9]{2}"}[5m]))
|
||||
/ sum by(method) (rate(etcd_http_received_total{job="etcd"}[5m])) > 0.01
|
||||
FOR 10m
|
||||
LABELS {
|
||||
severity = "warning"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "a high number of HTTP requests are failing",
|
||||
description = "{{ $value }}% of requests for {{ $labels.method }} failed on etcd instance {{ $labels.instance }}",
|
||||
}
|
||||
|
||||
# alert if more than 5% of requests to an HTTP endpoint have failed with a non 4xx response
|
||||
ALERT HighNumberOfFailedHTTPRequests
|
||||
IF sum by(method) (rate(etcd_http_failed_total{job="etcd", code!~"4[0-9]{2}"}[5m]))
|
||||
/ sum by(method) (rate(etcd_http_received_total{job="etcd"}[5m])) > 0.05
|
||||
FOR 5m
|
||||
LABELS {
|
||||
severity = "critical"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "a high number of HTTP requests are failing",
|
||||
description = "{{ $value }}% of requests for {{ $labels.method }} failed on etcd instance {{ $labels.instance }}",
|
||||
}
|
||||
|
||||
# alert if 50% of requests get a 4xx response
|
||||
ALERT HighNumberOfFailedHTTPRequests
|
||||
IF sum by(method) (rate(etcd_http_failed_total{job="etcd", code=~"4[0-9]{2}"}[5m]))
|
||||
/ sum by(method) (rate(etcd_http_received_total{job="etcd"}[5m])) > 0.5
|
||||
FOR 10m
|
||||
LABELS {
|
||||
severity = "critical"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "a high number of HTTP requests are failing",
|
||||
description = "{{ $value }}% of requests for {{ $labels.method }} failed with 4xx responses on etcd instance {{ $labels.instance }}",
|
||||
}
|
||||
|
||||
# alert if the 99th percentile of HTTP requests take more than 150ms
|
||||
ALERT HTTPRequestsSlow
|
||||
IF histogram_quantile(0.99, rate(etcd_http_successful_duration_second_bucket[5m])) > 0.15
|
||||
FOR 10m
|
||||
LABELS {
|
||||
severity = "warning"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "slow HTTP requests",
|
||||
description = "on etcd instance {{ $labels.instance }} HTTP requests to {{ $label.method }} are slow",
|
||||
}
|
||||
|
||||
### File descriptor alerts ###
|
||||
|
||||
instance:fd_utilization = process_open_fds / process_max_fds
|
||||
|
||||
# alert if file descriptors are likely to exhaust within the next 4 hours
|
||||
ALERT FdExhaustionClose
|
||||
IF predict_linear(instance:fd_utilization[1h], 3600 * 4) > 1
|
||||
FOR 10m
|
||||
LABELS {
|
||||
severity = "warning"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "file descriptors soon exhausted",
|
||||
description = "{{ $labels.job }} instance {{ $labels.instance }} will exhaust its file descriptors soon",
|
||||
}
|
||||
|
||||
# alert if file descriptors are likely to exhaust within the next hour
|
||||
ALERT FdExhaustionClose
|
||||
IF predict_linear(instance:fd_utilization[10m], 3600) > 1
|
||||
FOR 10m
|
||||
LABELS {
|
||||
severity = "critical"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "file descriptors soon exhausted",
|
||||
description = "{{ $labels.job }} instance {{ $labels.instance }} will exhaust its file descriptors soon",
|
||||
}
|
||||
|
||||
### etcd proposal alerts ###
|
||||
|
||||
# alert if there are several failed proposals within an hour
|
||||
ALERT HighNumberOfFailedProposals
|
||||
IF increase(etcd_server_proposal_failed_total{job="etcd"}[1h]) > 5
|
||||
LABELS {
|
||||
severity = "warning"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "a high number of proposals within the etcd cluster are failing",
|
||||
description = "etcd instance {{ $labels.instance }} has seen {{ $value }} proposal failures within the last hour",
|
||||
}
|
||||
|
||||
### etcd disk io latency alerts ###
|
||||
|
||||
# alert if 99th percentile of fsync durations is higher than 500ms
|
||||
ALERT HighFsyncDurations
|
||||
IF histogram_quantile(0.99, rate(etcd_wal_fsync_durations_seconds_bucket[5m])) > 0.5
|
||||
FOR 10m
|
||||
LABELS {
|
||||
severity = "warning"
|
||||
}
|
||||
ANNOTATIONS {
|
||||
summary = "high fsync durations",
|
||||
description = "etcd instance {{ $labels.instance }} fync durations are high",
|
||||
}
|
@ -53,8 +53,11 @@
|
||||
- [shafreeck/cetcd](https://github.com/shafreeck/cetcd) - Supports v2
|
||||
|
||||
**C++ libraries**
|
||||
|
||||
- [edwardcapriolo/etcdcpp](https://github.com/edwardcapriolo/etcdcpp) - Supports v2
|
||||
- [suryanathan/etcdcpp](https://github.com/suryanathan/etcdcpp) - Supports v2 (with waits)
|
||||
- [nokia/etcd-cpp-api](https://github.com/nokia/etcd-cpp-api) - Supports v2
|
||||
- [nokia/etcd-cpp-apiv3](https://github.com/nokia/etcd-cpp-apiv3)
|
||||
|
||||
**Clojure libraries**
|
||||
|
||||
@ -112,7 +115,6 @@
|
||||
- [mattn/etcdenv](https://github.com/mattn/etcdenv) - "env" shebang with etcd integration
|
||||
- [kelseyhightower/confd](https://github.com/kelseyhightower/confd) - Manage local app config files using templates and data from etcd
|
||||
- [configdb](https://git.autistici.org/ai/configdb/tree/master) - A REST relational abstraction on top of arbitrary database backends, aimed at storing configs and inventories.
|
||||
- [scrz](https://github.com/scrz/scrz) - Container manager, stores configuration in etcd.
|
||||
- [fleet](https://github.com/coreos/fleet) - Distributed init system
|
||||
- [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes) - Container cluster manager introduced by Google.
|
||||
- [mailgun/vulcand](https://github.com/mailgun/vulcand) - HTTP proxy that uses etcd as a configuration backend.
|
||||
|
@ -108,7 +108,7 @@ ETCD_INITIAL_CLUSTER_STATE=existing
|
||||
Stop the existing proxy so we can wipe its state on disk and reload it with the new configuration:
|
||||
|
||||
``` bash
|
||||
px aux | grep etcd
|
||||
ps aux | grep etcd
|
||||
kill %etcd_proxy_pid%
|
||||
```
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Reporting Bugs
|
||||
|
||||
If you find bugs or documentation mistakes in the etcd project, please let us know by [opening an issue][issue]. We treat bugs and mistakes very seriously and believe no issue is too small. Before creating a bug report, please check that an issue reporting the same problem does not already exist.
|
||||
If you find bugs or documentation mistakes in the etcd project, please let us know by [opening an issue][etcd-issue]. We treat bugs and mistakes very seriously and believe no issue is too small. Before creating a bug report, please check that an issue reporting the same problem does not already exist.
|
||||
|
||||
To make your bug report accurate and easy to understand, please try to create bug reports that are:
|
||||
|
||||
|
@ -7,25 +7,25 @@ To prove out the design of the v3 API the team has also built [a number of examp
|
||||
# Design
|
||||
|
||||
1. Flatten binary key-value space
|
||||
|
||||
|
||||
2. Keep the event history until compaction
|
||||
- access to old version of keys
|
||||
- user controlled history compaction
|
||||
|
||||
|
||||
3. Support range query
|
||||
- Pagination support with limit argument
|
||||
- Support consistency guarantee across multiple range queries
|
||||
|
||||
|
||||
4. Replace TTL key with Lease
|
||||
- more efficient/ low cost keep alive
|
||||
- a logical group of TTL keys
|
||||
|
||||
|
||||
5. Replace CAS/CAD with multi-object Txn
|
||||
- MUCH MORE powerful and flexible
|
||||
|
||||
|
||||
6. Support efficient watching with multiple ranges
|
||||
|
||||
7. RPC API supports the completed set of APIs.
|
||||
7. RPC API supports the completed set of APIs.
|
||||
- more efficient than JSON/HTTP
|
||||
- additional txn/lease support
|
||||
|
||||
@ -56,7 +56,7 @@ the size in the future a little bit or make it configurable.
|
||||
// A put is always successful
|
||||
Put( PutRequest { key = foo, value = bar } )
|
||||
|
||||
PutResponse {
|
||||
PutResponse {
|
||||
cluster_id = 0x1000,
|
||||
member_id = 0x1,
|
||||
revision = 1,
|
||||
@ -119,7 +119,7 @@ RangeResponse {
|
||||
Txn(TxnRequest {
|
||||
// mod_revision of foo0 is equal to 1, mod_revision of foo1 is greater than 1
|
||||
compare = {
|
||||
{compareType = equal, key = foo0, mod_revision = 1},
|
||||
{compareType = equal, key = foo0, mod_revision = 1},
|
||||
{compareType = greater, key = foo1, mod_revision = 1}}
|
||||
},
|
||||
// if the comparison succeeds, put foo2 = bar2
|
||||
@ -156,7 +156,7 @@ Watch( WatchRequest{
|
||||
end_revision = 10000,
|
||||
// server decided notification frequency
|
||||
progress_notification = true,
|
||||
}
|
||||
}
|
||||
… // this can be a watch request stream
|
||||
)
|
||||
|
||||
@ -176,7 +176,7 @@ WatchResponse {
|
||||
},
|
||||
}
|
||||
…
|
||||
|
||||
|
||||
// a notification at 2000
|
||||
WatchResponse {
|
||||
cluster_id = 0x1000,
|
||||
@ -185,9 +185,9 @@ WatchResponse {
|
||||
raft_term = 0x1,
|
||||
// nil event as notification
|
||||
}
|
||||
|
||||
…
|
||||
|
||||
|
||||
…
|
||||
|
||||
// put (foo0=bar3000) event at 3000
|
||||
WatchResponse {
|
||||
cluster_id = 0x1000,
|
||||
@ -204,8 +204,8 @@ WatchResponse {
|
||||
},
|
||||
}
|
||||
…
|
||||
|
||||
|
||||
```
|
||||
|
||||
[api-protobuf]: https://github.com/coreos/etcd/blob/master/etcdserver/etcdserverpb/rpc.proto
|
||||
[kv-protobuf]: https://github.com/coreos/etcd/blob/master/storage/storagepb/kv.proto
|
||||
[api-protobuf]: https://github.com/coreos/etcd/blob/release-2.3/etcdserver/etcdserverpb/rpc.proto
|
||||
[kv-protobuf]: https://github.com/coreos/etcd/blob/release-2.3/storage/storagepb/kv.proto
|
||||
|
@ -16,7 +16,7 @@ etcd takes several certificate related configuration options, either through com
|
||||
|
||||
`--key-file=<path>`: Key for the certificate. Must be unencrypted.
|
||||
|
||||
`--client-cert-auth`: When this is set etcd will check all incoming HTTPS requests for a client certificate signed by the trusted CA, requests that don't supply a valid client certificate will fail.
|
||||
`--client-cert-auth`: When this is set etcd will check all incoming HTTPS requests for a client certificate signed by the trusted CA, requests that don't supply a valid client certificate will fail. If [authentication][auth] is enabled, the certificate provides credentials for the user name given by the Common Name field.
|
||||
|
||||
`--trusted-ca-file=<path>`: Trusted certificate authority.
|
||||
|
||||
@ -188,6 +188,7 @@ Make sure that you sign your certificates with a Subject Name your member's publ
|
||||
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][alt-name] too.
|
||||
|
||||
[cfssl]: https://github.com/cloudflare/cfssl
|
||||
[tls-setup]: /hack/tls-setup
|
||||
[tls-setup]: ../../hack/tls-setup
|
||||
[tls-guide]: https://github.com/coreos/docs/blob/master/os/generate-self-signed-certificates.md
|
||||
[alt-name]: http://wiki.cacert.org/FAQ/subjectAltName
|
||||
[auth]: authentication.md
|
||||
|
@ -1,5 +1,6 @@
|
||||
Anthony Romano <anthony.romano@coreos.com> (@heyitsanthony) pkg:*
|
||||
Brandon Philips <brandon.philips@coreos.com> (@philips) pkg:*
|
||||
Fanmin Shi <fanmin.shi@coreos.com> (@fanminshi) pkg:*
|
||||
Gyu-Ho Lee <gyu_ho.lee@coreos.com> (@gyuho) pkg:*
|
||||
Xiang Li <xiang.li@coreos.com> (@xiang90) pkg:*
|
||||
|
||||
|
27
NEWS
27
NEWS
@ -1,4 +1,29 @@
|
||||
etcd v3.1.0 (2017-01-13)
|
||||
etcd v3.1.6 (2017-04-19)
|
||||
- remove auth check in Status API
|
||||
- fill in Auth API response header
|
||||
|
||||
etcd v3.1.5 (2017-03-27)
|
||||
- add '/etc/nsswitch.conf' file to alpine-based Docker image
|
||||
- fix raft memory leak issue
|
||||
- fix Windows file path issues
|
||||
|
||||
etcd v3.1.4 (2017-03-22)
|
||||
|
||||
etcd v3.1.3 (2017-03-10)
|
||||
- use machine default host when advertise URLs are default
|
||||
values(localhost:2379,2380) AND if listen URL is 0.0.0.0
|
||||
- fix 'etcd gateway' schema handling in DNS discovery
|
||||
- fix sd_notify behaviors in gateway, grpc-proxy
|
||||
|
||||
etcd v3.1.2 (2017-02-24)
|
||||
- use IPv4 default host, by default (when IPv4 and IPv6 are available)
|
||||
- fix 'etcd gateway' with multiple endpoints
|
||||
|
||||
etcd v3.1.1 (2017-02-17)
|
||||
|
||||
etcd v2.3.8 (2017-02-17)
|
||||
|
||||
etcd v3.1.0 (2017-01-20)
|
||||
- faster linearizable reads (implements Raft read-index)
|
||||
- automatic leadership transfer when leader steps down
|
||||
- etcd uses default route IP if advertise URL is not given
|
||||
|
24
README.md
24
README.md
@ -11,7 +11,7 @@
|
||||
|
||||

|
||||
|
||||
etcd is a distributed, consistent key-value store for shared configuration and service discovery, with a focus on being:
|
||||
etcd is a distributed reliable key-value store for the most critical data of a distributed system, with a focus on being:
|
||||
|
||||
* *Simple*: well-defined, user-facing API (gRPC)
|
||||
* *Secure*: automatic TLS with optional client cert authentication
|
||||
@ -37,13 +37,11 @@ See [etcdctl][etcdctl] for a simple command line client.
|
||||
|
||||
### Getting etcd
|
||||
|
||||
The easiest way to get etcd is to use one of the pre-built release binaries which are available for OSX, Linux, Windows, AppC (ACI), and Docker. Instructions for using these binaries are on the [GitHub releases page][github-release].
|
||||
The easiest way to get etcd is to use one of the pre-built release binaries which are available for OSX, Linux, Windows, [rkt][rkt], and Docker. Instructions for using these binaries are on the [GitHub releases page][github-release].
|
||||
|
||||
For those wanting to try the very latest version, you can [build the latest version of etcd][dl-build] from the `master` branch.
|
||||
You will first need [*Go*](https://golang.org/) installed on your machine (version 1.6+ is required).
|
||||
All development occurs on `master`, including new features and bug fixes.
|
||||
Bug fixes are first targeted at `master` and subsequently ported to release branches, as described in the [branch management][branch-management] guide.
|
||||
For those wanting to try the very latest version, [build the latest version of etcd][dl-build] from the `master` branch. This first needs [*Go*](https://golang.org/) installed (version 1.8+ is required). All development occurs on `master`, including new features and bug fixes. Bug fixes are first targeted at `master` and subsequently ported to release branches, as described in the [branch management][branch-management] guide.
|
||||
|
||||
[rkt]: https://github.com/rkt/rkt/releases/
|
||||
[github-release]: https://github.com/coreos/etcd/releases/
|
||||
[branch-management]: ./Documentation/branch_management.md
|
||||
[dl-build]: ./Documentation/dl_build.md#build-the-latest-version
|
||||
@ -75,9 +73,9 @@ That's it! etcd is now running and serving client requests. For more
|
||||
|
||||
### etcd TCP ports
|
||||
|
||||
The [official etcd ports][iana-ports] are 2379 for client requests, and 2380 for peer communication.
|
||||
The [official etcd ports][iana-ports] are 2379 for client requests, and 2380 for peer communication.
|
||||
|
||||
[iana-ports]: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=etcd
|
||||
[iana-ports]: http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.txt
|
||||
|
||||
### Running a local etcd cluster
|
||||
|
||||
@ -95,7 +93,7 @@ Every cluster member and proxy accepts key value reads and key value writes.
|
||||
|
||||
### Running etcd on Kubernetes
|
||||
|
||||
If you want to run etcd cluster on Kubernetes, try [etcd operator](https://github.com/coreos/etcd-operator).
|
||||
To run an etcd cluster on Kubernetes, try [etcd operator](https://github.com/coreos/etcd-operator).
|
||||
|
||||
### Next steps
|
||||
|
||||
@ -105,7 +103,7 @@ Now it's time to dig into the full etcd API and other guides.
|
||||
- Explore the full gRPC [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].
|
||||
- Find [language bindings and tools][integrations].
|
||||
- Use TLS to [secure an etcd cluster][security].
|
||||
- [Tune etcd][tuning].
|
||||
|
||||
@ -113,7 +111,7 @@ Now it's time to dig into the full etcd API and other guides.
|
||||
[api]: ./Documentation/dev-guide/api_reference_v3.md
|
||||
[clustering]: ./Documentation/op-guide/clustering.md
|
||||
[configuration]: ./Documentation/op-guide/configuration.md
|
||||
[libraries-and-tools]: ./Documentation/libraries-and-tools.md
|
||||
[integrations]: ./Documentation/integrations.md
|
||||
[security]: ./Documentation/op-guide/security.md
|
||||
[tuning]: ./Documentation/tuning.md
|
||||
|
||||
@ -130,10 +128,8 @@ See [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches and the co
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
See [reporting bugs](Documentation/reporting_bugs.md) for details about reporting any issue you may encounter.
|
||||
See [reporting bugs](Documentation/reporting_bugs.md) for details about reporting any issues.
|
||||
|
||||
### License
|
||||
|
||||
etcd is under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
|
||||
|
16
ROADMAP.md
16
ROADMAP.md
@ -6,25 +6,17 @@ This document defines a high level roadmap for etcd development.
|
||||
|
||||
The dates below should not be considered authoritative, but rather indicative of the projected timeline of the project. The [milestones defined in GitHub](https://github.com/coreos/etcd/milestones) represent the most up-to-date and issue-for-issue plans.
|
||||
|
||||
etcd 3.0 is our current stable branch. The roadmap below outlines new features that will be added to etcd, and while subject to change, define what future stable will look like.
|
||||
etcd 3.2 is our current stable branch. The roadmap below outlines new features that will be added to etcd, and while subject to change, define what future stable will look like.
|
||||
|
||||
### etcd 3.1 (2016-Oct)
|
||||
- Stable L4 gateway
|
||||
- Experimental support for scalable proxy
|
||||
- Automatic leadership transfer for the rolling upgrade
|
||||
- V3 API improvements
|
||||
- Get previous key-value pair
|
||||
- Get only keys (ignore values)
|
||||
- Get only key count
|
||||
|
||||
### etcd 3.2 (2017-Apr)
|
||||
### etcd 3.2 (2017-May)
|
||||
- Stable scalable proxy
|
||||
- Proxy-as-client interface passthrough
|
||||
- Lock service
|
||||
- Namespacing proxy
|
||||
- JWT token based authentication
|
||||
- TLS Command Name and JWT token based authentication
|
||||
- Read-modify-write V3 Put
|
||||
- Improved watch performance
|
||||
- Support non-blocking concurrent read
|
||||
|
||||
### etcd 3.3 (?)
|
||||
- TBD
|
||||
|
@ -803,7 +803,7 @@ func init() { proto.RegisterFile("auth.proto", fileDescriptorAuth) }
|
||||
|
||||
var fileDescriptorAuth = []byte{
|
||||
// 288 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x90, 0xc1, 0x4a, 0xc3, 0x30,
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x90, 0xc1, 0x4a, 0xc3, 0x30,
|
||||
0x1c, 0xc6, 0x9b, 0xb6, 0x1b, 0xed, 0x5f, 0x27, 0x25, 0x0c, 0x0c, 0x13, 0x42, 0xe9, 0xa9, 0x78,
|
||||
0xa8, 0xb0, 0x5d, 0xbc, 0x2a, 0xf6, 0x20, 0x78, 0x90, 0x50, 0xf1, 0x28, 0x1d, 0x0d, 0x75, 0x6c,
|
||||
0x6d, 0x4a, 0x32, 0x91, 0xbe, 0x89, 0x07, 0x1f, 0x68, 0xc7, 0x3d, 0x82, 0xab, 0x2f, 0x22, 0x4d,
|
||||
|
137
auth/jwt.go
Normal file
137
auth/jwt.go
Normal file
@ -0,0 +1,137 @@
|
||||
// Copyright 2017 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"io/ioutil"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type tokenJWT struct {
|
||||
signMethod string
|
||||
signKey *rsa.PrivateKey
|
||||
verifyKey *rsa.PublicKey
|
||||
}
|
||||
|
||||
func (t *tokenJWT) enable() {}
|
||||
func (t *tokenJWT) disable() {}
|
||||
func (t *tokenJWT) invalidateUser(string) {}
|
||||
func (t *tokenJWT) genTokenPrefix() (string, error) { return "", nil }
|
||||
|
||||
func (t *tokenJWT) info(ctx context.Context, token string, rev uint64) (*AuthInfo, bool) {
|
||||
// rev isn't used in JWT, it is only used in simple token
|
||||
var (
|
||||
username string
|
||||
revision uint64
|
||||
)
|
||||
|
||||
parsed, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
|
||||
return t.verifyKey, nil
|
||||
})
|
||||
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
if !parsed.Valid {
|
||||
plog.Warningf("invalid jwt token: %s", token)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
claims := parsed.Claims.(jwt.MapClaims)
|
||||
|
||||
username = claims["username"].(string)
|
||||
revision = uint64(claims["revision"].(float64))
|
||||
default:
|
||||
plog.Warningf("failed to parse jwt token: %s", err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &AuthInfo{Username: username, Revision: revision}, true
|
||||
}
|
||||
|
||||
func (t *tokenJWT) assign(ctx context.Context, username string, revision uint64) (string, error) {
|
||||
// Future work: let a jwt token include permission information would be useful for
|
||||
// permission checking in proxy side.
|
||||
tk := jwt.NewWithClaims(jwt.GetSigningMethod(t.signMethod),
|
||||
jwt.MapClaims{
|
||||
"username": username,
|
||||
"revision": revision,
|
||||
})
|
||||
|
||||
token, err := tk.SignedString(t.signKey)
|
||||
if err != nil {
|
||||
plog.Debugf("failed to sign jwt token: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
plog.Debugf("jwt token: %s", token)
|
||||
|
||||
return token, err
|
||||
}
|
||||
|
||||
func prepareOpts(opts map[string]string) (jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath string, err error) {
|
||||
for k, v := range opts {
|
||||
switch k {
|
||||
case "sign-method":
|
||||
jwtSignMethod = v
|
||||
case "pub-key":
|
||||
jwtPubKeyPath = v
|
||||
case "priv-key":
|
||||
jwtPrivKeyPath = v
|
||||
default:
|
||||
plog.Errorf("unknown token specific option: %s", k)
|
||||
return "", "", "", ErrInvalidAuthOpts
|
||||
}
|
||||
}
|
||||
|
||||
return jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath, nil
|
||||
}
|
||||
|
||||
func newTokenProviderJWT(opts map[string]string) (*tokenJWT, error) {
|
||||
jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath, err := prepareOpts(opts)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidAuthOpts
|
||||
}
|
||||
|
||||
t := &tokenJWT{}
|
||||
|
||||
t.signMethod = jwtSignMethod
|
||||
|
||||
verifyBytes, err := ioutil.ReadFile(jwtPubKeyPath)
|
||||
if err != nil {
|
||||
plog.Errorf("failed to read public key (%s) for jwt: %s", jwtPubKeyPath, err)
|
||||
return nil, err
|
||||
}
|
||||
t.verifyKey, err = jwt.ParseRSAPublicKeyFromPEM(verifyBytes)
|
||||
if err != nil {
|
||||
plog.Errorf("failed to parse public key (%s): %s", jwtPubKeyPath, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signBytes, err := ioutil.ReadFile(jwtPrivKeyPath)
|
||||
if err != nil {
|
||||
plog.Errorf("failed to read private key (%s) for jwt: %s", jwtPrivKeyPath, err)
|
||||
return nil, err
|
||||
}
|
||||
t.signKey, err = jwt.ParseRSAPrivateKeyFromPEM(signBytes)
|
||||
if err != nil {
|
||||
plog.Errorf("failed to parse private key (%s): %s", jwtPrivKeyPath, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
@ -15,93 +15,11 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
|
||||
"github.com/coreos/etcd/auth/authpb"
|
||||
"github.com/coreos/etcd/mvcc/backend"
|
||||
"github.com/coreos/etcd/pkg/adt"
|
||||
)
|
||||
|
||||
// isSubset returns true if a is a subset of b.
|
||||
// If a is a prefix of b, then a is a subset of b.
|
||||
// Given intervals [a1,a2) and [b1,b2), is
|
||||
// the a interval a subset of b?
|
||||
func isSubset(a, b *rangePerm) bool {
|
||||
switch {
|
||||
case len(a.end) == 0 && len(b.end) == 0:
|
||||
// a, b are both keys
|
||||
return bytes.Equal(a.begin, b.begin)
|
||||
case len(b.end) == 0:
|
||||
// b is a key, a is a range
|
||||
return false
|
||||
case len(a.end) == 0:
|
||||
// a is a key, b is a range. need b1 <= a1 and a1 < b2
|
||||
return bytes.Compare(b.begin, a.begin) <= 0 && bytes.Compare(a.begin, b.end) < 0
|
||||
default:
|
||||
// both are ranges. need b1 <= a1 and a2 <= b2
|
||||
return bytes.Compare(b.begin, a.begin) <= 0 && bytes.Compare(a.end, b.end) <= 0
|
||||
}
|
||||
}
|
||||
|
||||
func isRangeEqual(a, b *rangePerm) bool {
|
||||
return bytes.Equal(a.begin, b.begin) && bytes.Equal(a.end, b.end)
|
||||
}
|
||||
|
||||
// removeSubsetRangePerms removes any rangePerms that are subsets of other rangePerms.
|
||||
// If there are equal ranges, removeSubsetRangePerms only keeps one of them.
|
||||
// It returns a sorted rangePerm slice.
|
||||
func removeSubsetRangePerms(perms []*rangePerm) (newp []*rangePerm) {
|
||||
sort.Sort(RangePermSliceByBegin(perms))
|
||||
var prev *rangePerm
|
||||
for i := range perms {
|
||||
if i == 0 {
|
||||
prev = perms[i]
|
||||
newp = append(newp, perms[i])
|
||||
continue
|
||||
}
|
||||
if isRangeEqual(perms[i], prev) {
|
||||
continue
|
||||
}
|
||||
if isSubset(perms[i], prev) {
|
||||
continue
|
||||
}
|
||||
if isSubset(prev, perms[i]) {
|
||||
prev = perms[i]
|
||||
newp[len(newp)-1] = perms[i]
|
||||
continue
|
||||
}
|
||||
prev = perms[i]
|
||||
newp = append(newp, perms[i])
|
||||
}
|
||||
return newp
|
||||
}
|
||||
|
||||
// mergeRangePerms merges adjacent rangePerms.
|
||||
func mergeRangePerms(perms []*rangePerm) []*rangePerm {
|
||||
var merged []*rangePerm
|
||||
perms = removeSubsetRangePerms(perms)
|
||||
|
||||
i := 0
|
||||
for i < len(perms) {
|
||||
begin, next := i, i
|
||||
for next+1 < len(perms) && bytes.Compare(perms[next].end, perms[next+1].begin) >= 0 {
|
||||
next++
|
||||
}
|
||||
// don't merge ["a", "b") with ["b", ""), because perms[next+1].end is empty.
|
||||
if next != begin && len(perms[next].end) > 0 {
|
||||
merged = append(merged, &rangePerm{begin: perms[begin].begin, end: perms[next].end})
|
||||
} else {
|
||||
merged = append(merged, perms[begin])
|
||||
if next != begin {
|
||||
merged = append(merged, perms[next])
|
||||
}
|
||||
}
|
||||
i = next + 1
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
func getMergedPerms(tx backend.BatchTx, userName string) *unifiedRangePermissions {
|
||||
user := getUser(tx, userName)
|
||||
if user == nil {
|
||||
@ -109,7 +27,8 @@ func getMergedPerms(tx backend.BatchTx, userName string) *unifiedRangePermission
|
||||
return nil
|
||||
}
|
||||
|
||||
var readPerms, writePerms []*rangePerm
|
||||
readPerms := &adt.IntervalTree{}
|
||||
writePerms := &adt.IntervalTree{}
|
||||
|
||||
for _, roleName := range user.Roles {
|
||||
role := getRole(tx, roleName)
|
||||
@ -118,48 +37,66 @@ func getMergedPerms(tx backend.BatchTx, userName string) *unifiedRangePermission
|
||||
}
|
||||
|
||||
for _, perm := range role.KeyPermission {
|
||||
rp := &rangePerm{begin: perm.Key, end: perm.RangeEnd}
|
||||
var ivl adt.Interval
|
||||
var rangeEnd []byte
|
||||
|
||||
if len(perm.RangeEnd) != 1 || perm.RangeEnd[0] != 0 {
|
||||
rangeEnd = perm.RangeEnd
|
||||
}
|
||||
|
||||
if len(perm.RangeEnd) != 0 {
|
||||
ivl = adt.NewBytesAffineInterval(perm.Key, rangeEnd)
|
||||
} else {
|
||||
ivl = adt.NewBytesAffinePoint(perm.Key)
|
||||
}
|
||||
|
||||
switch perm.PermType {
|
||||
case authpb.READWRITE:
|
||||
readPerms = append(readPerms, rp)
|
||||
writePerms = append(writePerms, rp)
|
||||
readPerms.Insert(ivl, struct{}{})
|
||||
writePerms.Insert(ivl, struct{}{})
|
||||
|
||||
case authpb.READ:
|
||||
readPerms = append(readPerms, rp)
|
||||
readPerms.Insert(ivl, struct{}{})
|
||||
|
||||
case authpb.WRITE:
|
||||
writePerms = append(writePerms, rp)
|
||||
writePerms.Insert(ivl, struct{}{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &unifiedRangePermissions{
|
||||
readPerms: mergeRangePerms(readPerms),
|
||||
writePerms: mergeRangePerms(writePerms),
|
||||
readPerms: readPerms,
|
||||
writePerms: writePerms,
|
||||
}
|
||||
}
|
||||
|
||||
func checkKeyPerm(cachedPerms *unifiedRangePermissions, key, rangeEnd []byte, permtyp authpb.Permission_Type) bool {
|
||||
var tocheck []*rangePerm
|
||||
func checkKeyInterval(cachedPerms *unifiedRangePermissions, key, rangeEnd []byte, permtyp authpb.Permission_Type) bool {
|
||||
if len(rangeEnd) == 1 && rangeEnd[0] == 0 {
|
||||
rangeEnd = nil
|
||||
}
|
||||
|
||||
ivl := adt.NewBytesAffineInterval(key, rangeEnd)
|
||||
switch permtyp {
|
||||
case authpb.READ:
|
||||
tocheck = cachedPerms.readPerms
|
||||
return cachedPerms.readPerms.Contains(ivl)
|
||||
case authpb.WRITE:
|
||||
tocheck = cachedPerms.writePerms
|
||||
return cachedPerms.writePerms.Contains(ivl)
|
||||
default:
|
||||
plog.Panicf("unknown auth type: %v", permtyp)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
requiredPerm := &rangePerm{begin: key, end: rangeEnd}
|
||||
|
||||
for _, perm := range tocheck {
|
||||
if isSubset(requiredPerm, perm) {
|
||||
return true
|
||||
}
|
||||
func checkKeyPoint(cachedPerms *unifiedRangePermissions, key []byte, permtyp authpb.Permission_Type) bool {
|
||||
pt := adt.NewBytesAffinePoint(key)
|
||||
switch permtyp {
|
||||
case authpb.READ:
|
||||
return cachedPerms.readPerms.Intersects(pt)
|
||||
case authpb.WRITE:
|
||||
return cachedPerms.writePerms.Intersects(pt)
|
||||
default:
|
||||
plog.Panicf("unknown auth type: %v", permtyp)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@ -175,7 +112,11 @@ func (as *authStore) isRangeOpPermitted(tx backend.BatchTx, userName string, key
|
||||
as.rangePermCache[userName] = perms
|
||||
}
|
||||
|
||||
return checkKeyPerm(as.rangePermCache[userName], key, rangeEnd, permtyp)
|
||||
if len(rangeEnd) == 0 {
|
||||
return checkKeyPoint(as.rangePermCache[userName], key, permtyp)
|
||||
}
|
||||
|
||||
return checkKeyInterval(as.rangePermCache[userName], key, rangeEnd, permtyp)
|
||||
}
|
||||
|
||||
func (as *authStore) clearCachedPerm() {
|
||||
@ -187,35 +128,6 @@ func (as *authStore) invalidateCachedPerm(userName string) {
|
||||
}
|
||||
|
||||
type unifiedRangePermissions struct {
|
||||
// readPerms[i] and readPerms[j] (i != j) don't overlap
|
||||
readPerms []*rangePerm
|
||||
// writePerms[i] and writePerms[j] (i != j) don't overlap, too
|
||||
writePerms []*rangePerm
|
||||
}
|
||||
|
||||
type rangePerm struct {
|
||||
begin, end []byte
|
||||
}
|
||||
|
||||
type RangePermSliceByBegin []*rangePerm
|
||||
|
||||
func (slice RangePermSliceByBegin) Len() int {
|
||||
return len(slice)
|
||||
}
|
||||
|
||||
func (slice RangePermSliceByBegin) Less(i, j int) bool {
|
||||
switch bytes.Compare(slice[i].begin, slice[j].begin) {
|
||||
case 0: // begin(i) == begin(j)
|
||||
return bytes.Compare(slice[i].end, slice[j].end) == -1
|
||||
|
||||
case -1: // begin(i) < begin(j)
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (slice RangePermSliceByBegin) Swap(i, j int) {
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
readPerms *adt.IntervalTree
|
||||
writePerms *adt.IntervalTree
|
||||
}
|
||||
|
@ -15,165 +15,45 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/auth/authpb"
|
||||
"github.com/coreos/etcd/pkg/adt"
|
||||
)
|
||||
|
||||
func isPermsEqual(a, b []*rangePerm) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range a {
|
||||
if len(b) <= i {
|
||||
return false
|
||||
}
|
||||
|
||||
if !bytes.Equal(a[i].begin, b[i].begin) || !bytes.Equal(a[i].end, b[i].end) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func TestGetMergedPerms(t *testing.T) {
|
||||
func TestRangePermission(t *testing.T) {
|
||||
tests := []struct {
|
||||
params []*rangePerm
|
||||
want []*rangePerm
|
||||
perms []adt.Interval
|
||||
begin []byte
|
||||
end []byte
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("b")}},
|
||||
[]*rangePerm{{[]byte("a"), []byte("b")}},
|
||||
[]adt.Interval{adt.NewBytesAffineInterval([]byte("a"), []byte("c")), adt.NewBytesAffineInterval([]byte("x"), []byte("z"))},
|
||||
[]byte("a"), []byte("z"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("b")}, {[]byte("b"), []byte("")}},
|
||||
[]*rangePerm{{[]byte("a"), []byte("b")}, {[]byte("b"), []byte("")}},
|
||||
[]adt.Interval{adt.NewBytesAffineInterval([]byte("a"), []byte("f")), adt.NewBytesAffineInterval([]byte("c"), []byte("d")), adt.NewBytesAffineInterval([]byte("f"), []byte("z"))},
|
||||
[]byte("a"), []byte("z"),
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("b")}, {[]byte("b"), []byte("c")}},
|
||||
[]*rangePerm{{[]byte("a"), []byte("c")}},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("c")}, {[]byte("b"), []byte("d")}},
|
||||
[]*rangePerm{{[]byte("a"), []byte("d")}},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("b")}, {[]byte("b"), []byte("c")}, {[]byte("d"), []byte("e")}},
|
||||
[]*rangePerm{{[]byte("a"), []byte("c")}, {[]byte("d"), []byte("e")}},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("b")}, {[]byte("c"), []byte("d")}, {[]byte("e"), []byte("f")}},
|
||||
[]*rangePerm{{[]byte("a"), []byte("b")}, {[]byte("c"), []byte("d")}, {[]byte("e"), []byte("f")}},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{{[]byte("e"), []byte("f")}, {[]byte("c"), []byte("d")}, {[]byte("a"), []byte("b")}},
|
||||
[]*rangePerm{{[]byte("a"), []byte("b")}, {[]byte("c"), []byte("d")}, {[]byte("e"), []byte("f")}},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("b")}, {[]byte("c"), []byte("d")}, {[]byte("a"), []byte("z")}},
|
||||
[]*rangePerm{{[]byte("a"), []byte("z")}},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("b")}, {[]byte("c"), []byte("d")}, {[]byte("a"), []byte("z")}, {[]byte("1"), []byte("9")}},
|
||||
[]*rangePerm{{[]byte("1"), []byte("9")}, {[]byte("a"), []byte("z")}},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("b")}, {[]byte("c"), []byte("d")}, {[]byte("a"), []byte("z")}, {[]byte("1"), []byte("a")}},
|
||||
[]*rangePerm{{[]byte("1"), []byte("z")}},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("b")}, {[]byte("a"), []byte("z")}, {[]byte("5"), []byte("6")}, {[]byte("1"), []byte("9")}},
|
||||
[]*rangePerm{{[]byte("1"), []byte("9")}, {[]byte("a"), []byte("z")}},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("b")}, {[]byte("b"), []byte("c")}, {[]byte("c"), []byte("d")}, {[]byte("d"), []byte("f")}, {[]byte("1"), []byte("9")}},
|
||||
[]*rangePerm{{[]byte("1"), []byte("9")}, {[]byte("a"), []byte("f")}},
|
||||
},
|
||||
// overlapping
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("f")}, {[]byte("b"), []byte("g")}},
|
||||
[]*rangePerm{{[]byte("a"), []byte("g")}},
|
||||
},
|
||||
// keys
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("")}, {[]byte("b"), []byte("")}},
|
||||
[]*rangePerm{{[]byte("a"), []byte("")}, {[]byte("b"), []byte("")}},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("")}, {[]byte("a"), []byte("c")}},
|
||||
[]*rangePerm{{[]byte("a"), []byte("c")}},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("")}, {[]byte("a"), []byte("c")}, {[]byte("b"), []byte("")}},
|
||||
[]*rangePerm{{[]byte("a"), []byte("c")}},
|
||||
},
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("")}, {[]byte("b"), []byte("c")}, {[]byte("b"), []byte("")}, {[]byte("c"), []byte("")}, {[]byte("d"), []byte("")}},
|
||||
[]*rangePerm{{[]byte("a"), []byte("")}, {[]byte("b"), []byte("c")}, {[]byte("c"), []byte("")}, {[]byte("d"), []byte("")}},
|
||||
},
|
||||
// duplicate ranges
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("f")}, {[]byte("a"), []byte("f")}},
|
||||
[]*rangePerm{{[]byte("a"), []byte("f")}},
|
||||
},
|
||||
// duplicate keys
|
||||
{
|
||||
[]*rangePerm{{[]byte("a"), []byte("")}, {[]byte("a"), []byte("")}, {[]byte("a"), []byte("")}},
|
||||
[]*rangePerm{{[]byte("a"), []byte("")}},
|
||||
[]adt.Interval{adt.NewBytesAffineInterval([]byte("a"), []byte("d")), adt.NewBytesAffineInterval([]byte("a"), []byte("b")), adt.NewBytesAffineInterval([]byte("c"), []byte("f"))},
|
||||
[]byte("a"), []byte("f"),
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
result := mergeRangePerms(tt.params)
|
||||
if !isPermsEqual(result, tt.want) {
|
||||
t.Errorf("#%d: result=%q, want=%q", i, result, tt.want)
|
||||
readPerms := &adt.IntervalTree{}
|
||||
for _, p := range tt.perms {
|
||||
readPerms.Insert(p, struct{}{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveSubsetRangePerms(t *testing.T) {
|
||||
tests := []struct {
|
||||
perms []*rangePerm
|
||||
expect []*rangePerm
|
||||
}{
|
||||
{ // subsets converge
|
||||
[]*rangePerm{{[]byte{2}, []byte{3}}, {[]byte{2}, []byte{5}}, {[]byte{1}, []byte{4}}},
|
||||
[]*rangePerm{{[]byte{1}, []byte{4}}, {[]byte{2}, []byte{5}}},
|
||||
},
|
||||
{ // subsets converge
|
||||
[]*rangePerm{{[]byte{0}, []byte{3}}, {[]byte{0}, []byte{1}}, {[]byte{2}, []byte{4}}, {[]byte{0}, []byte{2}}},
|
||||
[]*rangePerm{{[]byte{0}, []byte{3}}, {[]byte{2}, []byte{4}}},
|
||||
},
|
||||
{ // biggest range at the end
|
||||
[]*rangePerm{{[]byte{2}, []byte{3}}, {[]byte{0}, []byte{2}}, {[]byte{1}, []byte{4}}, {[]byte{0}, []byte{5}}},
|
||||
[]*rangePerm{{[]byte{0}, []byte{5}}},
|
||||
},
|
||||
{ // biggest range at the beginning
|
||||
[]*rangePerm{{[]byte{0}, []byte{5}}, {[]byte{2}, []byte{3}}, {[]byte{0}, []byte{2}}, {[]byte{1}, []byte{4}}},
|
||||
[]*rangePerm{{[]byte{0}, []byte{5}}},
|
||||
},
|
||||
{ // no overlapping ranges
|
||||
[]*rangePerm{{[]byte{2}, []byte{3}}, {[]byte{0}, []byte{1}}, {[]byte{4}, []byte{7}}, {[]byte{8}, []byte{15}}},
|
||||
[]*rangePerm{{[]byte{0}, []byte{1}}, {[]byte{2}, []byte{3}}, {[]byte{4}, []byte{7}}, {[]byte{8}, []byte{15}}},
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
rs := removeSubsetRangePerms(tt.perms)
|
||||
if !reflect.DeepEqual(rs, tt.expect) {
|
||||
t.Fatalf("#%d: unexpected rangePerms %q, got %q", i, printPerms(rs), printPerms(tt.expect))
|
||||
result := checkKeyInterval(&unifiedRangePermissions{readPerms: readPerms}, tt.begin, tt.end, authpb.READ)
|
||||
if result != tt.want {
|
||||
t.Errorf("#%d: result=%t, want=%t", i, result, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printPerms(rs []*rangePerm) (txt string) {
|
||||
for i, p := range rs {
|
||||
if i != 0 {
|
||||
txt += ","
|
||||
}
|
||||
txt += fmt.Sprintf("%+v", *p)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -19,88 +19,89 @@ package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const (
|
||||
letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
defaultSimpleTokenLength = 16
|
||||
)
|
||||
|
||||
// var for testing purposes
|
||||
var (
|
||||
simpleTokenTTL = 5 * time.Minute
|
||||
simpleTokenTTLResolution = 1 * time.Second
|
||||
)
|
||||
|
||||
type simpleTokenTTLKeeper struct {
|
||||
tokens map[string]time.Time
|
||||
addSimpleTokenCh chan string
|
||||
resetSimpleTokenCh chan string
|
||||
deleteSimpleTokenCh chan string
|
||||
stopCh chan chan struct{}
|
||||
deleteTokenFunc func(string)
|
||||
}
|
||||
|
||||
func NewSimpleTokenTTLKeeper(deletefunc func(string)) *simpleTokenTTLKeeper {
|
||||
stk := &simpleTokenTTLKeeper{
|
||||
tokens: make(map[string]time.Time),
|
||||
addSimpleTokenCh: make(chan string, 1),
|
||||
resetSimpleTokenCh: make(chan string, 1),
|
||||
deleteSimpleTokenCh: make(chan string, 1),
|
||||
stopCh: make(chan chan struct{}),
|
||||
deleteTokenFunc: deletefunc,
|
||||
}
|
||||
go stk.run()
|
||||
return stk
|
||||
tokens map[string]time.Time
|
||||
donec chan struct{}
|
||||
stopc chan struct{}
|
||||
deleteTokenFunc func(string)
|
||||
mu *sync.Mutex
|
||||
}
|
||||
|
||||
func (tm *simpleTokenTTLKeeper) stop() {
|
||||
waitCh := make(chan struct{})
|
||||
tm.stopCh <- waitCh
|
||||
<-waitCh
|
||||
close(tm.stopCh)
|
||||
select {
|
||||
case tm.stopc <- struct{}{}:
|
||||
case <-tm.donec:
|
||||
}
|
||||
<-tm.donec
|
||||
}
|
||||
|
||||
func (tm *simpleTokenTTLKeeper) addSimpleToken(token string) {
|
||||
tm.addSimpleTokenCh <- token
|
||||
tm.tokens[token] = time.Now().Add(simpleTokenTTL)
|
||||
}
|
||||
|
||||
func (tm *simpleTokenTTLKeeper) resetSimpleToken(token string) {
|
||||
tm.resetSimpleTokenCh <- token
|
||||
if _, ok := tm.tokens[token]; ok {
|
||||
tm.tokens[token] = time.Now().Add(simpleTokenTTL)
|
||||
}
|
||||
}
|
||||
|
||||
func (tm *simpleTokenTTLKeeper) deleteSimpleToken(token string) {
|
||||
tm.deleteSimpleTokenCh <- token
|
||||
delete(tm.tokens, token)
|
||||
}
|
||||
|
||||
func (tm *simpleTokenTTLKeeper) run() {
|
||||
tokenTicker := time.NewTicker(simpleTokenTTLResolution)
|
||||
defer tokenTicker.Stop()
|
||||
defer func() {
|
||||
tokenTicker.Stop()
|
||||
close(tm.donec)
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case t := <-tm.addSimpleTokenCh:
|
||||
tm.tokens[t] = time.Now().Add(simpleTokenTTL)
|
||||
case t := <-tm.resetSimpleTokenCh:
|
||||
if _, ok := tm.tokens[t]; ok {
|
||||
tm.tokens[t] = time.Now().Add(simpleTokenTTL)
|
||||
}
|
||||
case t := <-tm.deleteSimpleTokenCh:
|
||||
delete(tm.tokens, t)
|
||||
case <-tokenTicker.C:
|
||||
nowtime := time.Now()
|
||||
tm.mu.Lock()
|
||||
for t, tokenendtime := range tm.tokens {
|
||||
if nowtime.After(tokenendtime) {
|
||||
tm.deleteTokenFunc(t)
|
||||
delete(tm.tokens, t)
|
||||
}
|
||||
}
|
||||
case waitCh := <-tm.stopCh:
|
||||
tm.tokens = make(map[string]time.Time)
|
||||
waitCh <- struct{}{}
|
||||
tm.mu.Unlock()
|
||||
case <-tm.stopc:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (as *authStore) GenSimpleToken() (string, error) {
|
||||
type tokenSimple struct {
|
||||
indexWaiter func(uint64) <-chan struct{}
|
||||
simpleTokenKeeper *simpleTokenTTLKeeper
|
||||
simpleTokensMu sync.Mutex
|
||||
simpleTokens map[string]string // token -> username
|
||||
}
|
||||
|
||||
func (t *tokenSimple) genTokenPrefix() (string, error) {
|
||||
ret := make([]byte, defaultSimpleTokenLength)
|
||||
|
||||
for i := 0; i < defaultSimpleTokenLength; i++ {
|
||||
@ -115,27 +116,105 @@ func (as *authStore) GenSimpleToken() (string, error) {
|
||||
return string(ret), nil
|
||||
}
|
||||
|
||||
func (as *authStore) assignSimpleTokenToUser(username, token string) {
|
||||
as.simpleTokensMu.Lock()
|
||||
|
||||
_, ok := as.simpleTokens[token]
|
||||
func (t *tokenSimple) assignSimpleTokenToUser(username, token string) {
|
||||
t.simpleTokensMu.Lock()
|
||||
_, ok := t.simpleTokens[token]
|
||||
if ok {
|
||||
plog.Panicf("token %s is alredy used", token)
|
||||
}
|
||||
|
||||
as.simpleTokens[token] = username
|
||||
as.simpleTokenKeeper.addSimpleToken(token)
|
||||
as.simpleTokensMu.Unlock()
|
||||
t.simpleTokens[token] = username
|
||||
t.simpleTokenKeeper.addSimpleToken(token)
|
||||
t.simpleTokensMu.Unlock()
|
||||
}
|
||||
|
||||
func (as *authStore) invalidateUser(username string) {
|
||||
as.simpleTokensMu.Lock()
|
||||
defer as.simpleTokensMu.Unlock()
|
||||
|
||||
for token, name := range as.simpleTokens {
|
||||
func (t *tokenSimple) invalidateUser(username string) {
|
||||
if t.simpleTokenKeeper == nil {
|
||||
return
|
||||
}
|
||||
t.simpleTokensMu.Lock()
|
||||
for token, name := range t.simpleTokens {
|
||||
if strings.Compare(name, username) == 0 {
|
||||
delete(as.simpleTokens, token)
|
||||
as.simpleTokenKeeper.deleteSimpleToken(token)
|
||||
delete(t.simpleTokens, token)
|
||||
t.simpleTokenKeeper.deleteSimpleToken(token)
|
||||
}
|
||||
}
|
||||
t.simpleTokensMu.Unlock()
|
||||
}
|
||||
|
||||
func (t *tokenSimple) enable() {
|
||||
delf := func(tk string) {
|
||||
if username, ok := t.simpleTokens[tk]; ok {
|
||||
plog.Infof("deleting token %s for user %s", tk, username)
|
||||
delete(t.simpleTokens, tk)
|
||||
}
|
||||
}
|
||||
t.simpleTokenKeeper = &simpleTokenTTLKeeper{
|
||||
tokens: make(map[string]time.Time),
|
||||
donec: make(chan struct{}),
|
||||
stopc: make(chan struct{}),
|
||||
deleteTokenFunc: delf,
|
||||
mu: &t.simpleTokensMu,
|
||||
}
|
||||
go t.simpleTokenKeeper.run()
|
||||
}
|
||||
|
||||
func (t *tokenSimple) disable() {
|
||||
t.simpleTokensMu.Lock()
|
||||
tk := t.simpleTokenKeeper
|
||||
t.simpleTokenKeeper = nil
|
||||
t.simpleTokens = make(map[string]string) // invalidate all tokens
|
||||
t.simpleTokensMu.Unlock()
|
||||
if tk != nil {
|
||||
tk.stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tokenSimple) info(ctx context.Context, token string, revision uint64) (*AuthInfo, bool) {
|
||||
if !t.isValidSimpleToken(ctx, token) {
|
||||
return nil, false
|
||||
}
|
||||
t.simpleTokensMu.Lock()
|
||||
username, ok := t.simpleTokens[token]
|
||||
if ok && t.simpleTokenKeeper != nil {
|
||||
t.simpleTokenKeeper.resetSimpleToken(token)
|
||||
}
|
||||
t.simpleTokensMu.Unlock()
|
||||
return &AuthInfo{Username: username, Revision: revision}, ok
|
||||
}
|
||||
|
||||
func (t *tokenSimple) assign(ctx context.Context, username string, rev uint64) (string, error) {
|
||||
// rev isn't used in simple token, it is only used in JWT
|
||||
index := ctx.Value("index").(uint64)
|
||||
simpleToken := ctx.Value("simpleToken").(string)
|
||||
token := fmt.Sprintf("%s.%d", simpleToken, index)
|
||||
t.assignSimpleTokenToUser(username, token)
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (t *tokenSimple) isValidSimpleToken(ctx context.Context, token string) bool {
|
||||
splitted := strings.Split(token, ".")
|
||||
if len(splitted) != 2 {
|
||||
return false
|
||||
}
|
||||
index, err := strconv.Atoi(splitted[1])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
select {
|
||||
case <-t.indexWaiter(uint64(index)):
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func newTokenProviderSimple(indexWaiter func(uint64) <-chan struct{}) *tokenSimple {
|
||||
return &tokenSimple{
|
||||
simpleTokens: make(map[string]string),
|
||||
indexWaiter: indexWaiter,
|
||||
}
|
||||
}
|
||||
|
288
auth/store.go
288
auth/store.go
@ -18,10 +18,10 @@ import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/coreos/etcd/auth/authpb"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
@ -29,6 +29,9 @@ import (
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/peer"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -57,6 +60,9 @@ var (
|
||||
ErrPermissionNotGranted = errors.New("auth: permission is not granted to the role")
|
||||
ErrAuthNotEnabled = errors.New("auth: authentication is not enabled")
|
||||
ErrAuthOldRevision = errors.New("auth: revision in header is old")
|
||||
ErrInvalidAuthToken = errors.New("auth: invalid auth token")
|
||||
ErrInvalidAuthOpts = errors.New("auth: invalid auth options")
|
||||
ErrInvalidAuthMgmt = errors.New("auth: invalid auth management")
|
||||
|
||||
// BcryptCost is the algorithm cost / strength for hashing auth passwords
|
||||
BcryptCost = bcrypt.DefaultCost
|
||||
@ -126,10 +132,6 @@ type AuthStore interface {
|
||||
// RoleList gets a list of all roles
|
||||
RoleList(r *pb.AuthRoleListRequest) (*pb.AuthRoleListResponse, error)
|
||||
|
||||
// AuthInfoFromToken gets a username from the given Token and current revision number
|
||||
// (The revision number is used for preventing the TOCTOU problem)
|
||||
AuthInfoFromToken(token string) (*AuthInfo, bool)
|
||||
|
||||
// IsPutPermitted checks put permission of the user
|
||||
IsPutPermitted(authInfo *AuthInfo, key []byte) error
|
||||
|
||||
@ -142,8 +144,9 @@ type AuthStore interface {
|
||||
// IsAdminPermitted checks admin permission of the user
|
||||
IsAdminPermitted(authInfo *AuthInfo) error
|
||||
|
||||
// GenSimpleToken produces a simple random string
|
||||
GenSimpleToken() (string, error)
|
||||
// GenTokenPrefix produces a random string in a case of simple token
|
||||
// in a case of JWT, it produces an empty string
|
||||
GenTokenPrefix() (string, error)
|
||||
|
||||
// Revision gets current revision of authStore
|
||||
Revision() uint64
|
||||
@ -153,20 +156,35 @@ type AuthStore interface {
|
||||
|
||||
// Close does cleanup of AuthStore
|
||||
Close() error
|
||||
|
||||
// AuthInfoFromCtx gets AuthInfo from gRPC's context
|
||||
AuthInfoFromCtx(ctx context.Context) (*AuthInfo, error)
|
||||
|
||||
// AuthInfoFromTLS gets AuthInfo from TLS info of gRPC's context
|
||||
AuthInfoFromTLS(ctx context.Context) *AuthInfo
|
||||
}
|
||||
|
||||
type TokenProvider interface {
|
||||
info(ctx context.Context, token string, revision uint64) (*AuthInfo, bool)
|
||||
assign(ctx context.Context, username string, revision uint64) (string, error)
|
||||
enable()
|
||||
disable()
|
||||
|
||||
invalidateUser(string)
|
||||
genTokenPrefix() (string, error)
|
||||
}
|
||||
|
||||
type authStore struct {
|
||||
// atomic operations; need 64-bit align, or 32-bit tests will crash
|
||||
revision uint64
|
||||
|
||||
be backend.Backend
|
||||
enabled bool
|
||||
enabledMu sync.RWMutex
|
||||
|
||||
rangePermCache map[string]*unifiedRangePermissions // username -> unifiedRangePermissions
|
||||
|
||||
simpleTokensMu sync.RWMutex
|
||||
simpleTokens map[string]string // token -> username
|
||||
simpleTokenKeeper *simpleTokenTTLKeeper
|
||||
|
||||
revision uint64
|
||||
tokenProvider TokenProvider
|
||||
}
|
||||
|
||||
func (as *authStore) AuthEnable() error {
|
||||
@ -196,20 +214,11 @@ func (as *authStore) AuthEnable() error {
|
||||
tx.UnsafePut(authBucketName, enableFlagKey, authEnabled)
|
||||
|
||||
as.enabled = true
|
||||
|
||||
tokenDeleteFunc := func(t string) {
|
||||
as.simpleTokensMu.Lock()
|
||||
defer as.simpleTokensMu.Unlock()
|
||||
if username, ok := as.simpleTokens[t]; ok {
|
||||
plog.Infof("deleting token %s for user %s", t, username)
|
||||
delete(as.simpleTokens, t)
|
||||
}
|
||||
}
|
||||
as.simpleTokenKeeper = NewSimpleTokenTTLKeeper(tokenDeleteFunc)
|
||||
as.tokenProvider.enable()
|
||||
|
||||
as.rangePermCache = make(map[string]*unifiedRangePermissions)
|
||||
|
||||
as.revision = getRevision(tx)
|
||||
as.setRevision(getRevision(tx))
|
||||
|
||||
plog.Noticef("Authentication enabled")
|
||||
|
||||
@ -231,14 +240,7 @@ func (as *authStore) AuthDisable() {
|
||||
b.ForceCommit()
|
||||
|
||||
as.enabled = false
|
||||
|
||||
as.simpleTokensMu.Lock()
|
||||
as.simpleTokens = make(map[string]string) // invalidate all tokens
|
||||
as.simpleTokensMu.Unlock()
|
||||
if as.simpleTokenKeeper != nil {
|
||||
as.simpleTokenKeeper.stop()
|
||||
as.simpleTokenKeeper = nil
|
||||
}
|
||||
as.tokenProvider.disable()
|
||||
|
||||
plog.Noticef("Authentication disabled")
|
||||
}
|
||||
@ -249,10 +251,7 @@ func (as *authStore) Close() error {
|
||||
if !as.enabled {
|
||||
return nil
|
||||
}
|
||||
if as.simpleTokenKeeper != nil {
|
||||
as.simpleTokenKeeper.stop()
|
||||
as.simpleTokenKeeper = nil
|
||||
}
|
||||
as.tokenProvider.disable()
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -261,10 +260,6 @@ func (as *authStore) Authenticate(ctx context.Context, username, password string
|
||||
return nil, ErrAuthNotEnabled
|
||||
}
|
||||
|
||||
// TODO(mitake): after adding jwt support, branching based on values of ctx is required
|
||||
index := ctx.Value("index").(uint64)
|
||||
simpleToken := ctx.Value("simpleToken").(string)
|
||||
|
||||
tx := as.be.BatchTx()
|
||||
tx.Lock()
|
||||
defer tx.Unlock()
|
||||
@ -274,14 +269,23 @@ func (as *authStore) Authenticate(ctx context.Context, username, password string
|
||||
return nil, ErrAuthFailed
|
||||
}
|
||||
|
||||
token := fmt.Sprintf("%s.%d", simpleToken, index)
|
||||
as.assignSimpleTokenToUser(username, token)
|
||||
// Password checking is already performed in the API layer, so we don't need to check for now.
|
||||
// Staleness of password can be detected with OCC in the API layer, too.
|
||||
|
||||
plog.Infof("authorized %s, token is %s", username, token)
|
||||
token, err := as.tokenProvider.assign(ctx, username, as.Revision())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plog.Debugf("authorized %s, token is %s", username, token)
|
||||
return &pb.AuthenticateResponse{Token: token}, nil
|
||||
}
|
||||
|
||||
func (as *authStore) CheckPassword(username, password string) (uint64, error) {
|
||||
if !as.isAuthEnabled() {
|
||||
return 0, ErrAuthNotEnabled
|
||||
}
|
||||
|
||||
tx := as.be.BatchTx()
|
||||
tx.Lock()
|
||||
defer tx.Unlock()
|
||||
@ -311,7 +315,7 @@ func (as *authStore) Recover(be backend.Backend) {
|
||||
}
|
||||
}
|
||||
|
||||
as.revision = getRevision(tx)
|
||||
as.setRevision(getRevision(tx))
|
||||
|
||||
tx.Unlock()
|
||||
|
||||
@ -355,6 +359,11 @@ func (as *authStore) UserAdd(r *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse,
|
||||
}
|
||||
|
||||
func (as *authStore) UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUserDeleteResponse, error) {
|
||||
if as.enabled && strings.Compare(r.Name, rootUser) == 0 {
|
||||
plog.Errorf("the user root must not be deleted")
|
||||
return nil, ErrInvalidAuthMgmt
|
||||
}
|
||||
|
||||
tx := as.be.BatchTx()
|
||||
tx.Lock()
|
||||
defer tx.Unlock()
|
||||
@ -369,7 +378,7 @@ func (as *authStore) UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUserDelete
|
||||
as.commitRevision(tx)
|
||||
|
||||
as.invalidateCachedPerm(r.Name)
|
||||
as.invalidateUser(r.Name)
|
||||
as.tokenProvider.invalidateUser(r.Name)
|
||||
|
||||
plog.Noticef("deleted a user: %s", r.Name)
|
||||
|
||||
@ -405,7 +414,7 @@ func (as *authStore) UserChangePassword(r *pb.AuthUserChangePasswordRequest) (*p
|
||||
as.commitRevision(tx)
|
||||
|
||||
as.invalidateCachedPerm(r.Name)
|
||||
as.invalidateUser(r.Name)
|
||||
as.tokenProvider.invalidateUser(r.Name)
|
||||
|
||||
plog.Noticef("changed a password of a user: %s", r.Name)
|
||||
|
||||
@ -480,6 +489,11 @@ func (as *authStore) UserList(r *pb.AuthUserListRequest) (*pb.AuthUserListRespon
|
||||
}
|
||||
|
||||
func (as *authStore) UserRevokeRole(r *pb.AuthUserRevokeRoleRequest) (*pb.AuthUserRevokeRoleResponse, error) {
|
||||
if as.enabled && strings.Compare(r.Name, rootUser) == 0 && strings.Compare(r.Role, rootRole) == 0 {
|
||||
plog.Errorf("the role root must not be revoked from the user root")
|
||||
return nil, ErrInvalidAuthMgmt
|
||||
}
|
||||
|
||||
tx := as.be.BatchTx()
|
||||
tx.Lock()
|
||||
defer tx.Unlock()
|
||||
@ -582,17 +596,10 @@ func (as *authStore) RoleRevokePermission(r *pb.AuthRoleRevokePermissionRequest)
|
||||
}
|
||||
|
||||
func (as *authStore) RoleDelete(r *pb.AuthRoleDeleteRequest) (*pb.AuthRoleDeleteResponse, error) {
|
||||
// TODO(mitake): current scheme of role deletion allows existing users to have the deleted roles
|
||||
//
|
||||
// Assume a case like below:
|
||||
// create a role r1
|
||||
// create a user u1 and grant r1 to u1
|
||||
// delete r1
|
||||
//
|
||||
// After this sequence, u1 is still granted the role r1. So if admin create a new role with the name r1,
|
||||
// the new r1 is automatically granted u1.
|
||||
// In some cases, it would be confusing. So we need to provide an option for deleting the grant relation
|
||||
// from all users.
|
||||
if as.enabled && strings.Compare(r.Role, rootRole) == 0 {
|
||||
plog.Errorf("the role root must not be deleted")
|
||||
return nil, ErrInvalidAuthMgmt
|
||||
}
|
||||
|
||||
tx := as.be.BatchTx()
|
||||
tx.Lock()
|
||||
@ -605,6 +612,28 @@ func (as *authStore) RoleDelete(r *pb.AuthRoleDeleteRequest) (*pb.AuthRoleDelete
|
||||
|
||||
delRole(tx, r.Role)
|
||||
|
||||
users := getAllUsers(tx)
|
||||
for _, user := range users {
|
||||
updatedUser := &authpb.User{
|
||||
Name: user.Name,
|
||||
Password: user.Password,
|
||||
}
|
||||
|
||||
for _, role := range user.Roles {
|
||||
if strings.Compare(role, r.Role) != 0 {
|
||||
updatedUser.Roles = append(updatedUser.Roles, role)
|
||||
}
|
||||
}
|
||||
|
||||
if len(updatedUser.Roles) == len(user.Roles) {
|
||||
continue
|
||||
}
|
||||
|
||||
putUser(tx, updatedUser)
|
||||
|
||||
as.invalidateCachedPerm(string(user.Name))
|
||||
}
|
||||
|
||||
as.commitRevision(tx)
|
||||
|
||||
plog.Noticef("deleted role %s", r.Role)
|
||||
@ -634,14 +663,8 @@ func (as *authStore) RoleAdd(r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse,
|
||||
return &pb.AuthRoleAddResponse{}, nil
|
||||
}
|
||||
|
||||
func (as *authStore) AuthInfoFromToken(token string) (*AuthInfo, bool) {
|
||||
as.simpleTokensMu.RLock()
|
||||
defer as.simpleTokensMu.RUnlock()
|
||||
t, ok := as.simpleTokens[token]
|
||||
if ok {
|
||||
as.simpleTokenKeeper.resetSimpleToken(token)
|
||||
}
|
||||
return &AuthInfo{Username: t, Revision: as.revision}, ok
|
||||
func (as *authStore) authInfoFromToken(ctx context.Context, token string) (*AuthInfo, bool) {
|
||||
return as.tokenProvider.info(ctx, token, as.Revision())
|
||||
}
|
||||
|
||||
type permSlice []*authpb.Permission
|
||||
@ -711,7 +734,7 @@ func (as *authStore) isOpPermitted(userName string, revision uint64, key, rangeE
|
||||
return ErrUserEmpty
|
||||
}
|
||||
|
||||
if revision < as.revision {
|
||||
if revision < as.Revision() {
|
||||
return ErrAuthOldRevision
|
||||
}
|
||||
|
||||
@ -753,6 +776,9 @@ func (as *authStore) IsAdminPermitted(authInfo *AuthInfo) error {
|
||||
if !as.isAuthEnabled() {
|
||||
return nil
|
||||
}
|
||||
if authInfo == nil {
|
||||
return ErrUserEmpty
|
||||
}
|
||||
|
||||
tx := as.be.BatchTx()
|
||||
tx.Lock()
|
||||
@ -871,7 +897,7 @@ func (as *authStore) isAuthEnabled() bool {
|
||||
return as.enabled
|
||||
}
|
||||
|
||||
func NewAuthStore(be backend.Backend) *authStore {
|
||||
func NewAuthStore(be backend.Backend, tp TokenProvider) *authStore {
|
||||
tx := be.BatchTx()
|
||||
tx.Lock()
|
||||
|
||||
@ -879,13 +905,29 @@ func NewAuthStore(be backend.Backend) *authStore {
|
||||
tx.UnsafeCreateBucket(authUsersBucketName)
|
||||
tx.UnsafeCreateBucket(authRolesBucketName)
|
||||
|
||||
as := &authStore{
|
||||
be: be,
|
||||
simpleTokens: make(map[string]string),
|
||||
revision: 0,
|
||||
enabled := false
|
||||
_, vs := tx.UnsafeRange(authBucketName, enableFlagKey, nil, 0)
|
||||
if len(vs) == 1 {
|
||||
if bytes.Equal(vs[0], authEnabled) {
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
as.commitRevision(tx)
|
||||
as := &authStore{
|
||||
be: be,
|
||||
revision: getRevision(tx),
|
||||
enabled: enabled,
|
||||
rangePermCache: make(map[string]*unifiedRangePermissions),
|
||||
tokenProvider: tp,
|
||||
}
|
||||
|
||||
if enabled {
|
||||
as.tokenProvider.enable()
|
||||
}
|
||||
|
||||
if as.Revision() == 0 {
|
||||
as.commitRevision(tx)
|
||||
}
|
||||
|
||||
tx.Unlock()
|
||||
be.ForceCommit()
|
||||
@ -903,21 +945,115 @@ func hasRootRole(u *authpb.User) bool {
|
||||
}
|
||||
|
||||
func (as *authStore) commitRevision(tx backend.BatchTx) {
|
||||
as.revision++
|
||||
atomic.AddUint64(&as.revision, 1)
|
||||
revBytes := make([]byte, revBytesLen)
|
||||
binary.BigEndian.PutUint64(revBytes, as.revision)
|
||||
binary.BigEndian.PutUint64(revBytes, as.Revision())
|
||||
tx.UnsafePut(authBucketName, revisionKey, revBytes)
|
||||
}
|
||||
|
||||
func getRevision(tx backend.BatchTx) uint64 {
|
||||
_, vs := tx.UnsafeRange(authBucketName, []byte(revisionKey), nil, 0)
|
||||
if len(vs) != 1 {
|
||||
plog.Panicf("failed to get the key of auth store revision")
|
||||
// this can happen in the initialization phase
|
||||
return 0
|
||||
}
|
||||
|
||||
return binary.BigEndian.Uint64(vs[0])
|
||||
}
|
||||
|
||||
func (as *authStore) Revision() uint64 {
|
||||
return as.revision
|
||||
func (as *authStore) setRevision(rev uint64) {
|
||||
atomic.StoreUint64(&as.revision, rev)
|
||||
}
|
||||
|
||||
func (as *authStore) Revision() uint64 {
|
||||
return atomic.LoadUint64(&as.revision)
|
||||
}
|
||||
|
||||
func (as *authStore) AuthInfoFromTLS(ctx context.Context) *AuthInfo {
|
||||
peer, ok := peer.FromContext(ctx)
|
||||
if !ok || peer == nil || peer.AuthInfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
tlsInfo := peer.AuthInfo.(credentials.TLSInfo)
|
||||
for _, chains := range tlsInfo.State.VerifiedChains {
|
||||
for _, chain := range chains {
|
||||
cn := chain.Subject.CommonName
|
||||
plog.Debugf("found common name %s", cn)
|
||||
|
||||
return &AuthInfo{
|
||||
Username: cn,
|
||||
Revision: as.Revision(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (as *authStore) AuthInfoFromCtx(ctx context.Context) (*AuthInfo, error) {
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ts, tok := md["token"]
|
||||
if !tok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
token := ts[0]
|
||||
authInfo, uok := as.authInfoFromToken(ctx, token)
|
||||
if !uok {
|
||||
plog.Warningf("invalid auth token: %s", token)
|
||||
return nil, ErrInvalidAuthToken
|
||||
}
|
||||
return authInfo, nil
|
||||
}
|
||||
|
||||
func (as *authStore) GenTokenPrefix() (string, error) {
|
||||
return as.tokenProvider.genTokenPrefix()
|
||||
}
|
||||
|
||||
func decomposeOpts(optstr string) (string, map[string]string, error) {
|
||||
opts := strings.Split(optstr, ",")
|
||||
tokenType := opts[0]
|
||||
|
||||
typeSpecificOpts := make(map[string]string)
|
||||
for i := 1; i < len(opts); i++ {
|
||||
pair := strings.Split(opts[i], "=")
|
||||
|
||||
if len(pair) != 2 {
|
||||
plog.Errorf("invalid token specific option: %s", optstr)
|
||||
return "", nil, ErrInvalidAuthOpts
|
||||
}
|
||||
|
||||
if _, ok := typeSpecificOpts[pair[0]]; ok {
|
||||
plog.Errorf("invalid token specific option, duplicated parameters (%s): %s", pair[0], optstr)
|
||||
return "", nil, ErrInvalidAuthOpts
|
||||
}
|
||||
|
||||
typeSpecificOpts[pair[0]] = pair[1]
|
||||
}
|
||||
|
||||
return tokenType, typeSpecificOpts, nil
|
||||
|
||||
}
|
||||
|
||||
func NewTokenProvider(tokenOpts string, indexWaiter func(uint64) <-chan struct{}) (TokenProvider, error) {
|
||||
tokenType, typeSpecificOpts, err := decomposeOpts(tokenOpts)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidAuthOpts
|
||||
}
|
||||
|
||||
switch tokenType {
|
||||
case "simple":
|
||||
plog.Warningf("simple token is not cryptographically signed")
|
||||
return newTokenProviderSimple(indexWaiter), nil
|
||||
case "jwt":
|
||||
return newTokenProviderJWT(typeSpecificOpts)
|
||||
default:
|
||||
plog.Errorf("unknown token type: %s", tokenType)
|
||||
return nil, ErrInvalidAuthOpts
|
||||
}
|
||||
}
|
||||
|
@ -15,43 +15,94 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/auth/authpb"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/mvcc/backend"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func init() { BcryptCost = bcrypt.MinCost }
|
||||
|
||||
func TestUserAdd(t *testing.T) {
|
||||
b, tPath := backend.NewDefaultTmpBackend()
|
||||
defer func() {
|
||||
b.Close()
|
||||
os.Remove(tPath)
|
||||
func dummyIndexWaiter(index uint64) <-chan struct{} {
|
||||
ch := make(chan struct{})
|
||||
go func() {
|
||||
ch <- struct{}{}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
as := NewAuthStore(b)
|
||||
ua := &pb.AuthUserAddRequest{Name: "foo"}
|
||||
_, err := as.UserAdd(ua) // add a non-existing user
|
||||
// TestNewAuthStoreRevision ensures newly auth store
|
||||
// keeps the old revision when there are no changes.
|
||||
func TestNewAuthStoreRevision(t *testing.T) {
|
||||
b, tPath := backend.NewDefaultTmpBackend()
|
||||
defer os.Remove(tPath)
|
||||
|
||||
tp, err := NewTokenProvider("simple", dummyIndexWaiter)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = as.UserAdd(ua) // add an existing user
|
||||
if err == nil {
|
||||
t.Fatalf("expected %v, got %v", ErrUserAlreadyExist, err)
|
||||
}
|
||||
if err != ErrUserAlreadyExist {
|
||||
t.Fatalf("expected %v, got %v", ErrUserAlreadyExist, err)
|
||||
}
|
||||
|
||||
ua = &pb.AuthUserAddRequest{Name: ""}
|
||||
_, err = as.UserAdd(ua) // add a user with empty name
|
||||
if err != ErrUserEmpty {
|
||||
as := NewAuthStore(b, tp)
|
||||
err = enableAuthAndCreateRoot(as)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
old := as.Revision()
|
||||
b.Close()
|
||||
as.Close()
|
||||
|
||||
// no changes to commit
|
||||
b2 := backend.NewDefaultBackend(tPath)
|
||||
as = NewAuthStore(b2, tp)
|
||||
new := as.Revision()
|
||||
b2.Close()
|
||||
as.Close()
|
||||
|
||||
if old != new {
|
||||
t.Fatalf("expected revision %d, got %d", old, new)
|
||||
}
|
||||
}
|
||||
|
||||
func setupAuthStore(t *testing.T) (store *authStore, teardownfunc func(t *testing.T)) {
|
||||
b, tPath := backend.NewDefaultTmpBackend()
|
||||
|
||||
tp, err := NewTokenProvider("simple", dummyIndexWaiter)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
as := NewAuthStore(b, tp)
|
||||
err = enableAuthAndCreateRoot(as)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// adds a new role
|
||||
_, err = as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ua := &pb.AuthUserAddRequest{Name: "foo", Password: "bar"}
|
||||
_, err = as.UserAdd(ua) // add a non-existing user
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tearDown := func(t *testing.T) {
|
||||
b.Close()
|
||||
os.Remove(tPath)
|
||||
as.Close()
|
||||
}
|
||||
return as, tearDown
|
||||
}
|
||||
|
||||
func enableAuthAndCreateRoot(as *authStore) error {
|
||||
@ -73,28 +124,32 @@ func enableAuthAndCreateRoot(as *authStore) error {
|
||||
return as.AuthEnable()
|
||||
}
|
||||
|
||||
func TestUserAdd(t *testing.T) {
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
ua := &pb.AuthUserAddRequest{Name: "foo"}
|
||||
_, err := as.UserAdd(ua) // add an existing user
|
||||
if err == nil {
|
||||
t.Fatalf("expected %v, got %v", ErrUserAlreadyExist, err)
|
||||
}
|
||||
if err != ErrUserAlreadyExist {
|
||||
t.Fatalf("expected %v, got %v", ErrUserAlreadyExist, err)
|
||||
}
|
||||
|
||||
ua = &pb.AuthUserAddRequest{Name: ""}
|
||||
_, err = as.UserAdd(ua) // add a user with empty name
|
||||
if err != ErrUserEmpty {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPassword(t *testing.T) {
|
||||
b, tPath := backend.NewDefaultTmpBackend()
|
||||
defer func() {
|
||||
b.Close()
|
||||
os.Remove(tPath)
|
||||
}()
|
||||
|
||||
as := NewAuthStore(b)
|
||||
defer as.Close()
|
||||
err := enableAuthAndCreateRoot(as)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ua := &pb.AuthUserAddRequest{Name: "foo", Password: "bar"}
|
||||
_, err = as.UserAdd(ua)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
// auth a non-existing user
|
||||
_, err = as.CheckPassword("foo-test", "bar")
|
||||
_, err := as.CheckPassword("foo-test", "bar")
|
||||
if err == nil {
|
||||
t.Fatalf("expected %v, got %v", ErrAuthFailed, err)
|
||||
}
|
||||
@ -119,28 +174,12 @@ func TestCheckPassword(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUserDelete(t *testing.T) {
|
||||
b, tPath := backend.NewDefaultTmpBackend()
|
||||
defer func() {
|
||||
b.Close()
|
||||
os.Remove(tPath)
|
||||
}()
|
||||
|
||||
as := NewAuthStore(b)
|
||||
defer as.Close()
|
||||
err := enableAuthAndCreateRoot(as)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ua := &pb.AuthUserAddRequest{Name: "foo"}
|
||||
_, err = as.UserAdd(ua)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
// delete an existing user
|
||||
ud := &pb.AuthUserDeleteRequest{Name: "foo"}
|
||||
_, err = as.UserDelete(ud)
|
||||
_, err := as.UserDelete(ud)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -156,37 +195,22 @@ func TestUserDelete(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUserChangePassword(t *testing.T) {
|
||||
b, tPath := backend.NewDefaultTmpBackend()
|
||||
defer func() {
|
||||
b.Close()
|
||||
os.Remove(tPath)
|
||||
}()
|
||||
|
||||
as := NewAuthStore(b)
|
||||
defer as.Close()
|
||||
err := enableAuthAndCreateRoot(as)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = as.UserAdd(&pb.AuthUserAddRequest{Name: "foo"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
ctx1 := context.WithValue(context.WithValue(context.TODO(), "index", uint64(1)), "simpleToken", "dummy")
|
||||
_, err = as.Authenticate(ctx1, "foo", "")
|
||||
_, err := as.Authenticate(ctx1, "foo", "bar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = as.UserChangePassword(&pb.AuthUserChangePasswordRequest{Name: "foo", Password: "bar"})
|
||||
_, err = as.UserChangePassword(&pb.AuthUserChangePasswordRequest{Name: "foo", Password: "baz"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx2 := context.WithValue(context.WithValue(context.TODO(), "index", uint64(2)), "simpleToken", "dummy")
|
||||
_, err = as.Authenticate(ctx2, "foo", "bar")
|
||||
_, err = as.Authenticate(ctx2, "foo", "baz")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -202,53 +226,22 @@ func TestUserChangePassword(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRoleAdd(t *testing.T) {
|
||||
b, tPath := backend.NewDefaultTmpBackend()
|
||||
defer func() {
|
||||
b.Close()
|
||||
os.Remove(tPath)
|
||||
}()
|
||||
|
||||
as := NewAuthStore(b)
|
||||
defer as.Close()
|
||||
err := enableAuthAndCreateRoot(as)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
// adds a new role
|
||||
_, err = as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test"})
|
||||
_, err := as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test-1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserGrant(t *testing.T) {
|
||||
b, tPath := backend.NewDefaultTmpBackend()
|
||||
defer func() {
|
||||
b.Close()
|
||||
os.Remove(tPath)
|
||||
}()
|
||||
|
||||
as := NewAuthStore(b)
|
||||
defer as.Close()
|
||||
err := enableAuthAndCreateRoot(as)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = as.UserAdd(&pb.AuthUserAddRequest{Name: "foo"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// adds a new role
|
||||
_, err = as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
// grants a role to the user
|
||||
_, err = as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "foo", Role: "role-test"})
|
||||
_, err := as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "foo", Role: "role-test"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -256,9 +249,407 @@ func TestUserGrant(t *testing.T) {
|
||||
// grants a role to a non-existing user
|
||||
_, err = as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "foo-test", Role: "role-test"})
|
||||
if err == nil {
|
||||
t.Fatalf("expected %v, got %v", ErrUserNotFound, err)
|
||||
t.Errorf("expected %v, got %v", ErrUserNotFound, err)
|
||||
}
|
||||
if err != ErrUserNotFound {
|
||||
t.Fatalf("expected %v, got %v", ErrUserNotFound, err)
|
||||
t.Errorf("expected %v, got %v", ErrUserNotFound, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUser(t *testing.T) {
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
_, err := as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "foo", Role: "role-test"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
u, err := as.UserGet(&pb.AuthUserGetRequest{Name: "foo"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if u == nil {
|
||||
t.Fatal("expect user not nil, got nil")
|
||||
}
|
||||
expected := []string{"role-test"}
|
||||
if !reflect.DeepEqual(expected, u.Roles) {
|
||||
t.Errorf("expected %v, got %v", expected, u.Roles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUsers(t *testing.T) {
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
ua := &pb.AuthUserAddRequest{Name: "user1", Password: "pwd1"}
|
||||
_, err := as.UserAdd(ua) // add a non-existing user
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ul, err := as.UserList(&pb.AuthUserListRequest{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !contains(ul.Users, "root") {
|
||||
t.Errorf("expected %v in %v", "root", ul.Users)
|
||||
}
|
||||
if !contains(ul.Users, "user1") {
|
||||
t.Errorf("expected %v in %v", "user1", ul.Users)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleGrantPermission(t *testing.T) {
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
_, err := as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test-1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
perm := &authpb.Permission{
|
||||
PermType: authpb.WRITE,
|
||||
Key: []byte("Keys"),
|
||||
RangeEnd: []byte("RangeEnd"),
|
||||
}
|
||||
_, err = as.RoleGrantPermission(&pb.AuthRoleGrantPermissionRequest{
|
||||
Name: "role-test-1",
|
||||
Perm: perm,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
r, err := as.RoleGet(&pb.AuthRoleGetRequest{Role: "role-test-1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(perm, r.Perm[0]) {
|
||||
t.Errorf("expected %v, got %v", perm, r.Perm[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleRevokePermission(t *testing.T) {
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
_, err := as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test-1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
perm := &authpb.Permission{
|
||||
PermType: authpb.WRITE,
|
||||
Key: []byte("Keys"),
|
||||
RangeEnd: []byte("RangeEnd"),
|
||||
}
|
||||
_, err = as.RoleGrantPermission(&pb.AuthRoleGrantPermissionRequest{
|
||||
Name: "role-test-1",
|
||||
Perm: perm,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = as.RoleGet(&pb.AuthRoleGetRequest{Role: "role-test-1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = as.RoleRevokePermission(&pb.AuthRoleRevokePermissionRequest{
|
||||
Role: "role-test-1",
|
||||
Key: "Keys",
|
||||
RangeEnd: "RangeEnd",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var r *pb.AuthRoleGetResponse
|
||||
r, err = as.RoleGet(&pb.AuthRoleGetRequest{Role: "role-test-1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(r.Perm) != 0 {
|
||||
t.Errorf("expected %v, got %v", 0, len(r.Perm))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserRevokePermission(t *testing.T) {
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
_, err := as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test-1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "foo", Role: "role-test"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "foo", Role: "role-test-1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
u, err := as.UserGet(&pb.AuthUserGetRequest{Name: "foo"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := []string{"role-test", "role-test-1"}
|
||||
if !reflect.DeepEqual(expected, u.Roles) {
|
||||
t.Fatalf("expected %v, got %v", expected, u.Roles)
|
||||
}
|
||||
|
||||
_, err = as.UserRevokeRole(&pb.AuthUserRevokeRoleRequest{Name: "foo", Role: "role-test-1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
u, err = as.UserGet(&pb.AuthUserGetRequest{Name: "foo"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected = []string{"role-test"}
|
||||
if !reflect.DeepEqual(expected, u.Roles) {
|
||||
t.Errorf("expected %v, got %v", expected, u.Roles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleDelete(t *testing.T) {
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
_, err := as.RoleDelete(&pb.AuthRoleDeleteRequest{Role: "role-test"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rl, err := as.RoleList(&pb.AuthRoleListRequest{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := []string{"root"}
|
||||
if !reflect.DeepEqual(expected, rl.Roles) {
|
||||
t.Errorf("expected %v, got %v", expected, rl.Roles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthInfoFromCtx(t *testing.T) {
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
ctx := context.Background()
|
||||
ai, err := as.AuthInfoFromCtx(ctx)
|
||||
if err != nil && ai != nil {
|
||||
t.Errorf("expected (nil, nil), got (%v, %v)", ai, err)
|
||||
}
|
||||
|
||||
ctx = metadata.NewContext(context.Background(), metadata.New(map[string]string{"tokens": "dummy"}))
|
||||
ai, err = as.AuthInfoFromCtx(ctx)
|
||||
if err != nil && ai != nil {
|
||||
t.Errorf("expected (nil, nil), got (%v, %v)", ai, err)
|
||||
}
|
||||
|
||||
ctx = context.WithValue(context.WithValue(context.TODO(), "index", uint64(1)), "simpleToken", "dummy")
|
||||
resp, err := as.Authenticate(ctx, "foo", "bar")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ctx = metadata.NewContext(context.Background(), metadata.New(map[string]string{"token": "Invalid Token"}))
|
||||
_, err = as.AuthInfoFromCtx(ctx)
|
||||
if err != ErrInvalidAuthToken {
|
||||
t.Errorf("expected %v, got %v", ErrInvalidAuthToken, err)
|
||||
}
|
||||
|
||||
ctx = metadata.NewContext(context.Background(), metadata.New(map[string]string{"token": "Invalid.Token"}))
|
||||
_, err = as.AuthInfoFromCtx(ctx)
|
||||
if err != ErrInvalidAuthToken {
|
||||
t.Errorf("expected %v, got %v", ErrInvalidAuthToken, err)
|
||||
}
|
||||
|
||||
ctx = metadata.NewContext(context.Background(), metadata.New(map[string]string{"token": resp.Token}))
|
||||
ai, err = as.AuthInfoFromCtx(ctx)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if ai.Username != "foo" {
|
||||
t.Errorf("expected %v, got %v", "foo", ai.Username)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthDisable(t *testing.T) {
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
as.AuthDisable()
|
||||
ctx := context.WithValue(context.WithValue(context.TODO(), "index", uint64(2)), "simpleToken", "dummy")
|
||||
_, err := as.Authenticate(ctx, "foo", "bar")
|
||||
if err != ErrAuthNotEnabled {
|
||||
t.Errorf("expected %v, got %v", ErrAuthNotEnabled, err)
|
||||
}
|
||||
|
||||
// Disabling disabled auth to make sure it can return safely if store is already disabled.
|
||||
as.AuthDisable()
|
||||
_, err = as.Authenticate(ctx, "foo", "bar")
|
||||
if err != ErrAuthNotEnabled {
|
||||
t.Errorf("expected %v, got %v", ErrAuthNotEnabled, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAuthRevisionRace ensures that access to authStore.revision is thread-safe.
|
||||
func TestAuthInfoFromCtxRace(t *testing.T) {
|
||||
b, tPath := backend.NewDefaultTmpBackend()
|
||||
defer os.Remove(tPath)
|
||||
|
||||
tp, err := NewTokenProvider("simple", dummyIndexWaiter)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
as := NewAuthStore(b, tp)
|
||||
defer as.Close()
|
||||
|
||||
donec := make(chan struct{})
|
||||
go func() {
|
||||
defer close(donec)
|
||||
ctx := metadata.NewContext(context.Background(), metadata.New(map[string]string{"token": "test"}))
|
||||
as.AuthInfoFromCtx(ctx)
|
||||
}()
|
||||
as.UserAdd(&pb.AuthUserAddRequest{Name: "test"})
|
||||
<-donec
|
||||
}
|
||||
|
||||
func TestIsAdminPermitted(t *testing.T) {
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
err := as.IsAdminPermitted(&AuthInfo{Username: "root", Revision: 1})
|
||||
if err != nil {
|
||||
t.Errorf("expected nil, got %v", err)
|
||||
}
|
||||
|
||||
// invalid user
|
||||
err = as.IsAdminPermitted(&AuthInfo{Username: "rooti", Revision: 1})
|
||||
if err != ErrUserNotFound {
|
||||
t.Errorf("expected %v, got %v", ErrUserNotFound, err)
|
||||
}
|
||||
|
||||
// non-admin user
|
||||
err = as.IsAdminPermitted(&AuthInfo{Username: "foo", Revision: 1})
|
||||
if err != ErrPermissionDenied {
|
||||
t.Errorf("expected %v, got %v", ErrPermissionDenied, err)
|
||||
}
|
||||
|
||||
// disabled auth should return nil
|
||||
as.AuthDisable()
|
||||
err = as.IsAdminPermitted(&AuthInfo{Username: "root", Revision: 1})
|
||||
if err != nil {
|
||||
t.Errorf("expected nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoverFromSnapshot(t *testing.T) {
|
||||
as, _ := setupAuthStore(t)
|
||||
|
||||
ua := &pb.AuthUserAddRequest{Name: "foo"}
|
||||
_, err := as.UserAdd(ua) // add an existing user
|
||||
if err == nil {
|
||||
t.Fatalf("expected %v, got %v", ErrUserAlreadyExist, err)
|
||||
}
|
||||
if err != ErrUserAlreadyExist {
|
||||
t.Fatalf("expected %v, got %v", ErrUserAlreadyExist, err)
|
||||
}
|
||||
|
||||
ua = &pb.AuthUserAddRequest{Name: ""}
|
||||
_, err = as.UserAdd(ua) // add a user with empty name
|
||||
if err != ErrUserEmpty {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
as.Close()
|
||||
|
||||
tp, err := NewTokenProvider("simple", dummyIndexWaiter)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
as2 := NewAuthStore(as.be, tp)
|
||||
defer func(a *authStore) {
|
||||
a.Close()
|
||||
}(as2)
|
||||
|
||||
if !as2.isAuthEnabled() {
|
||||
t.Fatal("recovering authStore from existing backend failed")
|
||||
}
|
||||
|
||||
ul, err := as.UserList(&pb.AuthUserListRequest{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !contains(ul.Users, "root") {
|
||||
t.Errorf("expected %v in %v", "root", ul.Users)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(array []string, str string) bool {
|
||||
for _, s := range array {
|
||||
if s == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestHammerSimpleAuthenticate(t *testing.T) {
|
||||
// set TTL values low to try to trigger races
|
||||
oldTTL, oldTTLRes := simpleTokenTTL, simpleTokenTTLResolution
|
||||
defer func() {
|
||||
simpleTokenTTL = oldTTL
|
||||
simpleTokenTTLResolution = oldTTLRes
|
||||
}()
|
||||
simpleTokenTTL = 10 * time.Millisecond
|
||||
simpleTokenTTLResolution = simpleTokenTTL
|
||||
users := make(map[string]struct{})
|
||||
|
||||
as, tearDown := setupAuthStore(t)
|
||||
defer tearDown(t)
|
||||
|
||||
// create lots of users
|
||||
for i := 0; i < 50; i++ {
|
||||
u := fmt.Sprintf("user-%d", i)
|
||||
ua := &pb.AuthUserAddRequest{Name: u, Password: "123"}
|
||||
if _, err := as.UserAdd(ua); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
users[u] = struct{}{}
|
||||
}
|
||||
|
||||
// hammer on authenticate with lots of users
|
||||
for i := 0; i < 10; i++ {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(users))
|
||||
for u := range users {
|
||||
go func(user string) {
|
||||
defer wg.Done()
|
||||
token := fmt.Sprintf("%s(%d)", user, i)
|
||||
ctx := context.WithValue(context.WithValue(context.TODO(), "index", uint64(1)), "simpleToken", token)
|
||||
if _, err := as.Authenticate(ctx, user, "123"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := as.AuthInfoFromCtx(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}(u)
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
|
207
bill-of-materials.json
Normal file
207
bill-of-materials.json
Normal file
@ -0,0 +1,207 @@
|
||||
[
|
||||
{
|
||||
"project": "bitbucket.org/ww/goautoneg",
|
||||
"license": "BSD 3-clause \"New\" or \"Revised\" License",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/beorn7/perks/quantile",
|
||||
"license": "MIT License",
|
||||
"confidence": 0.989
|
||||
},
|
||||
{
|
||||
"project": "github.com/bgentry/speakeasy",
|
||||
"license": "MIT License",
|
||||
"confidence": 0.944
|
||||
},
|
||||
{
|
||||
"project": "github.com/boltdb/bolt",
|
||||
"license": "MIT License",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/cockroachdb/cmux",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/coreos/etcd",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/coreos/go-semver/semver",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/coreos/go-systemd",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 0.997
|
||||
},
|
||||
{
|
||||
"project": "github.com/coreos/pkg",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/cpuguy83/go-md2man/md2man",
|
||||
"license": "MIT License",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/dgrijalva/jwt-go",
|
||||
"license": "MIT License",
|
||||
"confidence": 0.989
|
||||
},
|
||||
{
|
||||
"project": "github.com/dustin/go-humanize",
|
||||
"license": "MIT License",
|
||||
"confidence": 0.969
|
||||
},
|
||||
{
|
||||
"project": "github.com/ghodss/yaml",
|
||||
"license": "MIT License and BSD 3-clause \"New\" or \"Revised\" License",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/gogo/protobuf/proto",
|
||||
"license": "BSD 3-clause \"New\" or \"Revised\" License",
|
||||
"confidence": 0.909
|
||||
},
|
||||
{
|
||||
"project": "github.com/golang/groupcache/lru",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 0.997
|
||||
},
|
||||
{
|
||||
"project": "github.com/golang/protobuf",
|
||||
"license": "BSD 3-clause \"New\" or \"Revised\" License",
|
||||
"confidence": 0.92
|
||||
},
|
||||
{
|
||||
"project": "github.com/google/btree",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/grpc-ecosystem/go-grpc-prometheus",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/grpc-ecosystem/grpc-gateway",
|
||||
"license": "BSD 3-clause \"New\" or \"Revised\" License",
|
||||
"confidence": 0.979
|
||||
},
|
||||
{
|
||||
"project": "github.com/inconshreveable/mousetrap",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/jonboulle/clockwork",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/mattn/go-runewidth",
|
||||
"license": "MIT License",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/matttproud/golang_protobuf_extensions/pbutil",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/olekukonko/tablewriter",
|
||||
"license": "MIT License",
|
||||
"confidence": 0.989
|
||||
},
|
||||
{
|
||||
"project": "github.com/prometheus/client_golang/prometheus",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/prometheus/client_model/go",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/prometheus/common",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/prometheus/procfs",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/russross/blackfriday",
|
||||
"license": "BSD 2-clause \"Simplified\" License",
|
||||
"confidence": 0.963
|
||||
},
|
||||
{
|
||||
"project": "github.com/spf13/cobra",
|
||||
"license": "Apache License 2.0",
|
||||
"confidence": 0.957
|
||||
},
|
||||
{
|
||||
"project": "github.com/spf13/pflag",
|
||||
"license": "BSD 3-clause \"New\" or \"Revised\" License",
|
||||
"confidence": 0.966
|
||||
},
|
||||
{
|
||||
"project": "github.com/ugorji/go/codec",
|
||||
"license": "MIT License",
|
||||
"confidence": 0.995
|
||||
},
|
||||
{
|
||||
"project": "github.com/urfave/cli",
|
||||
"license": "MIT License",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "github.com/xiang90/probing",
|
||||
"license": "MIT License",
|
||||
"confidence": 1
|
||||
},
|
||||
{
|
||||
"project": "golang.org/x/crypto",
|
||||
"license": "BSD 3-clause \"New\" or \"Revised\" License",
|
||||
"confidence": 0.966
|
||||
},
|
||||
{
|
||||
"project": "golang.org/x/net",
|
||||
"license": "BSD 3-clause \"New\" or \"Revised\" License",
|
||||
"confidence": 0.966
|
||||
},
|
||||
{
|
||||
"project": "golang.org/x/text",
|
||||
"license": "BSD 3-clause \"New\" or \"Revised\" License",
|
||||
"confidence": 0.966
|
||||
},
|
||||
{
|
||||
"project": "golang.org/x/time/rate",
|
||||
"license": "BSD 3-clause \"New\" or \"Revised\" License",
|
||||
"confidence": 0.966
|
||||
},
|
||||
{
|
||||
"project": "google.golang.org/grpc",
|
||||
"license": "BSD 3-clause \"New\" or \"Revised\" License",
|
||||
"confidence": 0.979
|
||||
},
|
||||
{
|
||||
"project": "gopkg.in/cheggaaa/pb.v1",
|
||||
"license": "BSD 3-clause \"New\" or \"Revised\" License",
|
||||
"confidence": 0.992
|
||||
},
|
||||
{
|
||||
"project": "gopkg.in/yaml.v2",
|
||||
"license": "Apache License 2.0 and MIT License",
|
||||
"confidence": 1
|
||||
}
|
||||
]
|
18
bill-of-materials.override.json
Normal file
18
bill-of-materials.override.json
Normal file
@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"project": "bitbucket.org/ww/goautoneg",
|
||||
"license": "BSD 3-clause \"New\" or \"Revised\" License"
|
||||
},
|
||||
{
|
||||
"project": "github.com/ghodss/yaml",
|
||||
"license": "MIT License and BSD 3-clause \"New\" or \"Revised\" License"
|
||||
},
|
||||
{
|
||||
"project": "github.com/inconshreveable/mousetrap",
|
||||
"license": "Apache License 2.0"
|
||||
},
|
||||
{
|
||||
"project": "gopkg.in/yaml.v2",
|
||||
"license": "Apache License 2.0 and MIT License"
|
||||
}
|
||||
]
|
23
build
23
build
@ -3,9 +3,7 @@
|
||||
# set some environment variables
|
||||
ORG_PATH="github.com/coreos"
|
||||
REPO_PATH="${ORG_PATH}/etcd"
|
||||
export GO15VENDOREXPERIMENT="1"
|
||||
|
||||
eval $(go env)
|
||||
GIT_SHA=`git rev-parse --short HEAD || echo "GitNotFound"`
|
||||
if [ ! -z "$FAILPOINTS" ]; then
|
||||
GIT_SHA="$GIT_SHA"-FAILPOINTS
|
||||
@ -17,11 +15,7 @@ GO_LDFLAGS="$GO_LDFLAGS -X ${REPO_PATH}/cmd/vendor/${REPO_PATH}/version.GitSHA=$
|
||||
# enable/disable failpoints
|
||||
toggle_failpoints() {
|
||||
FAILPKGS="etcdserver/ mvcc/backend/"
|
||||
|
||||
mode="disable"
|
||||
if [ ! -z "$FAILPOINTS" ]; then mode="enable"; fi
|
||||
if [ ! -z "$1" ]; then mode="$1"; fi
|
||||
|
||||
mode="$1"
|
||||
if which gofail >/dev/null 2>&1; then
|
||||
gofail "$mode" $FAILPKGS
|
||||
elif [ "$mode" != "disable" ]; then
|
||||
@ -30,19 +24,26 @@ toggle_failpoints() {
|
||||
fi
|
||||
}
|
||||
|
||||
toggle_failpoints_default() {
|
||||
mode="disable"
|
||||
if [ ! -z "$FAILPOINTS" ]; then mode="enable"; fi
|
||||
toggle_failpoints "$mode"
|
||||
}
|
||||
|
||||
etcd_build() {
|
||||
out="bin"
|
||||
if [ -n "${BINDIR}" ]; then out="${BINDIR}"; fi
|
||||
toggle_failpoints
|
||||
toggle_failpoints_default
|
||||
# Static compilation is useful when etcd is run in a container
|
||||
CGO_ENABLED=0 go build $GO_BUILD_FLAGS -installsuffix cgo -ldflags "$GO_LDFLAGS" -o ${out}/etcd ${REPO_PATH}/cmd/etcd || return
|
||||
CGO_ENABLED=0 go build $GO_BUILD_FLAGS -installsuffix cgo -ldflags "$GO_LDFLAGS" -o ${out}/etcdctl ${REPO_PATH}/cmd/etcdctl || return
|
||||
}
|
||||
|
||||
etcd_setup_gopath() {
|
||||
CDIR=$(cd `dirname "$0"` && pwd)
|
||||
d=$(dirname "$0")
|
||||
CDIR=$(cd "$d" && pwd)
|
||||
cd "$CDIR"
|
||||
etcdGOPATH=${CDIR}/gopath
|
||||
etcdGOPATH="${CDIR}/gopath"
|
||||
# preserve old gopath to support building with unvendored tooling deps (e.g., gofail)
|
||||
if [ -n "$GOPATH" ]; then
|
||||
GOPATH=":$GOPATH"
|
||||
@ -53,7 +54,7 @@ etcd_setup_gopath() {
|
||||
ln -s ${CDIR}/cmd/vendor ${etcdGOPATH}/src
|
||||
}
|
||||
|
||||
toggle_failpoints
|
||||
toggle_failpoints_default
|
||||
|
||||
# only build when called directly, not sourced
|
||||
if echo "$0" | grep "build$" >/dev/null; then
|
||||
|
@ -15,6 +15,7 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@ -27,6 +28,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/version"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
@ -201,6 +204,9 @@ type Client interface {
|
||||
// returned
|
||||
SetEndpoints(eps []string) error
|
||||
|
||||
// GetVersion retrieves the current etcd server and cluster version
|
||||
GetVersion(ctx context.Context) (*version.Versions, error)
|
||||
|
||||
httpClient
|
||||
}
|
||||
|
||||
@ -477,6 +483,33 @@ func (c *httpClusterClient) AutoSync(ctx context.Context, interval time.Duration
|
||||
}
|
||||
}
|
||||
|
||||
func (c *httpClusterClient) GetVersion(ctx context.Context) (*version.Versions, error) {
|
||||
act := &getAction{Prefix: "/version"}
|
||||
|
||||
resp, body, err := c.Do(ctx, act)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
if len(body) == 0 {
|
||||
return nil, ErrEmptyBody
|
||||
}
|
||||
var vresp version.Versions
|
||||
if err := json.Unmarshal(body, &vresp); err != nil {
|
||||
return nil, ErrInvalidJSON
|
||||
}
|
||||
return &vresp, nil
|
||||
default:
|
||||
var etcdErr Error
|
||||
if err := json.Unmarshal(body, &etcdErr); err != nil {
|
||||
return nil, ErrInvalidJSON
|
||||
}
|
||||
return nil, etcdErr
|
||||
}
|
||||
}
|
||||
|
||||
type roundTripResponse struct {
|
||||
resp *http.Response
|
||||
err error
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
"github.com/coreos/etcd/version"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
@ -860,6 +861,34 @@ func TestHTTPClusterClientAutoSyncFail(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClusterClientGetVersion(t *testing.T) {
|
||||
body := []byte(`{"etcdserver":"2.3.2","etcdcluster":"2.3.0"}`)
|
||||
cf := newStaticHTTPClientFactory([]staticHTTPResponse{
|
||||
{
|
||||
resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Length": []string{"44"}}},
|
||||
body: body,
|
||||
},
|
||||
})
|
||||
|
||||
hc := &httpClusterClient{
|
||||
clientFactory: cf,
|
||||
rand: rand.New(rand.NewSource(0)),
|
||||
}
|
||||
err := hc.SetEndpoints([]string{"http://127.0.0.1:4003", "http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error during setup: %#v", err)
|
||||
}
|
||||
|
||||
actual, err := hc.GetVersion(context.Background())
|
||||
if err != nil {
|
||||
t.Errorf("non-nil error: %#v", err)
|
||||
}
|
||||
expected := version.Versions{Server: "2.3.2", Cluster: "2.3.0"}
|
||||
if !reflect.DeepEqual(&expected, actual) {
|
||||
t.Errorf("incorrect Response: want=%#v got=%#v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHTTPClusterClientSyncPinEndpoint tests that Sync() pins the endpoint when
|
||||
// it gets the exactly same member list as before.
|
||||
func TestHTTPClusterClientSyncPinEndpoint(t *testing.T) {
|
||||
|
@ -14,8 +14,27 @@
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/pkg/srv"
|
||||
)
|
||||
|
||||
// Discoverer is an interface that wraps the Discover method.
|
||||
type Discoverer interface {
|
||||
// Discover looks up the etcd servers for the domain.
|
||||
Discover(domain string) ([]string, error)
|
||||
}
|
||||
|
||||
type srvDiscover struct{}
|
||||
|
||||
// NewSRVDiscover constructs a new Discoverer that uses the stdlib to lookup SRV records.
|
||||
func NewSRVDiscover() Discoverer {
|
||||
return &srvDiscover{}
|
||||
}
|
||||
|
||||
func (d *srvDiscover) Discover(domain string) ([]string, error) {
|
||||
srvs, err := srv.GetClient("etcd-client", domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return srvs.Endpoints, nil
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ import (
|
||||
func TestV2NoRetryEOF(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
// generate an EOF response; specify address so appears first in sorted ep list
|
||||
lEOF := integration.NewListenerWithAddr(t, fmt.Sprintf("eof:123.%d.sock", os.Getpid()))
|
||||
lEOF := integration.NewListenerWithAddr(t, fmt.Sprintf("127.0.0.1:%05d", os.Getpid()))
|
||||
defer lEOF.Close()
|
||||
tries := uint32(0)
|
||||
go func() {
|
||||
@ -65,8 +65,7 @@ func TestV2NoRetryEOF(t *testing.T) {
|
||||
// TestV2NoRetryNoLeader tests destructive api calls won't retry if given an error code.
|
||||
func TestV2NoRetryNoLeader(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
lHttp := integration.NewListenerWithAddr(t, fmt.Sprintf("errHttp:123.%d.sock", os.Getpid()))
|
||||
lHttp := integration.NewListenerWithAddr(t, fmt.Sprintf("127.0.0.1:%05d", os.Getpid()))
|
||||
eh := &errHandler{errCode: http.StatusServiceUnavailable}
|
||||
srv := httptest.NewUnstartedServer(eh)
|
||||
defer lHttp.Close()
|
||||
|
@ -1,65 +0,0 @@
|
||||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var (
|
||||
// indirection for testing
|
||||
lookupSRV = net.LookupSRV
|
||||
)
|
||||
|
||||
type srvDiscover struct{}
|
||||
|
||||
// NewSRVDiscover constructs a new Discoverer that uses the stdlib to lookup SRV records.
|
||||
func NewSRVDiscover() Discoverer {
|
||||
return &srvDiscover{}
|
||||
}
|
||||
|
||||
// Discover looks up the etcd servers for the domain.
|
||||
func (d *srvDiscover) Discover(domain string) ([]string, error) {
|
||||
var urls []*url.URL
|
||||
|
||||
updateURLs := func(service, scheme string) error {
|
||||
_, addrs, err := lookupSRV(service, "tcp", domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, srv := range addrs {
|
||||
urls = append(urls, &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: net.JoinHostPort(srv.Target, fmt.Sprintf("%d", srv.Port)),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
errHTTPS := updateURLs("etcd-client-ssl", "https")
|
||||
errHTTP := updateURLs("etcd-client", "http")
|
||||
|
||||
if errHTTPS != nil && errHTTP != nil {
|
||||
return nil, fmt.Errorf("dns lookup errors: %s and %s", errHTTPS, errHTTP)
|
||||
}
|
||||
|
||||
endpoints := make([]string, len(urls))
|
||||
for i := range urls {
|
||||
endpoints[i] = urls[i].String()
|
||||
}
|
||||
return endpoints, nil
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSRVDiscover(t *testing.T) {
|
||||
defer func() { lookupSRV = net.LookupSRV }()
|
||||
|
||||
tests := []struct {
|
||||
withSSL []*net.SRV
|
||||
withoutSSL []*net.SRV
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
[]*net.SRV{},
|
||||
[]*net.SRV{},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
[]*net.SRV{
|
||||
{Target: "10.0.0.1", Port: 2480},
|
||||
{Target: "10.0.0.2", Port: 2480},
|
||||
{Target: "10.0.0.3", Port: 2480},
|
||||
},
|
||||
[]*net.SRV{},
|
||||
[]string{"https://10.0.0.1:2480", "https://10.0.0.2:2480", "https://10.0.0.3:2480"},
|
||||
},
|
||||
{
|
||||
[]*net.SRV{
|
||||
{Target: "10.0.0.1", Port: 2480},
|
||||
{Target: "10.0.0.2", Port: 2480},
|
||||
{Target: "10.0.0.3", Port: 2480},
|
||||
},
|
||||
[]*net.SRV{
|
||||
{Target: "10.0.0.1", Port: 7001},
|
||||
},
|
||||
[]string{"https://10.0.0.1:2480", "https://10.0.0.2:2480", "https://10.0.0.3:2480", "http://10.0.0.1:7001"},
|
||||
},
|
||||
{
|
||||
[]*net.SRV{
|
||||
{Target: "10.0.0.1", Port: 2480},
|
||||
{Target: "10.0.0.2", Port: 2480},
|
||||
{Target: "10.0.0.3", Port: 2480},
|
||||
},
|
||||
[]*net.SRV{
|
||||
{Target: "10.0.0.1", Port: 7001},
|
||||
},
|
||||
[]string{"https://10.0.0.1:2480", "https://10.0.0.2:2480", "https://10.0.0.3:2480", "http://10.0.0.1:7001"},
|
||||
},
|
||||
{
|
||||
[]*net.SRV{
|
||||
{Target: "a.example.com", Port: 2480},
|
||||
{Target: "b.example.com", Port: 2480},
|
||||
{Target: "c.example.com", Port: 2480},
|
||||
},
|
||||
[]*net.SRV{},
|
||||
[]string{"https://a.example.com:2480", "https://b.example.com:2480", "https://c.example.com:2480"},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
lookupSRV = func(service string, proto string, domain string) (string, []*net.SRV, error) {
|
||||
if service == "etcd-client-ssl" {
|
||||
return "", tt.withSSL, nil
|
||||
}
|
||||
if service == "etcd-client" {
|
||||
return "", tt.withoutSSL, nil
|
||||
}
|
||||
return "", nil, errors.New("Unknown service in mock")
|
||||
}
|
||||
|
||||
d := NewSRVDiscover()
|
||||
|
||||
endpoints, err := d.Discover("example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("%d: err: %#v", i, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(endpoints, tt.expected) {
|
||||
t.Errorf("#%d: endpoints = %v, want %v", i, endpoints, tt.expected)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ pass `context.WithTimeout` to APIs:
|
||||
|
||||
```go
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
resp, err := kvc.Put(ctx, "sample_key", "sample_value")
|
||||
resp, err := cli.Put(ctx, "sample_key", "sample_value")
|
||||
cancel()
|
||||
if err != nil {
|
||||
// handle error!
|
||||
@ -57,7 +57,7 @@ etcd client returns 2 types of errors:
|
||||
Here is the example code to handle client errors:
|
||||
|
||||
```go
|
||||
resp, err := kvc.Put(ctx, "", "")
|
||||
resp, err := cli.Put(ctx, "", "")
|
||||
if err != nil {
|
||||
switch err {
|
||||
case context.Canceled:
|
||||
@ -76,6 +76,10 @@ if err != nil {
|
||||
|
||||
The etcd client optionally exposes RPC metrics through [go-grpc-prometheus](https://github.com/grpc-ecosystem/go-grpc-prometheus). See the [examples](https://github.com/coreos/etcd/blob/master/clientv3/example_metrics_test.go).
|
||||
|
||||
## Namespacing
|
||||
|
||||
The [namespace](https://godoc.org/github.com/coreos/etcd/clientv3/namespace) package provides `clientv3` interface wrappers to transparently isolate client requests to a user-defined prefix.
|
||||
|
||||
## Examples
|
||||
|
||||
More code examples can be found at [GoDoc](https://godoc.org/github.com/coreos/etcd/clientv3).
|
||||
|
@ -100,19 +100,11 @@ type Auth interface {
|
||||
}
|
||||
|
||||
type auth struct {
|
||||
c *Client
|
||||
|
||||
conn *grpc.ClientConn // conn in-use
|
||||
remote pb.AuthClient
|
||||
}
|
||||
|
||||
func NewAuth(c *Client) Auth {
|
||||
conn := c.ActiveConnection()
|
||||
return &auth{
|
||||
conn: c.ActiveConnection(),
|
||||
remote: pb.NewAuthClient(conn),
|
||||
c: c,
|
||||
}
|
||||
return &auth{remote: pb.NewAuthClient(c.ActiveConnection())}
|
||||
}
|
||||
|
||||
func (auth *auth) AuthEnable(ctx context.Context) (*AuthEnableResponse, error) {
|
||||
|
@ -43,11 +43,22 @@ type simpleBalancer struct {
|
||||
|
||||
// mu protects upEps, pinAddr, and connectingAddr
|
||||
mu sync.RWMutex
|
||||
// upEps holds the current endpoints that have an active connection
|
||||
upEps map[string]struct{}
|
||||
|
||||
// upc closes when upEps transitions from empty to non-zero or the balancer closes.
|
||||
upc chan struct{}
|
||||
|
||||
// downc closes when grpc calls down() on pinAddr
|
||||
downc chan struct{}
|
||||
|
||||
// stopc is closed to signal updateNotifyLoop should stop.
|
||||
stopc chan struct{}
|
||||
|
||||
// donec closes when all goroutines are exited
|
||||
donec chan struct{}
|
||||
|
||||
// updateAddrsC notifies updateNotifyLoop to update addrs.
|
||||
updateAddrsC chan struct{}
|
||||
|
||||
// grpc issues TLS cert checks using the string passed into dial so
|
||||
// that string must be the host. To recover the full scheme://host URL,
|
||||
// have a map from hosts to the original endpoint.
|
||||
@ -66,15 +77,19 @@ func newSimpleBalancer(eps []string) *simpleBalancer {
|
||||
for i := range eps {
|
||||
addrs[i].Addr = getHost(eps[i])
|
||||
}
|
||||
notifyCh <- addrs
|
||||
sb := &simpleBalancer{
|
||||
addrs: addrs,
|
||||
notifyCh: notifyCh,
|
||||
readyc: make(chan struct{}),
|
||||
upEps: make(map[string]struct{}),
|
||||
upc: make(chan struct{}),
|
||||
host2ep: getHost2ep(eps),
|
||||
addrs: addrs,
|
||||
notifyCh: notifyCh,
|
||||
readyc: make(chan struct{}),
|
||||
upc: make(chan struct{}),
|
||||
stopc: make(chan struct{}),
|
||||
downc: make(chan struct{}),
|
||||
donec: make(chan struct{}),
|
||||
updateAddrsC: make(chan struct{}, 1),
|
||||
host2ep: getHost2ep(eps),
|
||||
}
|
||||
close(sb.downc)
|
||||
go sb.updateNotifyLoop()
|
||||
return sb
|
||||
}
|
||||
|
||||
@ -105,7 +120,6 @@ func (b *simpleBalancer) updateAddrs(eps []string) {
|
||||
np := getHost2ep(eps)
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
match := len(np) == len(b.host2ep)
|
||||
for k, v := range np {
|
||||
@ -116,6 +130,7 @@ func (b *simpleBalancer) updateAddrs(eps []string) {
|
||||
}
|
||||
if match {
|
||||
// same endpoints, so no need to update address
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
@ -126,7 +141,92 @@ func (b *simpleBalancer) updateAddrs(eps []string) {
|
||||
addrs = append(addrs, grpc.Address{Addr: getHost(eps[i])})
|
||||
}
|
||||
b.addrs = addrs
|
||||
b.notifyCh <- addrs
|
||||
|
||||
// updating notifyCh can trigger new connections,
|
||||
// only update addrs if all connections are down
|
||||
// or addrs does not include pinAddr.
|
||||
update := !hasAddr(addrs, b.pinAddr)
|
||||
b.mu.Unlock()
|
||||
|
||||
if update {
|
||||
select {
|
||||
case b.updateAddrsC <- struct{}{}:
|
||||
case <-b.stopc:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasAddr(addrs []grpc.Address, targetAddr string) bool {
|
||||
for _, addr := range addrs {
|
||||
if targetAddr == addr.Addr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *simpleBalancer) updateNotifyLoop() {
|
||||
defer close(b.donec)
|
||||
|
||||
for {
|
||||
b.mu.RLock()
|
||||
upc, downc, addr := b.upc, b.downc, b.pinAddr
|
||||
b.mu.RUnlock()
|
||||
// downc or upc should be closed
|
||||
select {
|
||||
case <-downc:
|
||||
downc = nil
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case <-upc:
|
||||
upc = nil
|
||||
default:
|
||||
}
|
||||
switch {
|
||||
case downc == nil && upc == nil:
|
||||
// stale
|
||||
select {
|
||||
case <-b.stopc:
|
||||
return
|
||||
default:
|
||||
}
|
||||
case downc == nil:
|
||||
b.notifyAddrs()
|
||||
select {
|
||||
case <-upc:
|
||||
case <-b.updateAddrsC:
|
||||
b.notifyAddrs()
|
||||
case <-b.stopc:
|
||||
return
|
||||
}
|
||||
case upc == nil:
|
||||
select {
|
||||
// close connections that are not the pinned address
|
||||
case b.notifyCh <- []grpc.Address{{Addr: addr}}:
|
||||
case <-downc:
|
||||
case <-b.stopc:
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-downc:
|
||||
case <-b.updateAddrsC:
|
||||
case <-b.stopc:
|
||||
return
|
||||
}
|
||||
b.notifyAddrs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *simpleBalancer) notifyAddrs() {
|
||||
b.mu.RLock()
|
||||
addrs := b.addrs
|
||||
b.mu.RUnlock()
|
||||
select {
|
||||
case b.notifyCh <- addrs:
|
||||
case <-b.stopc:
|
||||
}
|
||||
}
|
||||
|
||||
func (b *simpleBalancer) Up(addr grpc.Address) func(error) {
|
||||
@ -139,49 +239,46 @@ func (b *simpleBalancer) Up(addr grpc.Address) func(error) {
|
||||
if b.closed {
|
||||
return func(err error) {}
|
||||
}
|
||||
|
||||
if len(b.upEps) == 0 {
|
||||
// notify waiting Get()s and pin first connected address
|
||||
close(b.upc)
|
||||
b.pinAddr = addr.Addr
|
||||
// gRPC might call Up on a stale address.
|
||||
// Prevent updating pinAddr with a stale address.
|
||||
if !hasAddr(b.addrs, addr.Addr) {
|
||||
return func(err error) {}
|
||||
}
|
||||
b.upEps[addr.Addr] = struct{}{}
|
||||
|
||||
if b.pinAddr != "" {
|
||||
return func(err error) {}
|
||||
}
|
||||
// notify waiting Get()s and pin first connected address
|
||||
close(b.upc)
|
||||
b.downc = make(chan struct{})
|
||||
b.pinAddr = addr.Addr
|
||||
// notify client that a connection is up
|
||||
b.readyOnce.Do(func() { close(b.readyc) })
|
||||
|
||||
return func(err error) {
|
||||
b.mu.Lock()
|
||||
delete(b.upEps, addr.Addr)
|
||||
if len(b.upEps) == 0 && b.pinAddr != "" {
|
||||
b.upc = make(chan struct{})
|
||||
} else if b.pinAddr == addr.Addr {
|
||||
// choose new random up endpoint
|
||||
for k := range b.upEps {
|
||||
b.pinAddr = k
|
||||
break
|
||||
}
|
||||
}
|
||||
b.upc = make(chan struct{})
|
||||
close(b.downc)
|
||||
b.pinAddr = ""
|
||||
b.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *simpleBalancer) Get(ctx context.Context, opts grpc.BalancerGetOptions) (grpc.Address, func(), error) {
|
||||
var addr string
|
||||
var (
|
||||
addr string
|
||||
closed bool
|
||||
)
|
||||
|
||||
// If opts.BlockingWait is false (for fail-fast RPCs), it should return
|
||||
// an address it has notified via Notify immediately instead of blocking.
|
||||
if !opts.BlockingWait {
|
||||
b.mu.RLock()
|
||||
closed := b.closed
|
||||
closed = b.closed
|
||||
addr = b.pinAddr
|
||||
upEps := len(b.upEps)
|
||||
b.mu.RUnlock()
|
||||
if closed {
|
||||
return grpc.Address{Addr: ""}, nil, grpc.ErrClientConnClosing
|
||||
}
|
||||
|
||||
if upEps == 0 {
|
||||
if addr == "" {
|
||||
return grpc.Address{Addr: ""}, nil, ErrNoAddrAvilable
|
||||
}
|
||||
return grpc.Address{Addr: addr}, func() {}, nil
|
||||
@ -193,17 +290,20 @@ func (b *simpleBalancer) Get(ctx context.Context, opts grpc.BalancerGetOptions)
|
||||
b.mu.RUnlock()
|
||||
select {
|
||||
case <-ch:
|
||||
case <-b.donec:
|
||||
return grpc.Address{Addr: ""}, nil, grpc.ErrClientConnClosing
|
||||
case <-ctx.Done():
|
||||
return grpc.Address{Addr: ""}, nil, ctx.Err()
|
||||
}
|
||||
b.mu.RLock()
|
||||
closed = b.closed
|
||||
addr = b.pinAddr
|
||||
upEps := len(b.upEps)
|
||||
b.mu.RUnlock()
|
||||
if addr == "" {
|
||||
// Close() which sets b.closed = true can be called before Get(), Get() must exit if balancer is closed.
|
||||
if closed {
|
||||
return grpc.Address{Addr: ""}, nil, grpc.ErrClientConnClosing
|
||||
}
|
||||
if upEps > 0 {
|
||||
if addr != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -214,19 +314,36 @@ func (b *simpleBalancer) Notify() <-chan []grpc.Address { return b.notifyCh }
|
||||
|
||||
func (b *simpleBalancer) Close() error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
// In case gRPC calls close twice. TODO: remove the checking
|
||||
// when we are sure that gRPC wont call close twice.
|
||||
if b.closed {
|
||||
b.mu.Unlock()
|
||||
<-b.donec
|
||||
return nil
|
||||
}
|
||||
b.closed = true
|
||||
close(b.notifyCh)
|
||||
// terminate all waiting Get()s
|
||||
close(b.stopc)
|
||||
b.pinAddr = ""
|
||||
if len(b.upEps) == 0 {
|
||||
|
||||
// In the case of following scenario:
|
||||
// 1. upc is not closed; no pinned address
|
||||
// 2. client issues an rpc, calling invoke(), which calls Get(), enters for loop, blocks
|
||||
// 3. clientconn.Close() calls balancer.Close(); closed = true
|
||||
// 4. for loop in Get() never exits since ctx is the context passed in by the client and may not be canceled
|
||||
// we must close upc so Get() exits from blocking on upc
|
||||
select {
|
||||
case <-b.upc:
|
||||
default:
|
||||
// terminate all waiting Get()s
|
||||
close(b.upc)
|
||||
}
|
||||
|
||||
b.mu.Unlock()
|
||||
|
||||
// wait for updateNotifyLoop to finish
|
||||
<-b.donec
|
||||
close(b.notifyCh)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -16,9 +16,14 @@ package clientv3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
@ -29,6 +34,10 @@ var (
|
||||
|
||||
func TestBalancerGetUnblocking(t *testing.T) {
|
||||
sb := newSimpleBalancer(endpoints)
|
||||
defer sb.Close()
|
||||
if addrs := <-sb.Notify(); len(addrs) != len(endpoints) {
|
||||
t.Errorf("Initialize newSimpleBalancer should have triggered Notify() chan, but it didn't")
|
||||
}
|
||||
unblockingOpts := grpc.BalancerGetOptions{BlockingWait: false}
|
||||
|
||||
_, _, err := sb.Get(context.Background(), unblockingOpts)
|
||||
@ -37,6 +46,9 @@ func TestBalancerGetUnblocking(t *testing.T) {
|
||||
}
|
||||
|
||||
down1 := sb.Up(grpc.Address{Addr: endpoints[1]})
|
||||
if addrs := <-sb.Notify(); len(addrs) != 1 {
|
||||
t.Errorf("first Up() should have triggered balancer to send the first connected address via Notify chan so that other connections can be closed")
|
||||
}
|
||||
down2 := sb.Up(grpc.Address{Addr: endpoints[2]})
|
||||
addrFirst, putFun, err := sb.Get(context.Background(), unblockingOpts)
|
||||
if err != nil {
|
||||
@ -54,6 +66,9 @@ func TestBalancerGetUnblocking(t *testing.T) {
|
||||
}
|
||||
|
||||
down1(errors.New("error"))
|
||||
if addrs := <-sb.Notify(); len(addrs) != len(endpoints) {
|
||||
t.Errorf("closing the only connection should triggered balancer to send the all endpoints via Notify chan so that we can establish a connection")
|
||||
}
|
||||
down2(errors.New("error"))
|
||||
_, _, err = sb.Get(context.Background(), unblockingOpts)
|
||||
if err != ErrNoAddrAvilable {
|
||||
@ -63,6 +78,10 @@ func TestBalancerGetUnblocking(t *testing.T) {
|
||||
|
||||
func TestBalancerGetBlocking(t *testing.T) {
|
||||
sb := newSimpleBalancer(endpoints)
|
||||
defer sb.Close()
|
||||
if addrs := <-sb.Notify(); len(addrs) != len(endpoints) {
|
||||
t.Errorf("Initialize newSimpleBalancer should have triggered Notify() chan, but it didn't")
|
||||
}
|
||||
blockingOpts := grpc.BalancerGetOptions{BlockingWait: true}
|
||||
|
||||
ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*100)
|
||||
@ -76,7 +95,11 @@ func TestBalancerGetBlocking(t *testing.T) {
|
||||
go func() {
|
||||
// ensure sb.Up() will be called after sb.Get() to see if Up() releases blocking Get()
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
downC <- sb.Up(grpc.Address{Addr: endpoints[1]})
|
||||
f := sb.Up(grpc.Address{Addr: endpoints[1]})
|
||||
if addrs := <-sb.Notify(); len(addrs) != 1 {
|
||||
t.Errorf("first Up() should have triggered balancer to send the first connected address via Notify chan so that other connections can be closed")
|
||||
}
|
||||
downC <- f
|
||||
}()
|
||||
addrFirst, putFun, err := sb.Get(context.Background(), blockingOpts)
|
||||
if err != nil {
|
||||
@ -97,6 +120,9 @@ func TestBalancerGetBlocking(t *testing.T) {
|
||||
}
|
||||
|
||||
down1(errors.New("error"))
|
||||
if addrs := <-sb.Notify(); len(addrs) != len(endpoints) {
|
||||
t.Errorf("closing the only connection should triggered balancer to send the all endpoints via Notify chan so that we can establish a connection")
|
||||
}
|
||||
down2(errors.New("error"))
|
||||
ctx, _ = context.WithTimeout(context.Background(), time.Millisecond*100)
|
||||
_, _, err = sb.Get(ctx, blockingOpts)
|
||||
@ -104,3 +130,110 @@ func TestBalancerGetBlocking(t *testing.T) {
|
||||
t.Errorf("Get() with no up endpoints should timeout, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBalancerDoNotBlockOnClose ensures that balancer and grpc don't deadlock each other
|
||||
// due to rapid open/close conn. The deadlock causes balancer.Close() to block forever.
|
||||
// See issue: https://github.com/coreos/etcd/issues/7283 for more detail.
|
||||
func TestBalancerDoNotBlockOnClose(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
kcl := newKillConnListener(t, 3)
|
||||
defer kcl.close()
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
sb := newSimpleBalancer(kcl.endpoints())
|
||||
conn, err := grpc.Dial("", grpc.WithInsecure(), grpc.WithBalancer(sb))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
kvc := pb.NewKVClient(conn)
|
||||
<-sb.readyc
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(100)
|
||||
cctx, cancel := context.WithCancel(context.TODO())
|
||||
for j := 0; j < 100; j++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
kvc.Range(cctx, &pb.RangeRequest{}, grpc.FailFast(false))
|
||||
}()
|
||||
}
|
||||
// balancer.Close() might block
|
||||
// if balancer and grpc deadlock each other.
|
||||
bclosec, cclosec := make(chan struct{}), make(chan struct{})
|
||||
go func() {
|
||||
defer close(bclosec)
|
||||
sb.Close()
|
||||
}()
|
||||
go func() {
|
||||
defer close(cclosec)
|
||||
conn.Close()
|
||||
}()
|
||||
select {
|
||||
case <-bclosec:
|
||||
case <-time.After(3 * time.Second):
|
||||
testutil.FatalStack(t, "balancer close timeout")
|
||||
}
|
||||
select {
|
||||
case <-cclosec:
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("grpc conn close timeout")
|
||||
}
|
||||
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
// killConnListener listens incoming conn and kills it immediately.
|
||||
type killConnListener struct {
|
||||
wg sync.WaitGroup
|
||||
eps []string
|
||||
stopc chan struct{}
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func newKillConnListener(t *testing.T, size int) *killConnListener {
|
||||
kcl := &killConnListener{stopc: make(chan struct{}), t: t}
|
||||
|
||||
for i := 0; i < size; i++ {
|
||||
ln, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
kcl.eps = append(kcl.eps, ln.Addr().String())
|
||||
kcl.wg.Add(1)
|
||||
go kcl.listen(ln)
|
||||
}
|
||||
return kcl
|
||||
}
|
||||
|
||||
func (kcl *killConnListener) endpoints() []string {
|
||||
return kcl.eps
|
||||
}
|
||||
|
||||
func (kcl *killConnListener) listen(l net.Listener) {
|
||||
go func() {
|
||||
defer kcl.wg.Done()
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
select {
|
||||
case <-kcl.stopc:
|
||||
return
|
||||
default:
|
||||
}
|
||||
if err != nil {
|
||||
kcl.t.Fatal(err)
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
<-kcl.stopc
|
||||
l.Close()
|
||||
}
|
||||
|
||||
func (kcl *killConnListener) close() {
|
||||
close(kcl.stopc)
|
||||
kcl.wg.Wait()
|
||||
}
|
||||
|
@ -20,12 +20,12 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
@ -36,6 +36,7 @@ import (
|
||||
|
||||
var (
|
||||
ErrNoAvailableEndpoints = errors.New("etcdclient: no available endpoints")
|
||||
ErrOldCluster = errors.New("etcdclient: old cluster version")
|
||||
)
|
||||
|
||||
// Client provides and manages an etcd v3 client session.
|
||||
@ -47,7 +48,9 @@ type Client struct {
|
||||
Auth
|
||||
Maintenance
|
||||
|
||||
conn *grpc.ClientConn
|
||||
conn *grpc.ClientConn
|
||||
dialerrc chan error
|
||||
|
||||
cfg Config
|
||||
creds *credentials.TransportCredentials
|
||||
balancer *simpleBalancer
|
||||
@ -74,26 +77,28 @@ func New(cfg Config) (*Client, error) {
|
||||
return newClient(&cfg)
|
||||
}
|
||||
|
||||
// NewCtxClient creates a client with a context but no underlying grpc
|
||||
// connection. This is useful for embedded cases that override the
|
||||
// service interface implementations and do not need connection management.
|
||||
func NewCtxClient(ctx context.Context) *Client {
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
return &Client{ctx: cctx, cancel: cancel}
|
||||
}
|
||||
|
||||
// NewFromURL creates a new etcdv3 client from a URL.
|
||||
func NewFromURL(url string) (*Client, error) {
|
||||
return New(Config{Endpoints: []string{url}})
|
||||
}
|
||||
|
||||
// NewFromConfigFile creates a new etcdv3 client from a configuration file.
|
||||
func NewFromConfigFile(path string) (*Client, error) {
|
||||
cfg, err := configFromFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return New(*cfg)
|
||||
}
|
||||
|
||||
// Close shuts down the client's etcd connections.
|
||||
func (c *Client) Close() error {
|
||||
c.cancel()
|
||||
c.Watcher.Close()
|
||||
c.Lease.Close()
|
||||
return toErr(c.ctx, c.conn.Close())
|
||||
if c.conn != nil {
|
||||
return toErr(c.ctx, c.conn.Close())
|
||||
}
|
||||
return c.ctx.Err()
|
||||
}
|
||||
|
||||
// Ctx is a context for "out of band" messages (e.g., for sending
|
||||
@ -177,8 +182,9 @@ func parseEndpoint(endpoint string) (proto string, host string, scheme string) {
|
||||
host = url.Host
|
||||
switch url.Scheme {
|
||||
case "http", "https":
|
||||
case "unix":
|
||||
case "unix", "unixs":
|
||||
proto = "unix"
|
||||
host = url.Host + url.Path
|
||||
default:
|
||||
proto, host = "", ""
|
||||
}
|
||||
@ -191,7 +197,7 @@ func (c *Client) processCreds(scheme string) (creds *credentials.TransportCreden
|
||||
case "unix":
|
||||
case "http":
|
||||
creds = nil
|
||||
case "https":
|
||||
case "https", "unixs":
|
||||
if creds != nil {
|
||||
break
|
||||
}
|
||||
@ -213,6 +219,11 @@ func (c *Client) dialSetupOpts(endpoint string, dopts ...grpc.DialOption) (opts
|
||||
|
||||
f := func(host string, t time.Duration) (net.Conn, error) {
|
||||
proto, host, _ := parseEndpoint(c.balancer.getEndpoint(host))
|
||||
if host == "" && endpoint != "" {
|
||||
// dialing an endpoint not in the balancer; use
|
||||
// endpoint passed into dial
|
||||
proto, host, _ = parseEndpoint(endpoint)
|
||||
}
|
||||
if proto == "" {
|
||||
return nil, fmt.Errorf("unknown scheme for %q", host)
|
||||
}
|
||||
@ -221,7 +232,15 @@ func (c *Client) dialSetupOpts(endpoint string, dopts ...grpc.DialOption) (opts
|
||||
return nil, c.ctx.Err()
|
||||
default:
|
||||
}
|
||||
return net.DialTimeout(proto, host, t)
|
||||
dialer := &net.Dialer{Timeout: t}
|
||||
conn, err := dialer.DialContext(c.ctx, proto, host)
|
||||
if err != nil {
|
||||
select {
|
||||
case c.dialerrc <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
opts = append(opts, grpc.WithDialer(f))
|
||||
|
||||
@ -281,19 +300,29 @@ func (c *Client) dial(endpoint string, dopts ...grpc.DialOption) (*grpc.ClientCo
|
||||
tokenMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
err := c.getToken(context.TODO())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
ctx := c.ctx
|
||||
if c.cfg.DialTimeout > 0 {
|
||||
cctx, cancel := context.WithTimeout(ctx, c.cfg.DialTimeout)
|
||||
defer cancel()
|
||||
ctx = cctx
|
||||
}
|
||||
|
||||
opts = append(opts, grpc.WithPerRPCCredentials(c.tokenCred))
|
||||
err := c.getToken(ctx)
|
||||
if err != nil {
|
||||
if toErr(ctx, err) != rpctypes.ErrAuthNotEnabled {
|
||||
if err == ctx.Err() && ctx.Err() != c.ctx.Err() {
|
||||
err = grpc.ErrClientConnTimeout
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
opts = append(opts, grpc.WithPerRPCCredentials(c.tokenCred))
|
||||
}
|
||||
}
|
||||
|
||||
// add metrics options
|
||||
opts = append(opts, grpc.WithUnaryInterceptor(prometheus.UnaryClientInterceptor))
|
||||
opts = append(opts, grpc.WithStreamInterceptor(prometheus.StreamClientInterceptor))
|
||||
opts = append(opts, c.cfg.DialOptions...)
|
||||
|
||||
conn, err := grpc.Dial(host, opts...)
|
||||
conn, err := grpc.DialContext(c.ctx, host, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -318,13 +347,19 @@ func newClient(cfg *Config) (*Client, error) {
|
||||
}
|
||||
|
||||
// use a temporary skeleton client to bootstrap first connection
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
baseCtx := context.TODO()
|
||||
if cfg.Context != nil {
|
||||
baseCtx = cfg.Context
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(baseCtx)
|
||||
client := &Client{
|
||||
conn: nil,
|
||||
cfg: *cfg,
|
||||
creds: creds,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
conn: nil,
|
||||
dialerrc: make(chan error, 1),
|
||||
cfg: *cfg,
|
||||
creds: creds,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
if cfg.Username != "" && cfg.Password != "" {
|
||||
client.Username = cfg.Username
|
||||
@ -332,8 +367,12 @@ func newClient(cfg *Config) (*Client, error) {
|
||||
}
|
||||
|
||||
client.balancer = newSimpleBalancer(cfg.Endpoints)
|
||||
// use Endpoints[0] so that for https:// without any tls config given, then
|
||||
// grpc will assume the ServerName is in the endpoint.
|
||||
conn, err := client.dial(cfg.Endpoints[0], grpc.WithBalancer(client.balancer))
|
||||
if err != nil {
|
||||
client.cancel()
|
||||
client.balancer.Close()
|
||||
return nil, err
|
||||
}
|
||||
client.conn = conn
|
||||
@ -351,9 +390,15 @@ func newClient(cfg *Config) (*Client, error) {
|
||||
case <-waitc:
|
||||
}
|
||||
if !hasConn {
|
||||
err := grpc.ErrClientConnTimeout
|
||||
select {
|
||||
case err = <-client.dialerrc:
|
||||
default:
|
||||
}
|
||||
client.cancel()
|
||||
client.balancer.Close()
|
||||
conn.Close()
|
||||
return nil, grpc.ErrClientConnTimeout
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@ -364,10 +409,57 @@ func newClient(cfg *Config) (*Client, error) {
|
||||
client.Auth = NewAuth(client)
|
||||
client.Maintenance = NewMaintenance(client)
|
||||
|
||||
if cfg.RejectOldCluster {
|
||||
if err := client.checkVersion(); err != nil {
|
||||
client.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
go client.autoSync()
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) checkVersion() (err error) {
|
||||
var wg sync.WaitGroup
|
||||
errc := make(chan error, len(c.cfg.Endpoints))
|
||||
ctx, cancel := context.WithCancel(c.ctx)
|
||||
if c.cfg.DialTimeout > 0 {
|
||||
ctx, _ = context.WithTimeout(ctx, c.cfg.DialTimeout)
|
||||
}
|
||||
wg.Add(len(c.cfg.Endpoints))
|
||||
for _, ep := range c.cfg.Endpoints {
|
||||
// if cluster is current, any endpoint gives a recent version
|
||||
go func(e string) {
|
||||
defer wg.Done()
|
||||
resp, rerr := c.Status(ctx, e)
|
||||
if rerr != nil {
|
||||
errc <- rerr
|
||||
return
|
||||
}
|
||||
vs := strings.Split(resp.Version, ".")
|
||||
maj, min := 0, 0
|
||||
if len(vs) >= 2 {
|
||||
maj, rerr = strconv.Atoi(vs[0])
|
||||
min, rerr = strconv.Atoi(vs[1])
|
||||
}
|
||||
if maj < 3 || (maj == 3 && min < 2) {
|
||||
rerr = ErrOldCluster
|
||||
}
|
||||
errc <- rerr
|
||||
}(ep)
|
||||
}
|
||||
// wait for success
|
||||
for i := 0; i < len(c.cfg.Endpoints); i++ {
|
||||
if err = <-errc; err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
wg.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
// ActiveConnection returns the current in-use connection
|
||||
func (c *Client) ActiveConnection() *grpc.ClientConn { return c.conn }
|
||||
|
||||
@ -413,3 +505,11 @@ func toErr(ctx context.Context, err error) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func canceledByCaller(stopCtx context.Context, err error) bool {
|
||||
if stopCtx.Err() == nil || err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return err == context.Canceled || err == context.DeadlineExceeded
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ package clientv3
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -25,36 +26,100 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestDialTimeout(t *testing.T) {
|
||||
func TestDialCancel(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
donec := make(chan error)
|
||||
// accept first connection so client is created with dial timeout
|
||||
ln, err := net.Listen("unix", "dialcancel:12345")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
ep := "unix://dialcancel:12345"
|
||||
cfg := Config{
|
||||
Endpoints: []string{ep},
|
||||
DialTimeout: 30 * time.Second}
|
||||
c, err := New(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// connect to ipv4 blackhole so dial blocks
|
||||
c.SetEndpoints("http://254.0.0.1:12345")
|
||||
|
||||
// issue Get to force redial attempts
|
||||
getc := make(chan struct{})
|
||||
go func() {
|
||||
// without timeout, grpc keeps redialing if connection refused
|
||||
cfg := Config{
|
||||
Endpoints: []string{"localhost:12345"},
|
||||
DialTimeout: 2 * time.Second}
|
||||
c, err := New(cfg)
|
||||
if c != nil || err == nil {
|
||||
t.Errorf("new client should fail")
|
||||
}
|
||||
donec <- err
|
||||
defer close(getc)
|
||||
// Get may hang forever on grpc's Stream.Header() if its
|
||||
// context is never canceled.
|
||||
c.Get(c.Ctx(), "abc")
|
||||
}()
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
// wait a little bit so client close is after dial starts
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
select {
|
||||
case err := <-donec:
|
||||
t.Errorf("dial didn't wait (%v)", err)
|
||||
default:
|
||||
}
|
||||
donec := make(chan struct{})
|
||||
go func() {
|
||||
defer close(donec)
|
||||
c.Close()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("failed to timeout dial on time")
|
||||
case err := <-donec:
|
||||
if err != grpc.ErrClientConnTimeout {
|
||||
t.Errorf("unexpected error %v, want %v", err, grpc.ErrClientConnTimeout)
|
||||
t.Fatalf("failed to close")
|
||||
case <-donec:
|
||||
}
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("get failed to exit")
|
||||
case <-getc:
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialTimeout(t *testing.T) {
|
||||
defer testutil.AfterTest(t)
|
||||
|
||||
testCfgs := []Config{
|
||||
{
|
||||
Endpoints: []string{"http://254.0.0.1:12345"},
|
||||
DialTimeout: 2 * time.Second,
|
||||
},
|
||||
{
|
||||
Endpoints: []string{"http://254.0.0.1:12345"},
|
||||
DialTimeout: time.Second,
|
||||
Username: "abc",
|
||||
Password: "def",
|
||||
},
|
||||
}
|
||||
|
||||
for i, cfg := range testCfgs {
|
||||
donec := make(chan error)
|
||||
go func() {
|
||||
// without timeout, dial continues forever on ipv4 blackhole
|
||||
c, err := New(cfg)
|
||||
if c != nil || err == nil {
|
||||
t.Errorf("#%d: new client should fail", i)
|
||||
}
|
||||
donec <- err
|
||||
}()
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
select {
|
||||
case err := <-donec:
|
||||
t.Errorf("#%d: dial didn't wait (%v)", i, err)
|
||||
default:
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("#%d: failed to timeout dial on time", i)
|
||||
case err := <-donec:
|
||||
if err != grpc.ErrClientConnTimeout {
|
||||
t.Errorf("#%d: unexpected error %v, want %v", i, err, grpc.ErrClientConnTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
66
clientv3/clientv3util/example_key_test.go
Normal file
66
clientv3/clientv3util/example_key_test.go
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright 2017 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clientv3util_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/clientv3/clientv3util"
|
||||
)
|
||||
|
||||
func ExampleKeyExists_put() {
|
||||
cli, err := clientv3.New(clientv3.Config{
|
||||
Endpoints: []string{"127.0.0.1:2379"},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cli.Close()
|
||||
kvc := clientv3.NewKV(cli)
|
||||
|
||||
// perform a put only if key is missing
|
||||
// It is useful to do the check (transactionally) to avoid overwriting
|
||||
// the existing key which would generate potentially unwanted events,
|
||||
// unless of course you wanted to do an overwrite no matter what.
|
||||
_, err = kvc.Txn(context.Background()).
|
||||
If(clientv3util.KeyMissing("purpleidea")).
|
||||
Then(clientv3.OpPut("purpleidea", "hello world")).
|
||||
Commit()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleKeyExists_delete() {
|
||||
cli, err := clientv3.New(clientv3.Config{
|
||||
Endpoints: []string{"127.0.0.1:2379"},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer cli.Close()
|
||||
kvc := clientv3.NewKV(cli)
|
||||
|
||||
// perform a delete only if key already exists
|
||||
_, err = kvc.Txn(context.Background()).
|
||||
If(clientv3util.KeyExists("purpleidea")).
|
||||
Then(clientv3.OpDelete("purpleidea")).
|
||||
Commit()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
33
clientv3/clientv3util/util.go
Normal file
33
clientv3/clientv3util/util.go
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright 2017 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package clientv3util contains utility functions derived from clientv3.
|
||||
package clientv3util
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
)
|
||||
|
||||
// KeyExists returns a comparison operation that evaluates to true iff the given
|
||||
// key exists. It does this by checking if the key `Version` is greater than 0.
|
||||
// It is a useful guard in transaction delete operations.
|
||||
func KeyExists(key string) clientv3.Cmp {
|
||||
return clientv3.Compare(clientv3.Version(key), ">", 0)
|
||||
}
|
||||
|
||||
// KeyMissing returns a comparison operation that evaluates to true iff the
|
||||
// given key does not exist.
|
||||
func KeyMissing(key string) clientv3.Cmp {
|
||||
return clientv3.Compare(clientv3.Version(key), "=", 0)
|
||||
}
|
@ -50,28 +50,26 @@ func NewCluster(c *Client) Cluster {
|
||||
return &cluster{remote: RetryClusterClient(c)}
|
||||
}
|
||||
|
||||
func NewClusterFromClusterClient(remote pb.ClusterClient) Cluster {
|
||||
return &cluster{remote: remote}
|
||||
}
|
||||
|
||||
func (c *cluster) MemberAdd(ctx context.Context, peerAddrs []string) (*MemberAddResponse, error) {
|
||||
r := &pb.MemberAddRequest{PeerURLs: peerAddrs}
|
||||
resp, err := c.remote.MemberAdd(ctx, r)
|
||||
if err == nil {
|
||||
return (*MemberAddResponse)(resp), nil
|
||||
}
|
||||
if isHaltErr(ctx, err) {
|
||||
if err != nil {
|
||||
return nil, toErr(ctx, err)
|
||||
}
|
||||
return nil, toErr(ctx, err)
|
||||
return (*MemberAddResponse)(resp), nil
|
||||
}
|
||||
|
||||
func (c *cluster) MemberRemove(ctx context.Context, id uint64) (*MemberRemoveResponse, error) {
|
||||
r := &pb.MemberRemoveRequest{ID: id}
|
||||
resp, err := c.remote.MemberRemove(ctx, r)
|
||||
if err == nil {
|
||||
return (*MemberRemoveResponse)(resp), nil
|
||||
}
|
||||
if isHaltErr(ctx, err) {
|
||||
if err != nil {
|
||||
return nil, toErr(ctx, err)
|
||||
}
|
||||
return nil, toErr(ctx, err)
|
||||
return (*MemberRemoveResponse)(resp), nil
|
||||
}
|
||||
|
||||
func (c *cluster) MemberUpdate(ctx context.Context, id uint64, peerAddrs []string) (*MemberUpdateResponse, error) {
|
||||
|
@ -82,6 +82,23 @@ func ModRevision(key string) Cmp {
|
||||
return Cmp{Key: []byte(key), Target: pb.Compare_MOD}
|
||||
}
|
||||
|
||||
// KeyBytes returns the byte slice holding with the comparison key.
|
||||
func (cmp *Cmp) KeyBytes() []byte { return cmp.Key }
|
||||
|
||||
// WithKeyBytes sets the byte slice for the comparison key.
|
||||
func (cmp *Cmp) WithKeyBytes(key []byte) { cmp.Key = key }
|
||||
|
||||
// ValueBytes returns the byte slice holding the comparison value, if any.
|
||||
func (cmp *Cmp) ValueBytes() []byte {
|
||||
if tu, ok := cmp.TargetUnion.(*pb.Compare_Value); ok {
|
||||
return tu.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithValueBytes sets the byte slice for the comparison's value.
|
||||
func (cmp *Cmp) WithValueBytes(v []byte) { cmp.TargetUnion.(*pb.Compare_Value).Value = v }
|
||||
|
||||
func mustInt64(val interface{}) int64 {
|
||||
if v, ok := val.(int64); ok {
|
||||
return v
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@ -36,6 +37,7 @@ type Election struct {
|
||||
leaderKey string
|
||||
leaderRev int64
|
||||
leaderSession *Session
|
||||
hdr *pb.ResponseHeader
|
||||
}
|
||||
|
||||
// NewElection returns a new election on a given key prefix.
|
||||
@ -43,6 +45,16 @@ func NewElection(s *Session, pfx string) *Election {
|
||||
return &Election{session: s, keyPrefix: pfx + "/"}
|
||||
}
|
||||
|
||||
// ResumeElection initializes an election with a known leader.
|
||||
func ResumeElection(s *Session, pfx string, leaderKey string, leaderRev int64) *Election {
|
||||
return &Election{
|
||||
session: s,
|
||||
leaderKey: leaderKey,
|
||||
leaderRev: leaderRev,
|
||||
leaderSession: s,
|
||||
}
|
||||
}
|
||||
|
||||
// Campaign puts a value as eligible for the election. It blocks until
|
||||
// it is elected, an error occurs, or the context is cancelled.
|
||||
func (e *Election) Campaign(ctx context.Context, val string) error {
|
||||
@ -69,7 +81,7 @@ func (e *Election) Campaign(ctx context.Context, val string) error {
|
||||
}
|
||||
}
|
||||
|
||||
err = waitDeletes(ctx, client, e.keyPrefix, e.leaderRev-1)
|
||||
_, err = waitDeletes(ctx, client, e.keyPrefix, e.leaderRev-1)
|
||||
if err != nil {
|
||||
// clean up in case of context cancel
|
||||
select {
|
||||
@ -80,6 +92,7 @@ func (e *Election) Campaign(ctx context.Context, val string) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
e.hdr = resp.Header
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -101,6 +114,8 @@ func (e *Election) Proclaim(ctx context.Context, val string) error {
|
||||
e.leaderKey = ""
|
||||
return ErrElectionNotLeader
|
||||
}
|
||||
|
||||
e.hdr = tresp.Header
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -110,28 +125,36 @@ func (e *Election) Resign(ctx context.Context) (err error) {
|
||||
return nil
|
||||
}
|
||||
client := e.session.Client()
|
||||
_, err = client.Delete(ctx, e.leaderKey)
|
||||
cmp := v3.Compare(v3.CreateRevision(e.leaderKey), "=", e.leaderRev)
|
||||
resp, err := client.Txn(ctx).If(cmp).Then(v3.OpDelete(e.leaderKey)).Commit()
|
||||
if err == nil {
|
||||
e.hdr = resp.Header
|
||||
}
|
||||
e.leaderKey = ""
|
||||
e.leaderSession = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Leader returns the leader value for the current election.
|
||||
func (e *Election) Leader(ctx context.Context) (string, error) {
|
||||
func (e *Election) Leader(ctx context.Context) (*v3.GetResponse, error) {
|
||||
client := e.session.Client()
|
||||
resp, err := client.Get(ctx, e.keyPrefix, v3.WithFirstCreate()...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
} else if len(resp.Kvs) == 0 {
|
||||
// no leader currently elected
|
||||
return "", ErrElectionNoLeader
|
||||
return nil, ErrElectionNoLeader
|
||||
}
|
||||
return string(resp.Kvs[0].Value), nil
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Observe returns a channel that observes all leader proposal values as
|
||||
// GetResponse values on the current leader key. The channel closes when
|
||||
// the context is cancelled or the underlying watcher is otherwise disrupted.
|
||||
// Observe returns a channel that reliably observes ordered leader proposals
|
||||
// as GetResponse values on every current elected leader key. It will not
|
||||
// necessarily fetch all historical leader updates, but will always post the
|
||||
// most recent leader value.
|
||||
//
|
||||
// The channel closes when the context is canceled or the underlying watcher
|
||||
// is otherwise disrupted.
|
||||
func (e *Election) Observe(ctx context.Context) <-chan v3.GetResponse {
|
||||
retc := make(chan v3.GetResponse)
|
||||
go e.observe(ctx, retc)
|
||||
@ -149,13 +172,13 @@ func (e *Election) observe(ctx context.Context, ch chan<- v3.GetResponse) {
|
||||
}
|
||||
|
||||
var kv *mvccpb.KeyValue
|
||||
var hdr *pb.ResponseHeader
|
||||
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
if len(resp.Kvs) == 0 {
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
// wait for first key put on prefix
|
||||
opts := []v3.OpOption{v3.WithRev(resp.Header.Revision), v3.WithPrefix()}
|
||||
wch := client.Watch(cctx, e.keyPrefix, opts...)
|
||||
|
||||
for kv == nil {
|
||||
wr, ok := <-wch
|
||||
if !ok || wr.Err() != nil {
|
||||
@ -165,16 +188,27 @@ func (e *Election) observe(ctx context.Context, ch chan<- v3.GetResponse) {
|
||||
// only accept PUTs; a DELETE will make observe() spin
|
||||
for _, ev := range wr.Events {
|
||||
if ev.Type == mvccpb.PUT {
|
||||
kv = ev.Kv
|
||||
hdr, kv = &wr.Header, ev.Kv
|
||||
// may have multiple revs; hdr.rev = the last rev
|
||||
// set to kv's rev in case batch has multiple PUTs
|
||||
hdr.Revision = kv.ModRevision
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
} else {
|
||||
kv = resp.Kvs[0]
|
||||
hdr, kv = resp.Header, resp.Kvs[0]
|
||||
}
|
||||
|
||||
wch := client.Watch(cctx, string(kv.Key), v3.WithRev(kv.ModRevision))
|
||||
select {
|
||||
case ch <- v3.GetResponse{Header: hdr, Kvs: []*mvccpb.KeyValue{kv}}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
wch := client.Watch(cctx, string(kv.Key), v3.WithRev(hdr.Revision+1))
|
||||
keyDeleted := false
|
||||
for !keyDeleted {
|
||||
wr, ok := <-wch
|
||||
@ -201,3 +235,9 @@ func (e *Election) observe(ctx context.Context, ch chan<- v3.GetResponse) {
|
||||
|
||||
// Key returns the leader key if elected, empty string otherwise.
|
||||
func (e *Election) Key() string { return e.leaderKey }
|
||||
|
||||
// Rev returns the leader key's creation revision, if elected.
|
||||
func (e *Election) Rev() int64 { return e.leaderRev }
|
||||
|
||||
// Header is the response header from the last successful election proposal.
|
||||
func (m *Election) Header() *pb.ResponseHeader { return m.hdr }
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user