Compare commits
1652 Commits
v0.4.7
...
v0.5.0-alp
Author | SHA1 | Date | |
---|---|---|---|
0b493ac8d4 | |||
c73d41d98b | |||
3d2f65fc0d | |||
6b283f6ea1 | |||
4367c9a1db | |||
c9894687fc | |||
a56fa60fb4 | |||
014ef0f52d | |||
2fc47034ee | |||
46cbfbc630 | |||
d3fd10798b | |||
a6ba4d357c | |||
e707af7c3a | |||
ca06fd0060 | |||
958ade86a5 | |||
85a4477f71 | |||
38ec84693f | |||
78865aa7f7 | |||
0d541e6338 | |||
5f6e536be8 | |||
4f85a68c25 | |||
c3aae88b0c | |||
32a82bb423 | |||
285cd404e3 | |||
a607e097c6 | |||
55b4ff0cdf | |||
82094f05e0 | |||
c4f273478d | |||
14e1442d2d | |||
810a5146dd | |||
5055863e09 | |||
bf47fe7cac | |||
9d19429993 | |||
142dfc7d88 | |||
0a9c6164af | |||
376268391b | |||
d6f37ec9ad | |||
ca1b30db10 | |||
9454d30854 | |||
f75e56932a | |||
5604b4c57c | |||
8f1885a398 | |||
ccded6644a | |||
321d65c4ac | |||
c5e6053fcd | |||
eb0d80767e | |||
6fa031fa69 | |||
21987c8701 | |||
457b30e585 | |||
2138163c61 | |||
211c5e3e29 | |||
c3b0de943c | |||
1e05cd75c7 | |||
2d942e970b | |||
087e0e8b62 | |||
b65dd84e1a | |||
66572561bf | |||
902f06c5c4 | |||
b53a98eb38 | |||
a1f5df22ad | |||
04f6208ace | |||
3cb885c6b2 | |||
f4ea274555 | |||
4b555dba99 | |||
9c8f9b3560 | |||
4ed60471fe | |||
7d28d80e5a | |||
45d7ef99c4 | |||
0d8345e0c1 | |||
2760739ceb | |||
5d755bd54a | |||
bd2b18b6de | |||
68bca981de | |||
6fdbb086f4 | |||
99b1af40c6 | |||
99bb479a60 | |||
98406af448 | |||
6c9169b4f4 | |||
3fc6f9c24f | |||
0d7c43d885 | |||
c5140d5c18 | |||
fdb82718e0 | |||
791b2fd503 | |||
3c3cae57c6 | |||
bdd2a0a018 | |||
c6104c1e2a | |||
b85496922f | |||
89eac70d09 | |||
58b171b3e5 | |||
bb84aaebaf | |||
ab00d23cd3 | |||
5de9d38cc6 | |||
d36f09d643 | |||
f71c247d87 | |||
71acd0c3d0 | |||
288624550e | |||
e4d0c25365 | |||
c628d7f412 | |||
5cb13fd071 | |||
9e001dee29 | |||
4d40816a90 | |||
0f7add9722 | |||
9f29545f66 | |||
45b7c9a4ac | |||
34dabe281b | |||
5fbef59dbc | |||
915f8f4822 | |||
cedcc0d8df | |||
ac49e1d50f | |||
866ec5948c | |||
aa5711bd0f | |||
f7434b55e5 | |||
2235b47030 | |||
5ead800ff5 | |||
e4b12a8e28 | |||
9aefb91531 | |||
5ed5d44652 | |||
cc0ef16346 | |||
a272f5d7e3 | |||
63cf0b9d90 | |||
ab69c2adbd | |||
075ab6415f | |||
dd09042632 | |||
165ac654e8 | |||
dbdeceda7b | |||
ff1f5a9d57 | |||
d1ec13210f | |||
2ba02c04be | |||
6dd4944e62 | |||
5da481213e | |||
433b4138c5 | |||
729770f32a | |||
3ec4da6ac6 | |||
9df06bfa94 | |||
20df86e3c3 | |||
6433be5738 | |||
3068340a83 | |||
da6827f09e | |||
75104c10d4 | |||
58af26736c | |||
17c6f21d68 | |||
f0760d6246 | |||
913d102a81 | |||
824049897d | |||
b47631b38f | |||
22b86684f0 | |||
5ed5d018be | |||
f6e8b677cf | |||
0ef270c25c | |||
1130273178 | |||
3eb126af4d | |||
c282664c23 | |||
d52d836761 | |||
5bdf6a4110 | |||
421d5fbe72 | |||
f35130a0ed | |||
500e9e2212 | |||
7c52a86325 | |||
124dd7096a | |||
388b4aeb71 | |||
6b4485d1ae | |||
74886713db | |||
8f3be206ed | |||
1db23109ad | |||
749097429f | |||
34b2fecd28 | |||
faede90293 | |||
b6cc34b52e | |||
308b8796e4 | |||
6e038e02a6 | |||
38250d3fac | |||
eab4692744 | |||
f0c3385cfc | |||
8b8b3efdaa | |||
8d519ffdb8 | |||
323fb1ec85 | |||
9d07db4432 | |||
7c1f4a9baf | |||
dee912f2fd | |||
bc62b05c7f | |||
48ec876af9 | |||
a576dbca43 | |||
eb472b7745 | |||
a535161a84 | |||
513c72ec8b | |||
e02ef6b141 | |||
2c5f062b7f | |||
1bb07115f2 | |||
9726d3909c | |||
c53e58e97c | |||
55c92ad456 | |||
781abc1db0 | |||
aa50af1c69 | |||
7f29045c0f | |||
0f8b035253 | |||
42a7c928d4 | |||
02ff59514f | |||
9a56001d63 | |||
8e633db5cb | |||
64a12e9341 | |||
ac71ad92af | |||
ed30b6deca | |||
76298ebcd8 | |||
d36a3e18d2 | |||
3dfb6723b2 | |||
6087e2b2f6 | |||
6e8de1f426 | |||
052521eaf1 | |||
549c643bfe | |||
af7d73717c | |||
816c173edf | |||
9359a57211 | |||
b99633207c | |||
4f6206bf65 | |||
bf44219766 | |||
19881b2f15 | |||
46ebf69c02 | |||
0cf0cb3d02 | |||
83ca16188c | |||
cf9dd31daa | |||
38617f5c9b | |||
027e944985 | |||
2be3f870cc | |||
ba38847bdd | |||
97597eca03 | |||
243886edc8 | |||
f61824ce01 | |||
ac810b86bc | |||
e85ba2f384 | |||
f5c1da6967 | |||
0f51cbde6c | |||
a910d8ba9f | |||
d756dd2079 | |||
5264c05ddb | |||
4e759b46ce | |||
011a67c878 | |||
e457d52f5c | |||
ccca32b138 | |||
dabb5c150d | |||
b7b3bf40e0 | |||
2c0f6e4bf9 | |||
3f6e584702 | |||
97c23c4333 | |||
95231c1278 | |||
f810dda9b2 | |||
f6e242aa01 | |||
8b12e1aa37 | |||
b59961228b | |||
738da2b3fa | |||
de0cf2fb8e | |||
4b1431109e | |||
6375bd7960 | |||
e99da41539 | |||
14f4163e41 | |||
5bba81f5fc | |||
a59e8cf1a6 | |||
9546df9a6c | |||
f7631be453 | |||
81304b2b7e | |||
8298e06627 | |||
ab67fa4cc6 | |||
bab19e3b0b | |||
d3bafd6aa4 | |||
84be7c1e9e | |||
ad1718a3e5 | |||
35bba87d2a | |||
bffe611fe6 | |||
6bcfa2b05d | |||
3857e92cad | |||
04e56a454e | |||
658a84312b | |||
ae7280dcf3 | |||
c6873c1eab | |||
a96f5ab146 | |||
6f851ac885 | |||
2b4201c53d | |||
57d447fef6 | |||
c07b9ae32e | |||
8fbf887e52 | |||
d1fb732e63 | |||
8b0eaa9e15 | |||
ad0664da9c | |||
b6b5081254 | |||
6796669484 | |||
87327a245d | |||
e08c2bbe3e | |||
8d052dd374 | |||
480e92d340 | |||
dad7500d13 | |||
d55546d62e | |||
acd8eecd4e | |||
2d31e5ab56 | |||
2472953939 | |||
80172c3d4a | |||
b316c6b002 | |||
6cb45236ac | |||
04b5853261 | |||
b1731f0843 | |||
36cacb8bd8 | |||
e849d8e157 | |||
387639e802 | |||
3e234918ee | |||
0ce78d7a9c | |||
52350d1d2f | |||
7384ee39a6 | |||
d0604c7d5c | |||
74c257f63d | |||
460d6490ba | |||
60cb18b6c2 | |||
e8302c8413 | |||
b986a52579 | |||
538ce935f0 | |||
94e4595af5 | |||
753bc5e166 | |||
80ca168cbe | |||
14795d8ed9 | |||
7545152318 | |||
54a2d8ffc9 | |||
ee27846d5b | |||
e77f8e311c | |||
585881a870 | |||
9964bfa6b9 | |||
6e6d1897d8 | |||
328d8f2d26 | |||
6f792354ca | |||
40048d7300 | |||
000962d689 | |||
444e6e952b | |||
f9af07eb5b | |||
b06499d0c2 | |||
4b77082b6e | |||
009b737cef | |||
94f701cf95 | |||
8cd95e916d | |||
86c66cd802 | |||
90f26e4a56 | |||
73215447c1 | |||
cba19e348f | |||
435611cf0d | |||
00dcbf8bf7 | |||
73e48068c2 | |||
2b9cabcbcd | |||
2baf3e0d79 | |||
677d9d1bea | |||
91a4aa151a | |||
b62fdac193 | |||
e630910e31 | |||
338f59db74 | |||
719c57a29d | |||
0e1d1646fd | |||
0fcb59e7d9 | |||
5456ef7049 | |||
cb59a46576 | |||
46528ee17b | |||
9a465b9cf5 | |||
dc46d40b8e | |||
bac13b5cb2 | |||
34dcbb4679 | |||
030aed8205 | |||
2e84eb3c36 | |||
9a38be297c | |||
f21d93ba60 | |||
45d8fbdcda | |||
ce4df96e69 | |||
d7f9228133 | |||
49a68adcf1 | |||
ea0bff80c0 | |||
d1b57b448d | |||
08593bcdf6 | |||
9fb02eb6fa | |||
543e12074a | |||
14852662ef | |||
7ef468b315 | |||
9b679de9dd | |||
c8634428fa | |||
507300130b | |||
e081ad7298 | |||
85800fd8f6 | |||
0276089ed9 | |||
3a41161e76 | |||
b9514ea265 | |||
4c9d67aaa2 | |||
ed29259801 | |||
500d21591f | |||
992e7c76e0 | |||
a85a47c8f9 | |||
ebe32689d4 | |||
d1d12abfd7 | |||
d7301a5cf4 | |||
d8258c38be | |||
0eddf3db1f | |||
af42f4a56b | |||
02551c277d | |||
17bd5c3d21 | |||
0d36385bb4 | |||
4089475c90 | |||
4d80f01201 | |||
c25c50582e | |||
d47de988e4 | |||
5fbe6c7134 | |||
8eee8c260e | |||
e21de51768 | |||
f8b8bdeb17 | |||
3d243baacd | |||
d2c4e981ed | |||
67412e07f8 | |||
89572b5fd7 | |||
09e9618b02 | |||
233617bea2 | |||
16c9970a03 | |||
86facb3f91 | |||
8d6bb4a471 | |||
051ad7585f | |||
2cb8efb9b5 | |||
1539d5c49c | |||
69842b344d | |||
52e08720b7 | |||
29ef918808 | |||
9f2a42bf7d | |||
9af5b74a8d | |||
46be5d7d5f | |||
63fa2a626a | |||
712a05be83 | |||
ab90369f9e | |||
7be0f4b618 | |||
ec7af4f519 | |||
0d0bc3a57e | |||
2c21ae0f16 | |||
1f84991b3a | |||
400dd2d7bc | |||
e42d65da12 | |||
d27d308935 | |||
5444a366da | |||
5780dfe690 | |||
6d9eb57555 | |||
d6a5dc9e61 | |||
ca4f12182a | |||
d00152765a | |||
13656eb4e7 | |||
829cec8ccf | |||
6b32395637 | |||
5014558b00 | |||
3162ead7b1 | |||
9ad4a8e33a | |||
89b032cd69 | |||
7498234e40 | |||
b40d30a8d2 | |||
c5d1fcd70a | |||
cb5a638c44 | |||
52dedab7b4 | |||
502a3c2460 | |||
1347e3952f | |||
ad0b7b7dbb | |||
1ca7c031ff | |||
456d1ebcae | |||
4f52d371c1 | |||
e475388bc0 | |||
48992cced3 | |||
f356648252 | |||
f26bb6ad44 | |||
06b196e345 | |||
ca73f25615 | |||
e2b6a4fc4c | |||
2ff3cac653 | |||
0398a31b16 | |||
e69c37adf0 | |||
00dc61d169 | |||
48c4145f1b | |||
a9984fda4f | |||
50d4abc676 | |||
c3f83f9275 | |||
341c7190d3 | |||
894e678ad6 | |||
ae4403c945 | |||
a44849deec | |||
99dd42026b | |||
b3d5333cb3 | |||
0fd28169c8 | |||
dc68dc9ebd | |||
d67a2855c1 | |||
92230cee63 | |||
63b328ed11 | |||
e200d2a8e2 | |||
ea6bcacfe4 | |||
68da8084d0 | |||
ef44ba10cf | |||
80212aaf4d | |||
500a72962e | |||
7af679333a | |||
1b7947357f | |||
40279d324a | |||
f7a0d5387b | |||
058537f34a | |||
8fa3834d69 | |||
3184e1c66f | |||
dcaa7f0a37 | |||
17382ec905 | |||
da23327265 | |||
7a4d42166b | |||
aa176610f3 | |||
f12583c163 | |||
1456ae4453 | |||
e0801360d3 | |||
ec18e46641 | |||
3134658ded | |||
47c2421f7b | |||
de3bf58876 | |||
c5ba66e6aa | |||
aed525edee | |||
766aa85320 | |||
6aa46d20d4 | |||
da64e7509c | |||
5c4edf65f9 | |||
1fa763b47b | |||
70bbf8b470 | |||
82023c591d | |||
233e940410 | |||
c28907ba95 | |||
c30b82b596 | |||
7311a2a67d | |||
bc7d372d5c | |||
67368ac7fa | |||
2af0b2031f | |||
b7c42b0d76 | |||
fc42bdb904 | |||
f7988e6069 | |||
3a29db1e9d | |||
7ef375efbd | |||
782d91f2d9 | |||
074ddb5876 | |||
eb72bdc3d2 | |||
da2ee9a90c | |||
8609acf573 | |||
2cd6594485 | |||
7f8f371b0e | |||
6a30d3ba04 | |||
9b9e72e2a3 | |||
97ae531eda | |||
fedb67a71a | |||
8168fed825 | |||
0a8721a708 | |||
79e9f2ab81 | |||
8cd6030a1d | |||
82476e04f0 | |||
efba919a93 | |||
c18acd7d6f | |||
e334148a91 | |||
eb2dd1892f | |||
a8a1d4fd93 | |||
f62d4908b0 | |||
828accf07b | |||
6d0658c8ca | |||
48c195fac7 | |||
7656069675 | |||
99e35554c0 | |||
b0fcd680f8 | |||
f98fbbfc14 | |||
2b03d35ab9 | |||
4183b69e12 | |||
31264e7eb5 | |||
09f9884c6a | |||
fbb874172c | |||
6fc0b1977b | |||
b53b74733a | |||
30c7a7f2dd | |||
a85ec90d68 | |||
57ae19b500 | |||
1177b07535 | |||
d7dfe07e5d | |||
9722aac10d | |||
a6dfde85e4 | |||
85c2d852f3 | |||
f693c6ddf2 | |||
0319b033ea | |||
32c38820c1 | |||
d06d55193b | |||
b8b4852ec9 | |||
6ffaa4db5d | |||
3516cc3ee5 | |||
f16a272898 | |||
f8b338d423 | |||
447caf1afc | |||
181cbbdfe0 | |||
d241275518 | |||
2ec999ab3b | |||
77271b0663 | |||
74ab003e1f | |||
5e7267a751 | |||
3e5073e9be | |||
1eb1020717 | |||
8bbbaa88b2 | |||
af5b8c6c44 | |||
8c5aa16d03 | |||
5fde52a403 | |||
9b35ca3a52 | |||
38af14b0f4 | |||
dbac2e8f15 | |||
ea99d3c002 | |||
682008724d | |||
abe97e49d5 | |||
73f2aaf98f | |||
c67fd14fe8 | |||
b4a7680bc4 | |||
a07c51a9c9 | |||
d2e858587f | |||
7b61565c0a | |||
f3870598b9 | |||
5f3fe7c61f | |||
1cd3345e00 | |||
75f6643982 | |||
8e8719f6ac | |||
1083ce8f73 | |||
36558b1924 | |||
3ad0df3722 | |||
fe0e168b3b | |||
ca1bbee737 | |||
5b8c4f4e0d | |||
cdea98d434 | |||
6a62621695 | |||
3859297225 | |||
d051af4d3d | |||
ef0ed31210 | |||
f65d117462 | |||
d7d6f84f64 | |||
2e0fec7a84 | |||
26160b2154 | |||
3f3b9866c6 | |||
45e4a8643a | |||
7fe4385ef9 | |||
5587e0d73f | |||
de024ec844 | |||
6cbc282be1 | |||
39e0a0cd0a | |||
d6aea2a795 | |||
8a311e5b76 | |||
1a0195e07e | |||
3ca3c9ad4c | |||
b9c8ac73be | |||
120b088723 | |||
5098cb0d32 | |||
45ebfb4217 | |||
d98fe2ce1a | |||
c15c3eab4c | |||
a10c62ae25 | |||
3500b56e54 | |||
e2d8037ded | |||
314d425718 | |||
7081dabd12 | |||
ec7bcbb50d | |||
6bc160b4e3 | |||
9e3d045b2b | |||
824b7231b8 | |||
dc9cb4b4ba | |||
7ce3bb180c | |||
d2df23183d | |||
2606508d1c | |||
f432b9d29b | |||
1ca5991c8c | |||
1308c3e809 | |||
e5f5fcff48 | |||
af6b29f291 | |||
1c4163faf8 | |||
01ecc60a88 | |||
172bd7d096 | |||
5b291b521b | |||
70bf464cd6 | |||
16ba77767e | |||
1c11f6a144 | |||
8490904f20 | |||
fff918c672 | |||
4a5bf2e1b7 | |||
b64246720b | |||
182c8316e1 | |||
e4a6c9651a | |||
b1fc0feb72 | |||
83137f9eba | |||
619c7f9fdb | |||
f84b5b1071 | |||
b3c7711da8 | |||
073eb7677d | |||
dd88a08f8e | |||
11582b0f5f | |||
add3906f6c | |||
c9cac5fee5 | |||
cbc84bc70e | |||
a40a270e19 | |||
1356037fc6 | |||
c0c0b08ff2 | |||
fd758c71b8 | |||
8944364884 | |||
7a698be6a3 | |||
6760345453 | |||
04bd48fef3 | |||
7639752c82 | |||
c2f96631d3 | |||
aa5b6cdc9e | |||
ce70e63cc6 | |||
d7b4e44a66 | |||
b3c1bd5616 | |||
b6a73c9358 | |||
34547229a6 | |||
0e8345aa73 | |||
768090754c | |||
08e5f39d8a | |||
89077167c3 | |||
1eb09acd8b | |||
1c27fad2cf | |||
e26ff32fd8 | |||
51529cc3f2 | |||
adefd83855 | |||
86473d8a27 | |||
e38fbfe9de | |||
d8bf9728d2 | |||
78a9bba276 | |||
5784693a39 | |||
e83f851995 | |||
15798a73d9 | |||
2a0f3d85c8 | |||
1d5d2e3726 | |||
04f6993108 | |||
61dc89e7f3 | |||
5e3fd6ee3f | |||
56c64ab2e8 | |||
9c9437a9e7 | |||
9b3478218e | |||
5874387871 | |||
8a60257bff | |||
9e46d54483 | |||
720aa6aeae | |||
0662afc95f | |||
5feef73a17 | |||
f393b1459a | |||
aab41f06d0 | |||
f96f1041fd | |||
05d8f7270f | |||
e20e286064 | |||
20ac0ee80d | |||
9158d293fd | |||
2f6086de22 | |||
a0f5625728 | |||
45f71af33e | |||
2e4f725c2b | |||
2da1010cf7 | |||
db3afb18df | |||
f196276ca6 | |||
fa762e6b25 | |||
c82309d2b4 | |||
98561f6b5d | |||
9ba35bc95e | |||
20ac7d6732 | |||
f603d90775 | |||
42957815d3 | |||
48e8ea1569 | |||
2b52384e7e | |||
a6b7f4e5ea | |||
4a65813a66 | |||
e30c1eeefd | |||
b0617be7e3 | |||
f7c353a703 | |||
18c300f80c | |||
314c13a8f0 | |||
002ace2403 | |||
13c20b1b64 | |||
c2ced7dc70 | |||
ef0ba361df | |||
5059062275 | |||
69fba03fc1 | |||
510213b1c2 | |||
2e2cd12407 | |||
f27b4cbbce | |||
a45d490598 | |||
c28fd92d10 | |||
b15fefa8ea | |||
bbbd5fd5ec | |||
bcedef83d3 | |||
c8c55aa378 | |||
1f736263b2 | |||
ec1df42d04 | |||
784d7ac680 | |||
a9caa24f8a | |||
ddc30c0a33 | |||
172a32e5e3 | |||
7aaaf49fee | |||
1ca03d8e9d | |||
b07be74a82 | |||
af01e11a5b | |||
f34e37f68f | |||
e97134e767 | |||
b85ad9bbc2 | |||
290b3915c2 | |||
03152004d7 | |||
25c2768b8f | |||
dcdc7913c0 | |||
a299e92dfa | |||
4d68c933d1 | |||
c6cb635e01 | |||
4649a28097 | |||
6ac4aea2bf | |||
3c4b155395 | |||
bc7b0108dc | |||
73504dca41 | |||
1ea3197feb | |||
99e9f561ee | |||
b94d0281d4 | |||
10220335f7 | |||
e19b0442f8 | |||
fb7968d704 | |||
27813599a1 | |||
342ea18239 | |||
e880dd41f2 | |||
0c7351c309 | |||
5470a6d3d6 | |||
db12e5704b | |||
b754406f10 | |||
e89f6efd20 | |||
d92931853e | |||
87df94dbd4 | |||
08ebb05335 | |||
fdfaf07c46 | |||
a35df0ad7c | |||
17068c5110 | |||
6081311db5 | |||
00d1daaf1e | |||
2fd5a9863b | |||
b8eb21c027 | |||
2b623cf0fa | |||
cf4af47f7e | |||
5441c6aa54 | |||
f2d3d90b60 | |||
17459c7bfc | |||
a782a1a7d1 | |||
a4cca35e9d | |||
fd48f3f2a4 | |||
ec8f493fde | |||
dc36ae7058 | |||
4203569da2 | |||
b82d70871f | |||
b801f1affe | |||
abdb2cad15 | |||
aaffb9eb78 | |||
ff6705b94b | |||
ceab948831 | |||
7d1126fb35 | |||
9711e70980 | |||
e08df4c8d2 | |||
34380ab096 | |||
e27b80643d | |||
49cc76d33b | |||
0335422e81 | |||
8ba801ec06 | |||
ddfcb67ce3 | |||
fac38aad33 | |||
1a36b53f14 | |||
b8e59a3c6a | |||
f2ebd64a1b | |||
4eb156a324 | |||
d5988c3ec2 | |||
44ab66d858 | |||
da9956df11 | |||
24aa4d9bd9 | |||
91003cb994 | |||
517e4271e1 | |||
f0789e7349 | |||
6449387ccb | |||
98221cf6c0 | |||
6e782b0e63 | |||
fd1f46313a | |||
5b4fe8a558 | |||
ec0e9c6e6a | |||
81585716dc | |||
9cc114df36 | |||
afce2948d2 | |||
fcf50e756d | |||
df70f653a4 | |||
655efca308 | |||
29f6d8a9e6 | |||
a42d52482c | |||
1c544667ff | |||
7160b5ae26 | |||
f2e92d9140 | |||
6b03135527 | |||
b66a40495d | |||
844897360c | |||
ba851b2eca | |||
ab61a8aa9a | |||
0ad0e24a86 | |||
40c19e525c | |||
f17391a72b | |||
f786de13d0 | |||
a91d745c46 | |||
936ecd097a | |||
757f400f5d | |||
6d4c79b157 | |||
47b9b5520f | |||
de21c39ca5 | |||
023dc7cba2 | |||
38c690b155 | |||
67e57ffca4 | |||
7a63e52901 | |||
30b70e18c5 | |||
6f17fa6c90 | |||
a7ec09c877 | |||
f846c5286a | |||
27cf7747ea | |||
43acdef660 | |||
7ac3b32de6 | |||
3e2c160eed | |||
699bc50365 | |||
e04c028d64 | |||
763c276d27 | |||
8a5ab2ec06 | |||
96624b1129 | |||
35ae488120 | |||
d9cfc35bed | |||
cc8d8f2102 | |||
07648f1f25 | |||
9607665323 | |||
9bf2c2ed9d | |||
6cd4434ff3 | |||
bc5791af11 | |||
69f2d5c590 | |||
77fbd2610c | |||
1d09c25f5f | |||
cec1956b8f | |||
8e0ee1cc5e | |||
1164c4b83d | |||
21860bc017 | |||
f9c12e2053 | |||
38c074cb05 | |||
21d116d3e1 | |||
140fd6d6c4 | |||
a9af70c52b | |||
29f9372370 | |||
e7ea6a374a | |||
ff7f340bba | |||
ed57a7b561 | |||
e6e1f2ff7d | |||
e085cc4e06 | |||
2927cc6e3b | |||
03f0ed657a | |||
10c9f7389b | |||
e9b790e27b | |||
da575c46fa | |||
79b8153eac | |||
1037e7ce55 | |||
a155f0bda6 | |||
54b9c55af3 | |||
34db45a948 | |||
ccee264b7d | |||
45f56a5377 | |||
df253a2b14 | |||
5c884c7797 | |||
0c09862494 | |||
f9ef453894 | |||
b261a5edc1 | |||
30289dad5c | |||
072a21782e | |||
d31443f5a3 | |||
6edb471d58 | |||
2030ca202f | |||
1a0ad54d3e | |||
98f9ee3613 | |||
58b8610024 | |||
50c1a34f78 | |||
073411f23f | |||
dc1357afa9 | |||
0e0fc2bd24 | |||
84c2bd0b7d | |||
20776f1947 | |||
281afa74ee | |||
e18b8c12be | |||
81b5967e0a | |||
eb1dcb324c | |||
5f66b35852 | |||
c03798f99b | |||
786982d8e5 | |||
52ddd389ff | |||
40341b488c | |||
3bc4b2db12 | |||
f7f65ec464 | |||
884c702512 | |||
413b6a59ff | |||
9bd9d88a9d | |||
0feb153034 | |||
7c03704b19 | |||
04abd5603f | |||
71a1c1aa84 | |||
1ee8392a8f | |||
338ca6050e | |||
7415d53020 | |||
e5a482266f | |||
a3334eed23 | |||
bafe960dba | |||
2b39ee1bb3 | |||
c251304068 | |||
e2d01eff35 | |||
21c214ac03 | |||
2342402434 | |||
255e62dcdd | |||
bed63cddf7 | |||
e736a11ac4 | |||
3d272c2686 | |||
51e4bbfeb0 | |||
4e31bb308d | |||
8c58684fb7 | |||
bac88c047b | |||
a9c288aadc | |||
b50f96e2e1 | |||
70443adc8d | |||
24fd126822 | |||
d9b35470a1 | |||
01871e7c29 | |||
68aa114301 | |||
d519491545 | |||
7ac3afc02b | |||
961a61d708 | |||
0241b8ba9a | |||
4087fa5c7a | |||
0c1d1b7aeb | |||
b8f2db36dd | |||
6b647fd481 | |||
e5cc58c179 | |||
eaffaacf5e | |||
1a677164be | |||
a5e72258d2 | |||
f7baea7406 | |||
676b5be972 | |||
800de8e3bf | |||
44acd57ea4 | |||
07ce8bc4bc | |||
8473f3bf52 | |||
c78239a629 | |||
b33b85870d | |||
f7444ff300 | |||
7c8b1a553f | |||
6f06923e96 | |||
ac0443bc89 | |||
df259e5878 | |||
90c0db3d42 | |||
c28fef5fc4 | |||
b0d865e845 | |||
0060c0749a | |||
57ea72d3c4 | |||
9997c9488a | |||
55b4267c30 | |||
e7ad45b064 | |||
e1d5caa7e8 | |||
491362f5db | |||
946e35c958 | |||
4f330a9ba2 | |||
908d326e22 | |||
378cadf073 | |||
f4a33dd6df | |||
5c8839387d | |||
9215ebb6aa | |||
12c1300d48 | |||
4a02a1a60c | |||
91c52630b6 | |||
f3348d6e13 | |||
9a57d1067d | |||
19235c8104 | |||
e4d2b2a06a | |||
145882244f | |||
54734b0903 | |||
b094410066 | |||
adff0f3813 | |||
a3b6a646eb | |||
ffd198808e | |||
26a5aaec34 | |||
9180919a30 | |||
8cd1b3a4f2 | |||
7caa33d819 | |||
8eadc9b8a5 | |||
0461c517e4 | |||
36730ca613 | |||
138ac0b296 | |||
c7ff6d4410 | |||
4f57330e29 | |||
756b54b4c3 | |||
cbec48e8f6 | |||
8c9d7e3e93 | |||
0851a1fe7f | |||
ee78890f22 | |||
1a6e908971 | |||
769c043537 | |||
4cc39e4517 | |||
dd49b7a133 | |||
bca8f9e0ed | |||
8e6c8e068d | |||
8408acf995 | |||
f41fac0719 | |||
5ddfe18cda | |||
8dfa490e49 | |||
28102e536b | |||
08ad47fe1a | |||
2866a7488e | |||
25ee66c6c7 | |||
e30505d33b | |||
d7a289ee41 | |||
c1e7a788cd | |||
a7102d491b | |||
e393509879 | |||
e7eff1a975 | |||
8f4c615704 | |||
f1856abe60 | |||
efe2141c16 | |||
d844377ca6 | |||
172c1eae5d | |||
99c7371337 | |||
a4c8bfa7b4 | |||
dae165eeaa | |||
16d337db70 | |||
b98cf17209 | |||
9e77d180c4 | |||
3ee83bc194 | |||
5a62df3691 | |||
3aa90b32f9 | |||
10b73418cf | |||
46a7a61b7d | |||
814558306e | |||
215662188b | |||
077f57d1c6 | |||
8d9b7b1680 | |||
8463421448 | |||
ade71208a2 | |||
bdb954b2f5 | |||
70580de197 | |||
3c77693881 | |||
966cfd6e8e | |||
01be21985d | |||
2ba57ee75d | |||
03174c9361 | |||
146e447cea | |||
f2d200a826 | |||
db8e4a2fc0 | |||
cf91035a36 | |||
576e26ea39 | |||
d218034630 | |||
fa11eef6d0 | |||
bb185a858f | |||
b796d227f1 | |||
06dea4830d | |||
dcd64f2a59 | |||
728690fa03 | |||
8f3544ece8 | |||
cb7b321240 | |||
c87f1d3924 | |||
24afe8d22b | |||
9cdb7f073d | |||
51d3dc7f6b | |||
22c8ec0a80 | |||
fc9fc3f888 | |||
6d720fb33f | |||
a0922ebd0f | |||
6d7acc6b1e | |||
56c6fab53c | |||
2b260a7ae5 | |||
2d9553cb18 | |||
68968c2cb0 | |||
f4613dd466 | |||
ba7d174349 | |||
2a212c9016 | |||
e9bb7c26fa | |||
97ee4dc847 | |||
f8be54b416 | |||
17e56a1c57 | |||
7469871d20 | |||
c84a25e433 | |||
c7cb8275c3 | |||
e8e588c67b | |||
59115b85f7 | |||
163b27c759 | |||
05b2d76d54 | |||
07058d3e0b | |||
ddd219f297 | |||
1eb2512961 | |||
38e8f3b764 | |||
c4defbc45c | |||
8c3450e200 | |||
665af71888 | |||
cd3f047ccd | |||
7638acdf37 | |||
735647e6a3 | |||
8cffd75e00 | |||
b2e0836bb3 | |||
2d3cef2496 | |||
78d7b38a17 | |||
e79e9c4853 | |||
5585984ed7 | |||
07c997f98c | |||
30fa119345 | |||
d2b99aa7c9 | |||
f4d8c3fc66 | |||
225e618b8f | |||
3dd6280df5 | |||
c98f1cb501 | |||
021e231476 | |||
432bc74a58 | |||
008337e150 | |||
edac2e909b | |||
91b62c0fbf | |||
ad307c6965 | |||
7486d3d4c5 | |||
481e229ad4 | |||
f87a6f3c1f | |||
4aa15294a8 | |||
134a962222 | |||
15bb84d320 | |||
5e91b3b716 | |||
9acb4cf2b0 | |||
c4b6896338 | |||
5abdfda06a | |||
dd7aa95379 | |||
75dce35a5d | |||
72c65e74f5 | |||
5deaada0dc | |||
9616162dbc | |||
022a663e91 | |||
a8c123c6c2 | |||
79de1dc339 | |||
3c483e19d7 | |||
1b37f313a6 | |||
0f50188652 | |||
3823b74208 | |||
1d40440830 | |||
ea66d94273 | |||
61a413c219 | |||
10eb997621 | |||
c312d6efad | |||
92bdb1390d | |||
ff14d1de52 | |||
6e6d81f094 | |||
71ed92b6bd | |||
f1f8fc4228 | |||
40ed560e16 | |||
c7e17e2f9e | |||
220a923a2d | |||
0453d09af6 | |||
f03c3bce05 | |||
e17f79ee70 | |||
8d37587e47 | |||
ce7536e564 | |||
e2d8e1868b | |||
f9c4ba3ea3 | |||
85103adfe0 | |||
3699f2e5f9 | |||
4c116a5a01 | |||
f6a9599eb4 | |||
2ff75cda0e | |||
ebafba0043 | |||
fab17f216d | |||
27c9a0535c | |||
5fdc124578 | |||
a28dc4559b | |||
63489b9ef5 | |||
442cae6844 | |||
4296cd3fa4 | |||
f891199ab0 | |||
f664c02a40 | |||
338f120cbf | |||
aff5b75487 | |||
71d12d3330 | |||
3b2a90e775 | |||
f7417cf693 | |||
f7580cd3b6 | |||
12a9b84b2b | |||
94521738a9 | |||
c1da78601a | |||
a5df254e53 | |||
c1c2aeffab | |||
cc1df691cc | |||
e9a22d0f34 | |||
33465a7e42 | |||
bb95187bc7 | |||
cce88a8504 | |||
6e9947599a | |||
14a6584a22 | |||
0ba26afb4d | |||
d71865e094 | |||
8e2557697f | |||
3229c91dbb | |||
0881021e54 | |||
a5eec89113 | |||
08b370ebe4 | |||
3653287c97 | |||
9203f68894 | |||
5574b6e224 | |||
41ccf13393 | |||
18c4be469f | |||
6f3162af69 | |||
b4fd9353cf | |||
a2b9f9310c | |||
1039f3dcea | |||
d77773acb3 | |||
afc69b6a74 | |||
ecc0f97e27 | |||
e2798405f6 | |||
7b2474681b | |||
0ecbe891b5 | |||
68b3644ac7 | |||
7aa0da7c91 | |||
902f1864b1 | |||
45dc778fbb | |||
0f1e18f8f0 | |||
65f7833c22 | |||
91daf1da86 | |||
7da180cfc5 | |||
e9a45ae35d | |||
1805c15093 | |||
622ee60d4f | |||
cc1b4b7ef0 | |||
d9ff56d7b7 | |||
3af8c7da3c | |||
55c4a3307d | |||
20c995c142 | |||
d8424a15dd | |||
b5b2031d5b | |||
c19932c9ba | |||
f911196e5f | |||
c9edb762b7 | |||
394893ff92 | |||
01fdaea8a6 | |||
434bf8ca81 | |||
b1426e2635 | |||
659eb5fd2a | |||
d0dc7427dd | |||
8e06333d45 | |||
b9e7c20a4c | |||
d6c3ebb1a0 | |||
a191df10a3 | |||
cd4b35c841 | |||
5ffc0adccc | |||
3529c381cf | |||
4b3c3203ff | |||
4f10917ce9 | |||
e587402c26 | |||
431ff3cce1 | |||
7c3e202c94 | |||
15029381e1 | |||
01c40fcf50 | |||
ba63cf666d | |||
6030261363 | |||
943fede19c | |||
2d870fa65b | |||
969b529b08 | |||
38ec659cd6 | |||
193679e041 | |||
7bb6230588 | |||
069a288a59 | |||
611d564865 | |||
43c9ca895b | |||
54b4f52e48 | |||
46eab903e9 | |||
0197ce4c66 | |||
215820dd40 | |||
a0fbc289ec | |||
5283002132 | |||
28634fce47 | |||
2b0936271c | |||
01322cd243 | |||
21fb7b4fbb | |||
efaef49734 | |||
ffa5eb08c3 | |||
a6132d459f | |||
12dd380d26 | |||
653a63fa00 | |||
88e5bce63d | |||
d70df4a15d | |||
a25cd45876 | |||
b383cd5acf | |||
9f5c7b310c | |||
9c6f2ed5bb | |||
5baefcce26 | |||
363e952551 | |||
3c7935a21d | |||
2bd74bc328 | |||
d1cdc02afc | |||
7b180d585e | |||
e72e75876c | |||
3fba10c8e6 | |||
e850c644da | |||
311db876b0 | |||
609e13a240 | |||
05e77ecf90 | |||
e7bc7becf3 | |||
1288e1f39d | |||
c7d1beaaa5 | |||
d7eef6a64e | |||
b49cbc959b | |||
b652a0d232 | |||
93104f114e | |||
6534525cf8 | |||
c952e91c4f | |||
2665cc1cc8 | |||
7aa9838d8d | |||
0c26c0bfab | |||
1be8550672 | |||
061fad12a9 | |||
30099d9135 | |||
8111930981 | |||
155bd09902 | |||
13ec81c87f | |||
bb328d5aa5 | |||
468c345e74 | |||
060de128a7 | |||
9793c518ab | |||
79689872af | |||
daa49023cf | |||
b4cf146a52 | |||
0429fe04df | |||
5bfaaa7964 | |||
0b77b42cad | |||
92778afd0b | |||
2f51735e8a | |||
7e7cfb1ce8 | |||
9ffc0b9f2c | |||
2e59635bea | |||
6dc49def25 | |||
d120962959 | |||
1c9928d721 | |||
e709f1b572 | |||
6059db1f4b | |||
6d46fc39aa | |||
5d498918bf | |||
4c324fe3a4 | |||
6d81aabd48 | |||
447f6a16cc | |||
d198173fd7 | |||
92cc288f6e | |||
e02baf33c9 | |||
e0e8495ace | |||
9756dba57a | |||
9746de91bf | |||
46974ef473 | |||
7bf3e062bd | |||
6f481af383 | |||
a922947bb0 | |||
a50857d38a | |||
da19964959 | |||
8ea840c19a | |||
7adb765660 | |||
b8b5734689 | |||
d2a553f6c4 | |||
5f9a5e6a5d | |||
18001dd779 | |||
404dc96645 | |||
a274e5b192 | |||
429b9487f7 | |||
a2c5c844a0 | |||
54e39a30f7 | |||
f21842cd04 | |||
bea28933d3 | |||
0ccd09532b | |||
638f36956b | |||
16e9aa77e3 | |||
ee2d5d66af | |||
984f309815 | |||
02922fa7a5 | |||
a0c0638744 | |||
60c8dbe0c9 | |||
83e1fe77c8 | |||
3ea913e76a | |||
b951aaf925 | |||
2e86cf2dc8 | |||
9c8aff66a1 | |||
3921295b21 | |||
605c4ce702 | |||
a69e416604 | |||
7b11dc1c05 | |||
5562c3b4ec | |||
5af8fe9a84 | |||
041524432d | |||
f34b77216f | |||
5fc5681cb4 | |||
98fdbaaae0 | |||
f1853b4364 | |||
690edb2c56 | |||
edd8d7e534 | |||
f95f53e446 | |||
447d7dc51b | |||
8ccb8b1f9f | |||
44836d9099 | |||
b8d71dfe70 | |||
02ced2c2d7 | |||
8d758be3e4 | |||
042a8e3d4f | |||
00935c873f | |||
2ff3ce74c5 | |||
0886e0ddf4 | |||
30f4d9faea | |||
e11c7f35b4 | |||
284e76f0da | |||
3f435571d3 | |||
18d95b336f | |||
c4e7432ef9 | |||
45c6bf80e1 | |||
4181f1b2e1 | |||
c3f8eabac3 | |||
10b2f88b83 | |||
9a59f16964 | |||
f534d6c8f6 | |||
6fb2c7c883 | |||
6234164f28 | |||
ac44e56ea0 | |||
5e486dd912 | |||
a88f077348 | |||
77233b26d0 | |||
02c854717b | |||
60b1f2f437 | |||
fc35324ba7 | |||
2af0ad505a | |||
5651272ec8 | |||
2a11c1487c | |||
62a90e77b3 | |||
064004b899 | |||
6a232dfc13 | |||
7be945f59b | |||
8eac28350d | |||
c86f484712 | |||
7dc02b947d | |||
095251f1fa | |||
62bdcf6f49 | |||
e9cb510ef5 | |||
5778c49689 | |||
67808f8db1 | |||
6044b1a0d7 | |||
f387e3e27d | |||
3817661f82 | |||
9f315ffe10 | |||
584186c7ff | |||
dd94d5d4e8 | |||
15a8b46359 | |||
e5b9e22518 | |||
a10461f60d | |||
8344303b1a | |||
cb2095cddc | |||
cc3a8e26c8 | |||
3a85d97fd9 | |||
6e95448ad7 | |||
7e27d588ff | |||
f8a3ac9338 | |||
d12b2c39dd | |||
78bbb37018 | |||
fb1ca245a7 | |||
8c12d6d00f | |||
96059a496a | |||
abd2448931 | |||
c24b6b4150 | |||
7cdd148e24 | |||
1a75beb57c | |||
b5f887f5d2 | |||
5cad4e595c | |||
193756fa38 | |||
961518c893 | |||
989f41477d | |||
8ce6b94e05 | |||
d293c4915c | |||
c03fbf68d6 | |||
853a458a0d | |||
f9c299da8b | |||
c7e358922b | |||
5d710c0f7a | |||
12572e5412 | |||
920b80c41f | |||
8111d4fbb8 | |||
a6a63d116f | |||
28f87c2a43 | |||
4d22ff90d5 | |||
a9c81088f8 | |||
c32d34166e | |||
6fa74b0e33 | |||
09d1575eeb | |||
eff3aadba1 | |||
167ef7e8b0 | |||
9bb7265d64 | |||
88674a623a | |||
b3de2b3450 | |||
59a720d8be | |||
828a8cf326 | |||
93b08502e4 | |||
c1c45575be | |||
1170c21f89 | |||
2066ff5acb | |||
0cdd1b58a4 | |||
9cd3b2153f | |||
a06729a96a | |||
888ddacd3c | |||
2ef9498d6f | |||
1f0e13e956 | |||
bee9d8bea5 | |||
b70be19653 | |||
092461d7c8 | |||
8f3d109c18 | |||
4c609ec59c | |||
cb7b75c15f | |||
74737b76cc | |||
c223eca938 | |||
5b052e1e10 | |||
ab79550693 | |||
73e3394d2d | |||
8ddcd9799d | |||
6e0a668455 | |||
9545662c6b | |||
50e0db4038 | |||
8d7be33dd8 | |||
f6f4329899 | |||
935320289e | |||
8353340697 | |||
45500c5e7b | |||
8942415933 | |||
fcc7a42d6c | |||
13012ddd9a | |||
706c6df2ce | |||
0ac49ba58d | |||
b06c0cc3ec | |||
895d80d0e1 | |||
0ca153e1e5 | |||
9f8ede7b03 | |||
0a2384bf4d | |||
be0bb56525 | |||
6e70dfc33a | |||
0e1f0a734b | |||
2b6e45d0ee | |||
07217b8d7c | |||
6ea8da077e | |||
ab4bcc1869 | |||
0e53287ea2 | |||
3fc68f00f0 | |||
4a04a89cb4 | |||
b3970c8db1 | |||
36c57b7717 | |||
8f629aae4b | |||
dead5e0eef | |||
fa156aaf53 | |||
e491c16afa | |||
95fe075a17 | |||
e698c378e7 | |||
b083766608 | |||
82db1e7919 | |||
7c7a70202b | |||
f734ba9974 | |||
511ac3280f | |||
a32cd31d3b | |||
186879cce0 | |||
dd9aa95f1f | |||
616db55e08 | |||
b52d4cdbb2 | |||
d99567f698 | |||
629c578cea | |||
95d295da54 | |||
97a1ca1ad3 | |||
c9db87a302 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
.git
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,6 +1,11 @@
|
|||||||
|
/coverage
|
||||||
/gopath
|
/gopath
|
||||||
/go-bindata
|
/go-bindata
|
||||||
/machine*
|
/machine*
|
||||||
/bin
|
/bin
|
||||||
.vagrant
|
.vagrant
|
||||||
*.etcd
|
*.etcd
|
||||||
|
/etcd
|
||||||
|
*.swp
|
||||||
|
/hack/insta-discovery/.env
|
||||||
|
*.test
|
||||||
|
10
.travis.yml
Normal file
10
.travis.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- 1.3
|
||||||
|
|
||||||
|
install:
|
||||||
|
- go get code.google.com/p/go.tools/cmd/cover
|
||||||
|
- go get code.google.com/p/go.tools/cmd/vet
|
||||||
|
|
||||||
|
script:
|
||||||
|
- ./test
|
@ -21,12 +21,13 @@ This is a rough outline of what a contributor's workflow looks like:
|
|||||||
- Make sure your commit messages are in the proper format (see below).
|
- 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.
|
- Push your changes to a topic branch in your fork of the repository.
|
||||||
- Submit a pull request to coreos/etcd.
|
- Submit a pull request to coreos/etcd.
|
||||||
|
- Your PR must receive a LGTM from two maintainers found in the MAINTAINERS file.
|
||||||
|
|
||||||
Thanks for your contributions!
|
Thanks for your contributions!
|
||||||
|
|
||||||
### Code style
|
### Code style
|
||||||
|
|
||||||
The coding style suggested by the Golang community is used in etcd. See [style doc](https://code.google.com/p/go-wiki/wiki/Style) for details.
|
The coding style suggested by the Golang community is used in etcd. See the [style doc](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) for details.
|
||||||
|
|
||||||
Please follow this style to make etcd easy to review, maintain and develop.
|
Please follow this style to make etcd easy to review, maintain and develop.
|
||||||
|
|
||||||
|
14
Dockerfile
14
Dockerfile
@ -1,12 +1,2 @@
|
|||||||
FROM ubuntu:12.04
|
FROM golang:onbuild
|
||||||
# Let's install go just like Docker (from source).
|
EXPOSE 4001 7001 2379 2380
|
||||||
RUN apt-get update -q
|
|
||||||
RUN DEBIAN_FRONTEND=noninteractive apt-get install -qy build-essential curl git
|
|
||||||
RUN curl -s https://storage.googleapis.com/golang/go1.3.src.tar.gz | tar -v -C /usr/local -xz
|
|
||||||
RUN cd /usr/local/go/src && ./make.bash --no-clean 2>&1
|
|
||||||
ENV PATH /usr/local/go/bin:$PATH
|
|
||||||
ADD . /opt/etcd
|
|
||||||
RUN cd /opt/etcd && ./build
|
|
||||||
EXPOSE 4001 7001
|
|
||||||
ENTRYPOINT ["/opt/etcd/bin/etcd"]
|
|
||||||
|
|
||||||
|
79
Documentation/0.5/admin_guide.md
Normal file
79
Documentation/0.5/admin_guide.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
## Administration
|
||||||
|
|
||||||
|
### Data Directory
|
||||||
|
|
||||||
|
#### Lifecycle
|
||||||
|
|
||||||
|
When first started, etcd stores its configuration into a data directory specified by the data-dir configuration parameter.
|
||||||
|
Configuration is stored in the write ahead log and includes: the local member ID, cluster ID, and initial cluster configuration.
|
||||||
|
The write ahead log and snapshot files are used during member operation and to recover after a restart.
|
||||||
|
|
||||||
|
If a member’s data directory is ever lost or corrupted then the user should remove the etcd member from the cluster via the [members API][members-api].
|
||||||
|
|
||||||
|
A user should avoid restarting an etcd member with a data directory from an out-of-date backup.
|
||||||
|
Using an out-of-date data directory can lead to inconsistency as the member had agreed to store information via raft then re-joins saying it needs that information again.
|
||||||
|
For maximum safety, if an etcd member suffers any sort of data corruption or loss, it must be removed from the cluster.
|
||||||
|
Once removed the member can be re-added with an empty data directory.
|
||||||
|
|
||||||
|
[members-api]: https://github.com/coreos/etcd/blob/master/Documentation/0.5/other_apis.md#members-api
|
||||||
|
|
||||||
|
#### Contents
|
||||||
|
|
||||||
|
The data directory has two sub-directories in it:
|
||||||
|
|
||||||
|
1. wal: write ahead log files are stored here. For details see the [wal package documentation][wal-pkg]
|
||||||
|
2. snap: log snapshots are stored here. For details see the [snap package documentation][snap-pkg]
|
||||||
|
|
||||||
|
[wal-pkg]: http://godoc.org/github.com/coreos/etcd/wal
|
||||||
|
[snap-pkg]: http://godoc.org/github.com/coreos/etcd/snap
|
||||||
|
|
||||||
|
### Cluster Lifecycle
|
||||||
|
|
||||||
|
If you are spinning up multiple clusters for testing it is recommended that you specify a unique initial-cluster-token for the different clusters.
|
||||||
|
This can protect you from cluster corruption in case of mis-configuration because two members started with different cluster tokens will refuse members from each other.
|
||||||
|
|
||||||
|
### Disaster Recovery
|
||||||
|
|
||||||
|
etcd is designed to be resilient to machine failures. An etcd cluster can automatically recover from any number of temporary failures (for example, machine reboots), and a cluster of N members can tolerate up to _(N/2)-1_ permanent failures (where a member can no longer access the cluster, due to hardware failure or disk corruption). However, in extreme circumstances, a cluster might permanently lose enough members such that quorum is irrevocably lost. For example, if a three-node cluster suffered two simultaneous and unrecoverable machine failures, it would be normally impossible for the cluster to restore quorum and continue functioning.
|
||||||
|
|
||||||
|
To recover from such scenarios, etcd provides functionality to backup and restore the datastore and recreate the cluster without data loss.
|
||||||
|
|
||||||
|
#### Backing up the datastore
|
||||||
|
|
||||||
|
The first step of the recovery is to backup the data directory on a functioning etcd node. To do this, use the `etcdctl backup` command, passing in the original data directory used by etcd. For example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
etcdctl backup \
|
||||||
|
--data-dir /var/lib/etcd \
|
||||||
|
--backup-dir /tmp/etcd_backup
|
||||||
|
```
|
||||||
|
|
||||||
|
This command will rewrite some of the metadata contained in the backup (specifically, the node ID and cluster ID), which means that the node will lose its former identity. In order to recreate a cluster from the backup, you will need to start a new, single-node cluster. The metadata is rewritten to prevent the new node from inadvertently being joined onto an existing cluster.
|
||||||
|
|
||||||
|
#### Restoring a backup
|
||||||
|
|
||||||
|
To restore a backup using the procedure created above, start etcd with the `-force-new-cluster` option and pointing to the backup directory. This will initialize a new, single-member cluster with the default advertised peer URLs, but preserve the entire contents of the etcd data store. Continuing from the previous example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
etcd \
|
||||||
|
-data-dir=/tmp/etcd_backup \
|
||||||
|
-force-new-cluster \
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Now etcd should be available on this node and serving the original datastore.
|
||||||
|
|
||||||
|
Once you have verified that etcd has started successfully, shut it down and move the data back to the previous location (you may wish to make another copy as well to be safe):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pkill etcd
|
||||||
|
rm -fr /var/lib/etcd
|
||||||
|
mv /tmp/etcd_backup /var/lib/etcd
|
||||||
|
etcd \
|
||||||
|
-data-dir=/var/lib/etcd \
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Restoring the cluster
|
||||||
|
|
||||||
|
Now that the node is running successfully, you can add more nodes to the cluster and restore resiliency. See the [runtime configuration](runtime-configuration.md) guide for more details.
|
1010
Documentation/0.5/api.md
Normal file
1010
Documentation/0.5/api.md
Normal file
File diff suppressed because it is too large
Load Diff
205
Documentation/0.5/clustering.md
Normal file
205
Documentation/0.5/clustering.md
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
# Clustering Guide
|
||||||
|
|
||||||
|
This guide will walk you through configuring a three machine etcd cluster with the following details:
|
||||||
|
|
||||||
|
|Name |Address |
|
||||||
|
|-------|-----------|
|
||||||
|
|infra0 |10.0.1.10 |
|
||||||
|
|infra1 |10.0.1.11 |
|
||||||
|
|infra2 |10.0.1.12 |
|
||||||
|
|
||||||
|
## Static
|
||||||
|
|
||||||
|
As we know the cluster members, their addresses and the size of the cluster before starting we can use an offline bootstrap configuration. Each machine will get either the following command line or environment variables:
|
||||||
|
|
||||||
|
```
|
||||||
|
ETCD_INITIAL_CLUSTER="infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380"
|
||||||
|
ETCD_INITIAL_CLUSTER_STATE=new
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
-initial-cluster infra0=http://10.0.1.10:2380,http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
|
||||||
|
-initial-cluster-state new
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are spinning up multiple clusters (or creating and destroying a single cluster) with same configuration for testing purpose, it is highly recommended that you specify a unique `initial-cluster-token` for the different clusters. By doing this, etcd can generate unique cluster IDs and member IDs for the clusters even if they otherwise have the exact same configuration. This can protect you from cross-cluster-interaction, which might corrupt your clusters.
|
||||||
|
|
||||||
|
On each machine you would start etcd with these flags:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ etcd -name infra0 -initial-advertise-peer-urls https://10.0.1.10:2380 \
|
||||||
|
-initial-cluster-token etcd-cluster-1 \
|
||||||
|
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
|
||||||
|
-initial-cluster-state new
|
||||||
|
```
|
||||||
|
```
|
||||||
|
$ etcd -name infra1 -initial-advertise-peer-urls https://10.0.1.11:2380 \
|
||||||
|
-initial-cluster-token etcd-cluster-1 \
|
||||||
|
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
|
||||||
|
-initial-cluster-state new
|
||||||
|
```
|
||||||
|
```
|
||||||
|
$ etcd -name infra2 -initial-advertise-peer-urls https://10.0.1.12:2380 \
|
||||||
|
-initial-cluster-token etcd-cluster-1 \
|
||||||
|
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
|
||||||
|
-initial-cluster-state new
|
||||||
|
```
|
||||||
|
|
||||||
|
The command line parameters starting with `-initial-cluster` will be ignored on subsequent runs of etcd. You are free to remove the environment variables or command line flags after the initial bootstrap process. If you need to make changes to the configuration later see our guide on [runtime configuration](runtime-configuration.md).
|
||||||
|
|
||||||
|
### Error Cases
|
||||||
|
|
||||||
|
In the following case we have not included our new host in the list of enumerated nodes. If this is a new cluster, the node must be added to the list of initial cluster members.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ etcd -name infra1 -initial-advertise-peer-urls http://10.0.1.11:2380 \
|
||||||
|
-initial-cluster infra0=http://10.0.1.10:2380 \
|
||||||
|
-initial-cluster-state new
|
||||||
|
etcd: infra1 not listed in the initial cluster config
|
||||||
|
exit 1
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case we are attempting to map a node (infra0) on a different address (127.0.0.1:2380) than its enumerated address in the cluster list (10.0.1.10:2380). If this node is to listen on multiple addresses, all addresses must be reflected in the "initial-cluster" configuration directive.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ etcd -name infra0 -initial-advertise-peer-urls http://127.0.0.1:2380 \
|
||||||
|
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
|
||||||
|
-initial-cluster-state=new
|
||||||
|
etcd: infra0 has different advertised URLs in the cluster and advertised peer URLs list
|
||||||
|
exit 1
|
||||||
|
```
|
||||||
|
|
||||||
|
If you configure a peer with a different set of configuration and attempt to join this cluster you will get a cluster ID mismatch and etcd will exit.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ etcd -name infra3 -initial-advertise-peer-urls http://10.0.1.13:2380 \
|
||||||
|
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra3=http://10.0.1.13:2380 \
|
||||||
|
-initial-cluster-state=new
|
||||||
|
etcd: conflicting cluster ID to the target cluster (c6ab534d07e8fcc4 != bc25ea2a74fb18b0). Exiting.
|
||||||
|
exit 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Discovery
|
||||||
|
|
||||||
|
In a number of cases you might not know the IPs of your cluster peers ahead of time. This is common when utilizing cloud providers or when your network uses DHCP. In these cases you can use an existing etcd cluster to bootstrap a new one. We call this process "discovery".
|
||||||
|
|
||||||
|
### Lifetime of a Discovery URL
|
||||||
|
|
||||||
|
A discovery URL identifies a unique etcd cluster. Instead of reusing a discovery URL, you should always create discovery URLs for new clusters.
|
||||||
|
|
||||||
|
Moreover, discovery URLs should ONLY be used for the initial bootstrapping of a cluster. To change cluster membership after the cluster is already running, see the [runtime reconfiguration][runtime] guide.
|
||||||
|
|
||||||
|
[runtime]: https://github.com/coreos/etcd/blob/master/Documentation/0.5/runtime-configuration.md
|
||||||
|
|
||||||
|
### Custom etcd discovery service
|
||||||
|
|
||||||
|
Discovery uses an existing cluster to bootstrap itself. If you are using your own etcd cluster you can create a URL like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl -X PUT https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83/_config/size -d value=3
|
||||||
|
```
|
||||||
|
|
||||||
|
By setting the size key to the URL, you create a discovery URL with expected-cluster-size of 3.
|
||||||
|
|
||||||
|
The URL you will use in this case will be `https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83` and the etcd members will use the `https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83` directory for registration as they start.
|
||||||
|
|
||||||
|
Now we start etcd with those relevant flags for each member:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \
|
||||||
|
-discovery https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
|
||||||
|
```
|
||||||
|
```
|
||||||
|
$ etcd -name infra1 -initial-advertise-peer-urls http://10.0.1.11:2380 \
|
||||||
|
-discovery https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
|
||||||
|
```
|
||||||
|
```
|
||||||
|
$ etcd -name infra2 -initial-advertise-peer-urls http://10.0.1.12:2380 \
|
||||||
|
-discovery https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
|
||||||
|
```
|
||||||
|
|
||||||
|
This will cause each member to register itself with the custom etcd discovery service and begin the cluster once all machines have been registered.
|
||||||
|
|
||||||
|
### Public discovery service
|
||||||
|
|
||||||
|
If you do not have access to an existing cluster you can use the public discovery service hosted at discovery.etcd.io. You can create a private discovery URL using the "new" endpoint like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl https://discovery.etcd.io/new?size=3
|
||||||
|
https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create the cluster with an initial expected size of 3 members. If you do not specify a size a default of 3 will be used.
|
||||||
|
|
||||||
|
```
|
||||||
|
ETCD_DISCOVERY=https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we start etcd with those relevant flags for each member:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \
|
||||||
|
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||||
|
```
|
||||||
|
```
|
||||||
|
$ etcd -name infra1 -initial-advertise-peer-urls http://10.0.1.11:2380 \
|
||||||
|
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||||
|
```
|
||||||
|
```
|
||||||
|
$ etcd -name infra2 -initial-advertise-peer-urls http://10.0.1.12:2380 \
|
||||||
|
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||||
|
```
|
||||||
|
|
||||||
|
This will cause each member to register itself with the discovery service and begin the cluster once all members have been registered.
|
||||||
|
|
||||||
|
You can use the environment variable `ETCD_DISCOVERY_PROXY` to cause etcd to use an HTTP proxy to connect to the discovery service.
|
||||||
|
|
||||||
|
### Error and Warning Cases
|
||||||
|
|
||||||
|
#### Discovery Server Errors
|
||||||
|
|
||||||
|
```
|
||||||
|
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \
|
||||||
|
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||||
|
etcd: error: the cluster doesn’t have a size configuration value in https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de/_config
|
||||||
|
exit 1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### User Errors
|
||||||
|
|
||||||
|
```
|
||||||
|
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \
|
||||||
|
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||||
|
etcd: error: the cluster using discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de has already started with all 5 members
|
||||||
|
exit 1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Warnings
|
||||||
|
|
||||||
|
This is a harmless warning notifying you that the discovery URL will be
|
||||||
|
ignored on this machine.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \
|
||||||
|
-discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||||
|
etcd: warn: ignoring discovery URL: etcd has already been initialized and has a valid log in /var/lib/etcd
|
||||||
|
```
|
||||||
|
|
||||||
|
# 0.4 to 0.5+ Migration Guide
|
||||||
|
|
||||||
|
In etcd 0.5 we introduced the ability to listen on more than one address and to advertise multiple addresses. This makes using etcd easier when you have complex networking, such as private and public networks on various cloud providers.
|
||||||
|
|
||||||
|
To make understanding this feature easier, we changed the naming of some flags, but we support the old flags to make the migration from the old to new version easier.
|
||||||
|
|
||||||
|
|Old Flag |New Flag |Migration Behavior |
|
||||||
|
|-----------------------|-----------------------|---------------------------------------------------------------------------------------|
|
||||||
|
|-peer-addr |-initial-advertise-peer-urls |If specified, peer-addr will be used as the only peer URL. Error if both flags specified.|
|
||||||
|
|-addr |-advertise-client-urls |If specified, addr will be used as the only client URL. Error if both flags specified.|
|
||||||
|
|-peer-bind-addr |-listen-peer-urls |If specified, peer-bind-addr will be used as the only peer bind URL. Error if both flags specified.|
|
||||||
|
|-bind-addr |-listen-client-urls |If specified, bind-addr will be used as the only client bind URL. Error if both flags specified.|
|
||||||
|
|-peers |none |Deprecated. The -initial-cluster flag provides a similar concept with different semantics. Please read this guide on cluster startup.|
|
||||||
|
|-peers-file |none |Deprecated. The -initial-cluster flag provides a similar concept with different semantics. Please read this guide on cluster startup.|
|
31
Documentation/0.5/glossary.md
Normal file
31
Documentation/0.5/glossary.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
## Glossary
|
||||||
|
|
||||||
|
This document defines the various terms used in etcd documentation, command line and source code.
|
||||||
|
|
||||||
|
### Node
|
||||||
|
|
||||||
|
Node is an instance of raft state machine.
|
||||||
|
|
||||||
|
It has a unique identification, and records other nodes' progress internally when it is the leader.
|
||||||
|
|
||||||
|
### Member
|
||||||
|
|
||||||
|
Member is an instance of etcd. It hosts a node, and provides service to clients.
|
||||||
|
|
||||||
|
### Cluster
|
||||||
|
|
||||||
|
Cluster consists of several members.
|
||||||
|
|
||||||
|
The node in each member follows raft consensus protocol to replicate logs. Cluster receives proposals from members, commits them and apply to local store.
|
||||||
|
|
||||||
|
### Peer
|
||||||
|
|
||||||
|
Peer is another member of the same cluster.
|
||||||
|
|
||||||
|
### Client
|
||||||
|
|
||||||
|
Client is a caller of the cluster's HTTP API.
|
||||||
|
|
||||||
|
### Machine (deprecated)
|
||||||
|
|
||||||
|
The alternative of Member in etcd before 0.5
|
96
Documentation/0.5/other_apis.md
Normal file
96
Documentation/0.5/other_apis.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
## Members API
|
||||||
|
|
||||||
|
* [List members](#list-members)
|
||||||
|
* [Add a member](#add-a-member)
|
||||||
|
* [Delete a member](#delete-a-member)
|
||||||
|
|
||||||
|
## List members
|
||||||
|
|
||||||
|
Return an HTTP 200 OK response code and a representation of all members in the etcd cluster.
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /v2/members HTTP/1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```
|
||||||
|
curl http://10.0.0.10:2379/v2/members
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"id": "272e204152",
|
||||||
|
"name": "infra1",
|
||||||
|
"peerURLs": [
|
||||||
|
"http://10.0.0.10:2380"
|
||||||
|
],
|
||||||
|
"clientURLs": [
|
||||||
|
"http://10.0.0.10:2379"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2225373f43",
|
||||||
|
"name": "infra2",
|
||||||
|
"peerURLs": [
|
||||||
|
"http://10.0.0.11:2380"
|
||||||
|
],
|
||||||
|
"clientURLs": [
|
||||||
|
"http://10.0.0.11:2379"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Add a member
|
||||||
|
|
||||||
|
Returns an HTTP 201 response code and the representation of added member with a newly generated a memberID when successful. Returns a string describing the failure condition when unsuccessful.
|
||||||
|
|
||||||
|
If the POST body is malformed an HTTP 400 will be returned. If the member exists in the cluster or existed in the cluster at some point in the past an HTTP 409 will be returned. If any of the given peerURLs exists in the cluster an HTTP 409 will be returned. If the cluster fails to process the request within timeout an HTTP 500 will be returned, though the request may be processed later.
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /v2/members HTTP/1.1
|
||||||
|
|
||||||
|
{"peerURLs": ["http://10.0.0.10:2379"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```
|
||||||
|
curl http://10.0.0.10:2379/v2/members -XPOST -H "Content-Type: application/json" -d '{"peerURLs":["http://10.0.0.10:2379"]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "3777296169",
|
||||||
|
"peerURLs": [
|
||||||
|
"http://10.0.0.10:2379"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Delete a member
|
||||||
|
|
||||||
|
Remove a member from the cluster. The member ID must be a hex-encoded uint64.
|
||||||
|
Returns empty when successful. Returns a string describing the failure condition when unsuccessful.
|
||||||
|
|
||||||
|
If the member does not exist in the cluster an HTTP 500(TODO: fix this) will be returned. If the cluster fails to process the request within timeout an HTTP 500 will be returned, though the request may be processed later.
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE /v2/members/<id> HTTP/1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```
|
||||||
|
curl http://10.0.0.10:2379/v2/members/272e204152 -XDELETE
|
||||||
|
```
|
143
Documentation/0.5/runtime-configuration.md
Normal file
143
Documentation/0.5/runtime-configuration.md
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
## Runtime Reconfiguration
|
||||||
|
|
||||||
|
etcd comes with support for incremental runtime reconfiguration, which allows users to update the membership of the cluster at run time.
|
||||||
|
|
||||||
|
## Reconfiguration Use Cases
|
||||||
|
|
||||||
|
Let us walk through the four use cases for re-configuring a cluster: replacing a member, increasing or decreasing cluster size, and restarting a cluster from a majority failure.
|
||||||
|
|
||||||
|
### Replace a Member
|
||||||
|
|
||||||
|
The most common use case of cluster reconfiguration is to replace a member because of a permanent failure of the existing member: for example, hardware failure, loss of network address, or data directory corruption.
|
||||||
|
It is important to replace failed members as soon as the failure is detected.
|
||||||
|
If etcd falls below a simple majority of members it can no longer accept writes: e.g. in a 3 member cluster the loss of two members will cause writes to fail and the cluster to stop operating.
|
||||||
|
|
||||||
|
### Increase Cluster Size
|
||||||
|
|
||||||
|
To make your cluster more resilient to machine failure you can increase the size of the cluster.
|
||||||
|
For example, if the cluster consists of three machines, it can tolerate one failure.
|
||||||
|
If we increase the cluster size to five, it can tolerate two machine failures.
|
||||||
|
|
||||||
|
Increasing the cluster size can also provide better read performance.
|
||||||
|
When a client accesses etcd, the normal read gets the data from the local copy of each member (members always shares the same view of the cluster at the same index, which is guaranteed by the sequential consistency of etcd).
|
||||||
|
Since clients can read from any member, increasing the number of members thus increases overall read throughput.
|
||||||
|
|
||||||
|
### Decrease Cluster Size
|
||||||
|
|
||||||
|
To improve the write performance of a cluster, you might want to trade off resilience by removing members.
|
||||||
|
etcd replicates the data to the majority of members of the cluster before committing the write.
|
||||||
|
Decreasing the cluster size means the etcd cluster has to do less work for each write, thus increasing the write performance.
|
||||||
|
|
||||||
|
### Restart Cluster from Majority Failure
|
||||||
|
|
||||||
|
If the majority of your cluster is lost, then you need to take manual action in order to recover safely.
|
||||||
|
The basic steps in the recovery process include creating a new cluster using the old data, forcing a single member to act as the leader, and finally using runtime configuration to add members to this new cluster.
|
||||||
|
|
||||||
|
TODO: https://github.com/coreos/etcd/issues/1242
|
||||||
|
|
||||||
|
## Cluster Reconfiguration Operations
|
||||||
|
|
||||||
|
Now that we have the use cases in mind, let us lay out the operations involved in each.
|
||||||
|
|
||||||
|
Before making any change, the simple majority (quorum) of etcd members must be available.
|
||||||
|
This is essentially the same requirement as for any other write to etcd.
|
||||||
|
|
||||||
|
All changes to the cluster are done one at a time:
|
||||||
|
|
||||||
|
To replace a single member you will make an add then a remove operation
|
||||||
|
To increase from 3 to 5 members you will make two add operations
|
||||||
|
To decrease from 5 to 3 you will make two remove operations
|
||||||
|
|
||||||
|
All of these examples will use the `etcdctl` command line tool that ships with etcd.
|
||||||
|
If you want to use the member API directly you can find the documentation [here](https://github.com/coreos/etcd/blob/master/Documentation/0.5/other_apis.md).
|
||||||
|
|
||||||
|
### Remove a Member
|
||||||
|
|
||||||
|
First, we need to find the target member:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ etcdctl member list
|
||||||
|
6e3bd23ae5f1eae0: name=node2 peerURLs=http://localhost:7002 clientURLs=http://127.0.0.1:4002
|
||||||
|
924e2e83e93f2560: name=node3 peerURLs=http://localhost:7003 clientURLs=http://127.0.0.1:4003
|
||||||
|
a8266ecf031671f3: name=node1 peerURLs=http://localhost:7001 clientURLs=http://127.0.0.1:4001
|
||||||
|
```
|
||||||
|
|
||||||
|
Let us say the member ID we want to remove is a8266ecf031671f3.
|
||||||
|
We then use the `remove` command to perform the removal:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ etcdctl member remove a8266ecf031671f3
|
||||||
|
Removed member a8266ecf031671f3 from cluster
|
||||||
|
```
|
||||||
|
|
||||||
|
The target member will stop itself at this point and print out the removal in the log:
|
||||||
|
|
||||||
|
```
|
||||||
|
etcd: this member has been permanently removed from the cluster. Exiting.
|
||||||
|
```
|
||||||
|
|
||||||
|
Removal of the leader is safe, but the cluster will be out of progress for a period of election timeout because it needs to elect the new leader.
|
||||||
|
|
||||||
|
### Add a Member
|
||||||
|
|
||||||
|
Adding a member is a two step process:
|
||||||
|
|
||||||
|
* Add the new member to the cluster via the [members API](https://github.com/coreos/etcd/blob/master/Documentation/0.5/other_apis.md#post-v2members) or the `etcdctl member add` command.
|
||||||
|
* Start the member with the correct configuration.
|
||||||
|
|
||||||
|
Using `etcdctl` let's add the new member to the cluster:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ etcdctl member add infra3 http://10.0.1.13:2380
|
||||||
|
added member 9bf1b35fc7761a23 to cluster
|
||||||
|
ETCD_NAME="infra3"
|
||||||
|
ETCD_INITIAL_CLUSTER="infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380,infra3=http://10.0.1.13:2380"
|
||||||
|
ETCD_INITIAL_CLUSTER_STATE=existing
|
||||||
|
```
|
||||||
|
|
||||||
|
> Notice that infra3 was added to the cluster using its advertised peer URL.
|
||||||
|
|
||||||
|
Now start the new etcd process with the relevant flags for the new member:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ export ETCD_NAME="infra3"
|
||||||
|
$ export ETCD_INITIAL_CLUSTER="infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380,infra3=http://10.0.1.13:2380"
|
||||||
|
$ export ETCD_INITIAL_CLUSTER_STATE=existing
|
||||||
|
$ etcd -listen-client-urls http://10.0.1.13:2379 -advertise-client-urls http://10.0.1.13:2379 -listen-peer-urls http://10.0.1.13:2380 -initial-advertise-peer-urls http://10.0.1.13:2380
|
||||||
|
```
|
||||||
|
|
||||||
|
The new member will run as a part of the cluster and immediately begin catching up with the rest of the cluster.
|
||||||
|
|
||||||
|
If you are adding multiple members the best practice is to configure the new member, then start the process, then configure the next, and so on.
|
||||||
|
A common case is increasing a cluster from 1 to 3: if you add one member to a 1-node cluster, the cluster cannot make progress before the new member starts because it needs two members as majority to agree on the consensus.
|
||||||
|
|
||||||
|
#### Error Cases
|
||||||
|
|
||||||
|
In the following case we have not included our new host in the list of enumerated nodes.
|
||||||
|
If this is a new cluster, the node must be added to the list of initial cluster members.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ etcd -name infra3 \
|
||||||
|
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
|
||||||
|
-initial-cluster-state existing
|
||||||
|
etcdserver: assign ids error: the member count is unequal
|
||||||
|
exit 1
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case we give a different address (10.0.1.14:2380) to the one that we used to join the cluster (10.0.1.13:2380).
|
||||||
|
|
||||||
|
```
|
||||||
|
$ etcd -name infra4 \
|
||||||
|
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380,infra4=http://10.0.1.14:2380 \
|
||||||
|
-initial-cluster-state existing
|
||||||
|
etcdserver: assign ids error: unmatched member while checking PeerURLs
|
||||||
|
exit 1
|
||||||
|
```
|
||||||
|
|
||||||
|
When we start etcd using the data directory of a removed member, etcd will exit automatically if it connects to any alive member in the cluster:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ etcd
|
||||||
|
etcd: this member has been permanently removed from the cluster. Exiting.
|
||||||
|
exit 1
|
||||||
|
```
|
@ -126,10 +126,10 @@ curl -L http://127.0.0.1:4001/v2/keys/message -XPUT -d value="Hello etcd"
|
|||||||
"value": "Hello etcd"
|
"value": "Hello etcd"
|
||||||
},
|
},
|
||||||
"prevNode": {
|
"prevNode": {
|
||||||
"createdIndex": 2
|
"createdIndex": 2,
|
||||||
"key": "/message",
|
"key": "/message",
|
||||||
"value": "Hello world",
|
"value": "Hello world",
|
||||||
"modifiedIndex": 2,
|
"modifiedIndex": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -206,7 +206,7 @@ If the TTL has expired, the key will have been deleted, and you will be returned
|
|||||||
"cause": "/foo",
|
"cause": "/foo",
|
||||||
"errorCode": 100,
|
"errorCode": 100,
|
||||||
"index": 6,
|
"index": 6,
|
||||||
"message": "Key Not Found"
|
"message": "Key not found"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -224,7 +224,7 @@ curl -L http://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar -d ttl= -d prevExis
|
|||||||
"key": "/foo",
|
"key": "/foo",
|
||||||
"modifiedIndex": 6,
|
"modifiedIndex": 6,
|
||||||
"value": "bar"
|
"value": "bar"
|
||||||
}
|
},
|
||||||
"prevNode": {
|
"prevNode": {
|
||||||
"createdIndex": 5,
|
"createdIndex": 5,
|
||||||
"expiration": "2013-12-04T12:01:21.874888581-08:00",
|
"expiration": "2013-12-04T12:01:21.874888581-08:00",
|
||||||
@ -383,7 +383,7 @@ curl -L http://127.0.0.1:4001/v2/keys/dir -XPUT -d ttl=30 -d dir=true
|
|||||||
"createdIndex": 17,
|
"createdIndex": 17,
|
||||||
"dir": true,
|
"dir": true,
|
||||||
"expiration": "2013-12-11T10:37:33.689275857-08:00",
|
"expiration": "2013-12-11T10:37:33.689275857-08:00",
|
||||||
"key": "/newdir",
|
"key": "/dir",
|
||||||
"modifiedIndex": 17,
|
"modifiedIndex": 17,
|
||||||
"ttl": 30
|
"ttl": 30
|
||||||
}
|
}
|
||||||
@ -417,7 +417,7 @@ curl 'http://127.0.0.1:4001/v2/keys/dir/asdf?consistent=true&wait=true'
|
|||||||
"dir":true,
|
"dir":true,
|
||||||
"modifiedIndex": 17,
|
"modifiedIndex": 17,
|
||||||
"expiration": "2013-12-11T10:39:35.689275857-08:00"
|
"expiration": "2013-12-11T10:39:35.689275857-08:00"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -457,7 +457,7 @@ The error code explains the problem:
|
|||||||
"cause": "/foo",
|
"cause": "/foo",
|
||||||
"errorCode": 105,
|
"errorCode": 105,
|
||||||
"index": 39776,
|
"index": 39776,
|
||||||
"message": "Already exists"
|
"message": "Key already exists"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -474,7 +474,7 @@ This will try to compare the previous value of the key and the previous value we
|
|||||||
"cause": "[two != one]",
|
"cause": "[two != one]",
|
||||||
"errorCode": 101,
|
"errorCode": 101,
|
||||||
"index": 8,
|
"index": 8,
|
||||||
"message": "Test Failed"
|
"message": "Compare failed"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -643,16 +643,22 @@ We should see the response as an array of items:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action": "get",
|
"action": "get",
|
||||||
"node": {
|
"node": {
|
||||||
"dir": true,
|
"key": "/",
|
||||||
"key": "/",
|
"dir": true,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"createdIndex": 2,
|
"key": "/foo_dir",
|
||||||
"dir": true,
|
"dir": true,
|
||||||
"key": "/foo_dir",
|
"modifiedIndex": 2,
|
||||||
"modifiedIndex": 2
|
"createdIndex": 2
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"key": "/foo",
|
||||||
|
"value": "two",
|
||||||
|
"modifiedIndex": 1,
|
||||||
|
"createdIndex": 1
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -667,27 +673,33 @@ curl -L http://127.0.0.1:4001/v2/keys/?recursive=true
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action": "get",
|
"action": "get",
|
||||||
"node": {
|
"node": {
|
||||||
"dir": true,
|
"key": "/",
|
||||||
"key": "/",
|
"dir": true,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"createdIndex": 2,
|
"key": "/foo_dir",
|
||||||
"dir": true,
|
"dir": true,
|
||||||
"key": "/foo_dir",
|
"nodes": [
|
||||||
"modifiedIndex": 2,
|
{
|
||||||
"nodes": [
|
"key": "/foo_dir/foo",
|
||||||
{
|
"value": "bar",
|
||||||
"createdIndex": 2,
|
"modifiedIndex": 2,
|
||||||
"key": "/foo_dir/foo",
|
"createdIndex": 2
|
||||||
"modifiedIndex": 2,
|
}
|
||||||
"value": "bar"
|
],
|
||||||
}
|
"modifiedIndex": 2,
|
||||||
]
|
"createdIndex": 2
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
}
|
"key": "/foo",
|
||||||
|
"value": "two",
|
||||||
|
"modifiedIndex": 1,
|
||||||
|
"createdIndex": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1,46 +1,43 @@
|
|||||||
# Client libraries support matrix for etcd
|
# Client libraries support matrix for etcd
|
||||||
|
|
||||||
As etcd features support is really uneven between client libraries, a compatibility matrix can be important.
|
As etcd features support is really uneven between client libraries, a compatibility matrix can be important.
|
||||||
We will consider in detail only the features of clients supporting the v2 API. Clients still supporting the v1 API *only* are listed below.
|
|
||||||
|
|
||||||
## v1-only clients
|
|
||||||
|
|
||||||
Clients supporting only the API version 1
|
|
||||||
|
|
||||||
- [justinsb/jetcd](https://github.com/justinsb/jetcd) Java
|
|
||||||
- [transitorykris/etcd-py](https://github.com/transitorykris/etcd-py) Python
|
|
||||||
- [russellhaering/txetcd](https://github.com/russellhaering/txetcd) Python
|
|
||||||
- [iconara/etcd-rb](https://github.com/iconara/etcd-rb) Ruby
|
|
||||||
- [jpfuentes2/etcd-ruby](https://github.com/jpfuentes2/etcd-ruby) Ruby
|
|
||||||
- [aterreno/etcd-clojure](https://github.com/aterreno/etcd-clojure) Clojure
|
|
||||||
- [marshall-lee/etcd.erl](https://github.com/marshall-lee/etcd.erl) Erlang
|
|
||||||
|
|
||||||
|
|
||||||
## v2 clients
|
## v2 clients
|
||||||
|
|
||||||
The v2 API has a lot of features, we will categorize them in a few categories:
|
The v2 API has a lot of features, we will categorize them in a few categories:
|
||||||
|
- **Language**: The language in which the client library was written.
|
||||||
- **HTTPS Auth**: Support for SSL-certificate based authentication
|
- **HTTPS Auth**: Support for SSL-certificate based authentication
|
||||||
- **Reconnect**: If the client is able to reconnect automatically to another server if one fails.
|
- **Reconnect**: If the client is able to reconnect automatically to another server if one fails.
|
||||||
- **Mod/Lock**: Support for the locking module
|
- **Mod/Lock**: Support for the locking module
|
||||||
- **Mod/Leader**: Support for the leader election module
|
- **Mod/Leader**: Support for the leader election module
|
||||||
- **GET,PUT,POST,DEL Features**: Support for all the modifiers when calling the etcd server with said HTTP method.
|
- **GET,PUT,POST,DEL Features**: Support for all the modifiers when calling the etcd server with said HTTP method.
|
||||||
|
|
||||||
|
|
||||||
### Supported features matrix
|
### Supported features matrix
|
||||||
|
|
||||||
| Client| [go-etcd](https://github.com/coreos/go-etcd) | [jetcd](https://github.com/diwakergupta/jetcd) | [python-etcd](https://github.com/jplana/python-etcd) | [python-etcd-client](https://github.com/dsoprea/PythonEtcdClient) | [node-etcd](https://github.com/stianeikeland/node-etcd) | [nodejs-etcd](https://github.com/lavagetto/nodejs-etcd) | [etcd-ruby](https://github.com/ranjib/etcd-ruby) | [etcd-api](https://github.com/jdarcy/etcd-api) | [cetcd](https://github.com/dwwoelfel/cetcd) | [clj-etcd](https://github.com/rthomas/clj-etcd) | [etcetera](https://github.com/drusellers/etcetera)| [Etcd.jl](https://github.com/forio/Etcd.jl) | [p5-etcd](https://metacpan.org/release/Etcd)
|
|
||||||
| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
|
|
||||||
| **HTTPS Auth** | Y | Y | Y | Y | Y | Y | - | - | - | - | - | - | - |
|
|
||||||
| **Reconnect** | Y | - | Y | Y | - | - | - | Y | - | - | - | - | - |
|
|
||||||
| **Mod/Lock** | - | - | Y | Y | - | - | - | - | - | - | - | Y | - |
|
|
||||||
| **Mod/Leader** | - | - | - | Y | - | - | - | - | - | - | - | Y | - |
|
|
||||||
| **GET Features** | F | B | F | F | F | F | F | B | F | G | F | F | F |
|
|
||||||
| **PUT Features** | F | B | F | F | F | F | F | G | F | G | F | F | F |
|
|
||||||
| **POST Features** | F | - | F | F | - | F | F | - | - | - | F | F | F |
|
|
||||||
| **DEL Features** | F | B | F | F | F | F | F | B | G | B | F | F | F |
|
|
||||||
|
|
||||||
**Legend**
|
**Legend**
|
||||||
|
|
||||||
**F**: Full support **G**: Good support **B**: Basic support
|
**F**: Full support **G**: Good support **B**: Basic support
|
||||||
**Y**: Feature supported **-**: Feature not supported
|
**Y**: Feature supported **-**: Feature not supported
|
||||||
|
|
||||||
|
Sorted alphabetically on language/name
|
||||||
|
|
||||||
|
|Client |**Language**|**HTTPS Auth**|**Re-connect**|**GET**|**PUT**|**POST**|**DEL**|**Mod Lock**|**Mod Leader**|
|
||||||
|
| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
|
||||||
|
|[etcd-api](https://github.com/jdarcy/etcd-api) |C |-|Y|B|G|-|B|-|-|
|
||||||
|
|[etcdcpp](https://github.com/edwardcapriolo/etcdcpp) |C++ |-|-|F|F|G|-|-|-|
|
||||||
|
|[cetcd](https://github.com/dwwoelfel/cetcd) |Clojure|-|-|F|F|-|G|-|-|
|
||||||
|
|[clj-etcd](https://github.com/rthomas/clj-etcd) |Clojure|-|-|G|G|-|B|-|-|
|
||||||
|
|[etcd-clojure](https://github.com/aterreno/etcd-clojure) |Clojure|-|-|F|F|F|F|-|-|
|
||||||
|
|[go-etcd](https://github.com/coreos/go-etcd) |go |Y|Y|F|F|F|F|-|-|
|
||||||
|
|[boon etcd client](https://github.com/boonproject/boon/blob/master/etcd/README.md) |java |Y|Y|F|F|F|F|-|F|
|
||||||
|
|[etcd4j](https://github.com/jurmous/etcd4j) |java |Y|Y|F|F|F|F|-|-|
|
||||||
|
|[jetcd](https://github.com/diwakergupta/jetcd) |java |Y|-|B|B|-|B|-|-|
|
||||||
|
|[jetcd](https://github.com/justinsb/jetcd) |java |-|-|B|B|-|B|-|-|
|
||||||
|
|[Etcd.jl](https://github.com/forio/Etcd.jl) |Julia |-|-|F|F|F|F|Y|Y|
|
||||||
|
|[etcetera](https://github.com/drusellers/etcetera) |.net |-|-|F|F|F|F|-|-|
|
||||||
|
|[node-etcd](https://github.com/stianeikeland/node-etcd) |nodejs |Y|-|F|F|-|F|-|-|
|
||||||
|
|[nodejs-etcd](https://github.com/lavagetto/nodejs-etcd) |nodejs |Y|-|F|F|F|F|-|-|
|
||||||
|
|[p5-etcd](https://metacpan.org/release/Etcd) |perl |-|-|F|F|F|F|-|-|
|
||||||
|
|[python-etcd](https://github.com/jplana/python-etcd) |python |Y|Y|F|F|F|F|Y|-|
|
||||||
|
|[python-etcd-client](https://github.com/dsoprea/PythonEtcdClient)|python |Y|Y|F|F|F|F|Y|Y|
|
||||||
|
|[txetcd](https://github.com/russellhaering/txetcd) |python |-|-|G|G|F|G|-|-|
|
||||||
|
|[etcd-ruby](https://github.com/ranjib/etcd-ruby) |ruby |-|-|F|F|F|F|-|-|
|
||||||
|
@ -13,11 +13,11 @@ Please note - at least 3 nodes are required for [cluster availability][optimal-c
|
|||||||
|
|
||||||
## Using discovery.etcd.io
|
## Using discovery.etcd.io
|
||||||
|
|
||||||
### Create a Token
|
### Create a Discovery URL
|
||||||
|
|
||||||
To use the discovery API, you must first create a token for your etcd cluster. Visit [https://discovery.etcd.io/new](https://discovery.etcd.io/new) to create a new token.
|
To use the discovery API, you must first create a unique discovery URL for your etcd cluster. Visit [https://discovery.etcd.io/new](https://discovery.etcd.io/new) to create a new discovery URL.
|
||||||
|
|
||||||
You can inspect the list of peers by viewing `https://discovery.etcd.io/<token>`.
|
You can inspect the list of peers by viewing `https://discovery.etcd.io/<cluster id>`.
|
||||||
|
|
||||||
### Start etcd With the Discovery Flag
|
### Start etcd With the Discovery Flag
|
||||||
|
|
||||||
@ -26,10 +26,10 @@ Specify the `-discovery` flag when you start each etcd instance. The list of exi
|
|||||||
Here's a full example:
|
Here's a full example:
|
||||||
|
|
||||||
```
|
```
|
||||||
TOKEN=$(curl https://discovery.etcd.io/new)
|
URL=$(curl https://discovery.etcd.io/new)
|
||||||
./etcd -name instance1 -peer-addr 10.1.2.3:7001 -addr 10.1.2.3:4001 -discovery $TOKEN
|
./etcd -name instance1 -peer-addr 10.1.2.3:7001 -addr 10.1.2.3:4001 -discovery $URL
|
||||||
./etcd -name instance2 -peer-addr 10.1.2.4:7001 -addr 10.1.2.4:4001 -discovery $TOKEN
|
./etcd -name instance2 -peer-addr 10.1.2.4:7001 -addr 10.1.2.4:4001 -discovery $URL
|
||||||
./etcd -name instance3 -peer-addr 10.1.2.5:7001 -addr 10.1.2.5:4001 -discovery $TOKEN
|
./etcd -name instance3 -peer-addr 10.1.2.5:7001 -addr 10.1.2.5:4001 -discovery $URL
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running Your Own Discovery Endpoint
|
## Running Your Own Discovery Endpoint
|
||||||
@ -37,10 +37,10 @@ TOKEN=$(curl https://discovery.etcd.io/new)
|
|||||||
The discovery API communicates with a separate etcd cluster to store and retrieve the list of peers. CoreOS provides [https://discovery.etcd.io](https://discovery.etcd.io) as a free service, but you can easily run your own etcd cluster for this purpose. Here's an example using an etcd cluster located at `10.10.10.10:4001`:
|
The discovery API communicates with a separate etcd cluster to store and retrieve the list of peers. CoreOS provides [https://discovery.etcd.io](https://discovery.etcd.io) as a free service, but you can easily run your own etcd cluster for this purpose. Here's an example using an etcd cluster located at `10.10.10.10:4001`:
|
||||||
|
|
||||||
```
|
```
|
||||||
TOKEN="testcluster"
|
URL="http://10.10.10.10:4001/v2/keys/testcluster"
|
||||||
./etcd -name instance1 -peer-addr 10.1.2.3:7001 -addr 10.1.2.3:4001 -discovery http://10.10.10.10:4001/v2/keys/$TOKEN
|
./etcd -name instance1 -peer-addr 10.1.2.3:7001 -addr 10.1.2.3:4001 -discovery $URL
|
||||||
./etcd -name instance2 -peer-addr 10.1.2.4:7001 -addr 10.1.2.4:4001 -discovery http://10.10.10.10:4001/v2/keys/$TOKEN
|
./etcd -name instance2 -peer-addr 10.1.2.4:7001 -addr 10.1.2.4:4001 -discovery $URL
|
||||||
./etcd -name instance3 -peer-addr 10.1.2.5:7001 -addr 10.1.2.5:4001 -discovery http://10.10.10.10:4001/v2/keys/$TOKEN
|
./etcd -name instance3 -peer-addr 10.1.2.5:7001 -addr 10.1.2.5:4001 -discovery $URL
|
||||||
```
|
```
|
||||||
|
|
||||||
If you're interested in how to discovery API works behind the scenes, read about the [Discovery Protocol](https://github.com/coreos/etcd/blob/master/Documentation/discovery-protocol.md).
|
If you're interested in how to discovery API works behind the scenes, read about the [Discovery Protocol](https://github.com/coreos/etcd/blob/master/Documentation/discovery-protocol.md).
|
||||||
|
@ -167,3 +167,9 @@ Etcd can also do internal server-to-server communication using SSL client certs.
|
|||||||
To do this just change the `-*-file` flags to `-peer-*-file`.
|
To do this just change the `-*-file` flags to `-peer-*-file`.
|
||||||
|
|
||||||
If you are using SSL for server-to-server communication, you must use it on all instances of etcd.
|
If you are using SSL for server-to-server communication, you must use it on all instances of etcd.
|
||||||
|
|
||||||
|
### Bootstrapping a new cluster by name
|
||||||
|
|
||||||
|
An etcd server is uniquely defined by the peer addresses it listens to. Suppose, however, that you wish to start over, while maintaining the data from the previous cluster -- that is, to pretend that this machine has never joined a cluster before.
|
||||||
|
|
||||||
|
You can use `--initial-cluster-name` to generate a new unique ID for each node, as a shared token that every node understands. Nodes also take this into account for bootstrapping the new cluster ID, so it also provides a way for a machine to listen on the same interfaces, disconnect from one cluster, and join a different cluster.
|
||||||
|
@ -75,8 +75,8 @@ cors = []
|
|||||||
cpu_profile_file = ""
|
cpu_profile_file = ""
|
||||||
data_dir = "."
|
data_dir = "."
|
||||||
discovery = "http://etcd.local:4001/v2/keys/_etcd/registry/examplecluster"
|
discovery = "http://etcd.local:4001/v2/keys/_etcd/registry/examplecluster"
|
||||||
http_read_timeout = 10
|
http_read_timeout = 10.0
|
||||||
http_write_timeout = 10
|
http_write_timeout = 10.0
|
||||||
key_file = ""
|
key_file = ""
|
||||||
peers = []
|
peers = []
|
||||||
peers_file = ""
|
peers_file = ""
|
||||||
@ -84,7 +84,7 @@ max_cluster_size = 9
|
|||||||
max_result_buffer = 1024
|
max_result_buffer = 1024
|
||||||
max_retry_attempts = 3
|
max_retry_attempts = 3
|
||||||
name = "default-name"
|
name = "default-name"
|
||||||
snapshot = false
|
snapshot = true
|
||||||
verbose = false
|
verbose = false
|
||||||
very_verbose = false
|
very_verbose = false
|
||||||
|
|
||||||
@ -112,8 +112,8 @@ sync_interval = 5.0
|
|||||||
* `ETCD_CPU_PROFILE_FILE`
|
* `ETCD_CPU_PROFILE_FILE`
|
||||||
* `ETCD_DATA_DIR`
|
* `ETCD_DATA_DIR`
|
||||||
* `ETCD_DISCOVERY`
|
* `ETCD_DISCOVERY`
|
||||||
* `ETCD_CLUSTER_HTTP_READ_TIMEOUT`
|
* `ETCD_HTTP_READ_TIMEOUT`
|
||||||
* `ETCD_CLUSTER_HTTP_WRITE_TIMEOUT`
|
* `ETCD_HTTP_WRITE_TIMEOUT`
|
||||||
* `ETCD_KEY_FILE`
|
* `ETCD_KEY_FILE`
|
||||||
* `ETCD_PEERS`
|
* `ETCD_PEERS`
|
||||||
* `ETCD_PEERS_FILE`
|
* `ETCD_PEERS_FILE`
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
# Development tools
|
|
||||||
|
|
||||||
## Vagrant
|
|
||||||
|
|
||||||
For fast start you can use Vagrant. `vagrant up` will make etcd build and running on virtual machine. Required Vagrant version is 1.5.0.
|
|
||||||
|
|
||||||
Next lets set a single key and then retrieve it:
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -L http://127.0.0.1:4001/v2/keys/mykey -XPUT -d value="this is awesome"
|
|
||||||
curl -L http://127.0.0.1:4001/v2/keys/mykey
|
|
||||||
```
|
|
@ -3,8 +3,10 @@
|
|||||||
**Tools**
|
**Tools**
|
||||||
|
|
||||||
- [etcdctl](https://github.com/coreos/etcdctl) - A command line client for etcd
|
- [etcdctl](https://github.com/coreos/etcdctl) - A command line client for etcd
|
||||||
|
- [etcd-backup](https://github.com/fanhattan/etcd-backup) - A powerful command line utility for dumping/restoring etcd - Supports v2
|
||||||
- [etcd-dump](https://npmjs.org/package/etcd-dump) - Command line utility for dumping/restoring etcd.
|
- [etcd-dump](https://npmjs.org/package/etcd-dump) - Command line utility for dumping/restoring etcd.
|
||||||
- [etcd-fs](https://github.com/xetorthio/etcd-fs) - FUSE filesystem for etcd
|
- [etcd-fs](https://github.com/xetorthio/etcd-fs) - FUSE filesystem for etcd
|
||||||
|
- [etcd-browser](https://github.com/henszey/etcd-browser) - A web-based key/value editor for etcd using AngularJS
|
||||||
|
|
||||||
**Go libraries**
|
**Go libraries**
|
||||||
|
|
||||||
@ -12,8 +14,12 @@
|
|||||||
|
|
||||||
**Java libraries**
|
**Java libraries**
|
||||||
|
|
||||||
|
|
||||||
|
- [boonproject/etcd](https://github.com/boonproject/boon/blob/master/etcd/README.md) - Supports v2, Async/Sync and waits
|
||||||
- [justinsb/jetcd](https://github.com/justinsb/jetcd)
|
- [justinsb/jetcd](https://github.com/justinsb/jetcd)
|
||||||
- [diwakergupta/jetcd](https://github.com/diwakergupta/jetcd) - Supports v2
|
- [diwakergupta/jetcd](https://github.com/diwakergupta/jetcd) - Supports v2
|
||||||
|
- [jurmous/etcd4j](https://github.com/jurmous/etcd4j) - Supports v2
|
||||||
|
- [AdoHe/etcd4j](http://github.com/AdoHe/etcd4j) - Supports v2 (enhance for real production cluster)
|
||||||
|
|
||||||
**Python libraries**
|
**Python libraries**
|
||||||
|
|
||||||
@ -36,6 +42,9 @@
|
|||||||
|
|
||||||
- [jdarcy/etcd-api](https://github.com/jdarcy/etcd-api) - Supports v2
|
- [jdarcy/etcd-api](https://github.com/jdarcy/etcd-api) - Supports v2
|
||||||
|
|
||||||
|
**C++ libraries**
|
||||||
|
- [edwardcapriolo/etcdcpp](https://github.com/edwardcapriolo/etcdcpp) - Supports v2
|
||||||
|
|
||||||
**Clojure libraries**
|
**Clojure libraries**
|
||||||
|
|
||||||
- [aterreno/etcd-clojure](https://github.com/aterreno/etcd-clojure)
|
- [aterreno/etcd-clojure](https://github.com/aterreno/etcd-clojure)
|
||||||
@ -92,3 +101,6 @@ A detailed recap of client functionalities can be found in the [clients compatib
|
|||||||
- [GoogleCloudPlatform/kubernetes](https://github.com/GoogleCloudPlatform/kubernetes) - Container cluster manager.
|
- [GoogleCloudPlatform/kubernetes](https://github.com/GoogleCloudPlatform/kubernetes) - Container cluster manager.
|
||||||
- [mailgun/vulcand](https://github.com/mailgun/vulcand) - HTTP proxy that uses etcd as a configuration backend.
|
- [mailgun/vulcand](https://github.com/mailgun/vulcand) - HTTP proxy that uses etcd as a configuration backend.
|
||||||
- [duedil-ltd/discodns](https://github.com/duedil-ltd/discodns) - Simple DNS nameserver using etcd as a database for names and records.
|
- [duedil-ltd/discodns](https://github.com/duedil-ltd/discodns) - Simple DNS nameserver using etcd as a database for names and records.
|
||||||
|
- [skynetservices/skydns](https://github.com/skynetservices/skydns) - RFC compliant DNS server
|
||||||
|
- [xordataexchange/crypt](https://github.com/xordataexchange/crypt) - Securely store values in etcd using GPG encryption
|
||||||
|
- [spf13/viper](https://github.com/spf13/viper) - Go configuration library, reads values from ENV, pflags, files, and etcd with optional encryption
|
||||||
|
@ -1,34 +1,50 @@
|
|||||||
# Reading and Writing over HTTPS
|
# Etcd security model
|
||||||
|
|
||||||
## Transport Security with HTTPS
|
Etcd supports SSL/TLS as well as authentication through client certificates, both for clients to server as well as peer (server to server / cluster) communication.
|
||||||
|
|
||||||
Etcd supports SSL/TLS and client cert authentication for clients to server, as well as server to server communication.
|
To get up and running you first need to have a CA certificate and a signed key pair for your node. It is recommended to create and sign a new key pair for every node in a cluster.
|
||||||
|
|
||||||
|
For convenience the [etcd-ca](https://github.com/coreos/etcd-ca) tool provides an easy interface to certificate generation, alternatively this site provides a good reference on how to generate self-signed key pairs:
|
||||||
|
|
||||||
First, you need to have a CA cert `clientCA.crt` and signed key pair `client.crt`, `client.key`.
|
|
||||||
This site has a good reference for how to generate self-signed key pairs:
|
|
||||||
http://www.g-loaded.eu/2005/11/10/be-your-own-ca/
|
http://www.g-loaded.eu/2005/11/10/be-your-own-ca/
|
||||||
Or you could use [etcd-ca](https://github.com/coreos/etcd-ca) to generate certs and keys.
|
|
||||||
|
|
||||||
For testing you can use the certificates in the `fixtures/ca` directory.
|
## Basic setup
|
||||||
|
|
||||||
Let's configure etcd to use this keypair:
|
Etcd takes several certificate related configuration options, either through command-line flags or environment variables:
|
||||||
|
|
||||||
|
**Client-to-server communication:**
|
||||||
|
|
||||||
|
`--cert-file=<path>`: Certificate used for SSL/TLS connections **to** etcd. When this option is set, you can reach etcd through HTTPS - for example at `https://127.0.0.1:4001`
|
||||||
|
`--key-file=<path>`: Key for the certificate. Must be unencrypted.
|
||||||
|
`--ca-file=<path>`: When this is set etcd will check all incoming HTTPS requests for a client certificate signed by the supplied CA, requests that don't supply a valid client certificate will fail.
|
||||||
|
|
||||||
|
**Peer (server-to-server / cluster) communication:**
|
||||||
|
|
||||||
|
The peer options work the same way as the client-to-server options:
|
||||||
|
|
||||||
|
`--peer-cert-file=<path>`: Certificate used for SSL/TLS connections between peers. This will be used both for listening on the peer address as well as sending requests to other peers.
|
||||||
|
`--peer-key-file=<path>`: Key for the certificate. Must be unencrypted.
|
||||||
|
`--peer-ca-file=<path>`: When set, etcd will check all incoming peer requests from the cluster for valid client certificates signed by the supplied CA.
|
||||||
|
|
||||||
|
If either a client-to-server or peer certificate is supplied the key must also be set. All of these configuration options are also available through the environment variables, `ETCD_CA_FILE`, `ETCD_PEER_CA_FILE` and so on.
|
||||||
|
|
||||||
|
## Example 1: Client-to-server transport security with HTTPS
|
||||||
|
|
||||||
|
For this you need your CA certificate (`ca.crt`) and signed key pair (`server.crt`, `server.key`) ready. If you just want to test the functionality, there are example certificates provided in the [etcd git repository](https://github.com/coreos/etcd/tree/master/fixtures/ca) (namely `server.crt` and `server.key.insecure`).
|
||||||
|
|
||||||
|
Assuming you have these files ready, let's configure etcd to use them to provide simple HTTPS transport security.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./etcd -f -name machine0 -data-dir machine0 -cert-file=./fixtures/ca/server.crt -key-file=./fixtures/ca/server.key.insecure
|
etcd -name machine0 -data-dir machine0 -cert-file=/path/to/server.crt -key-file=/path/to/server.key
|
||||||
```
|
```
|
||||||
|
|
||||||
There are a few new options we're using:
|
This should start up fine and you can now test the configuration by speaking HTTPS to etcd:
|
||||||
|
|
||||||
* `-f` - forces a new machine configuration, even if an existing configuration is found. (WARNING: data loss!)
|
|
||||||
* `-cert-file` and `-key-file` specify the location of the cert and key files to be used for for transport layer security between the client and server.
|
|
||||||
|
|
||||||
You can now test the configuration using HTTPS:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl --cacert ./fixtures/ca/server-chain.pem https://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar -v
|
curl --cacert /path/to/ca.crt https://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar -v
|
||||||
```
|
```
|
||||||
|
|
||||||
You should be able to see the handshake succeed.
|
You should be able to see the handshake succeed. Because we use self-signed certificates with our own certificate authorities you need to provide the CA to curl using the `--cacert` option. Another possibility would be to add your CA certificate to the trusted certificates on your system (usually in `/etc/ssl/certs`).
|
||||||
|
|
||||||
**OSX 10.9+ Users**: curl 7.30.0 on OSX 10.9+ doesn't understand certificates passed in on the command line.
|
**OSX 10.9+ Users**: curl 7.30.0 on OSX 10.9+ doesn't understand certificates passed in on the command line.
|
||||||
Instead you must import the dummy ca.crt directly into the keychain or add the `-k` flag to curl to ignore errors.
|
Instead you must import the dummy ca.crt directly into the keychain or add the `-k` flag to curl to ignore errors.
|
||||||
@ -36,42 +52,28 @@ If you want to test without the `-k` flag run `open ./fixtures/ca/ca.crt` and fo
|
|||||||
Please remove this certificate after you are done testing!
|
Please remove this certificate after you are done testing!
|
||||||
If you know of a workaround let us know.
|
If you know of a workaround let us know.
|
||||||
|
|
||||||
```
|
## Example 2: Client-to-server authentication with HTTPS client certificates
|
||||||
...
|
|
||||||
SSLv3, TLS handshake, Finished (20):
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
And also the response from the etcd server:
|
For now we've given the etcd client the ability to verify the server identity and provide transport security. We can however also use client certificates to prevent unauthorized access to etcd.
|
||||||
|
|
||||||
```json
|
The clients will provide their certificates to the server and the server will check whether the cert is signed by the supplied CA and decide whether to serve the request.
|
||||||
{
|
|
||||||
"action": "set",
|
|
||||||
"key": "/foo",
|
|
||||||
"modifiedIndex": 3,
|
|
||||||
"value": "bar"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
You need the same files mentioned in the first example for this, as well as a key pair for the client (`client.crt`, `client.key`) signed by the same certificate authority.
|
||||||
## Authentication with HTTPS Client Certificates
|
|
||||||
|
|
||||||
We can also do authentication using CA certs.
|
|
||||||
The clients will provide their cert to the server and the server will check whether the cert is signed by the CA and decide whether to serve the request.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./etcd -f -name machine0 -data-dir machine0 -ca-file=./fixtures/ca/ca.crt -cert-file=./fixtures/ca/server.crt -key-file=./fixtures/ca/server.key.insecure
|
etcd -name machine0 -data-dir machine0 -ca-file=/path/to/ca.crt -cert-file=/path/to/server.crt -key-file=/path/to/server.key
|
||||||
```
|
```
|
||||||
|
|
||||||
```-ca-file``` is the path to the CA cert.
|
Notice that the addition of the `-ca-file` option automatically enables client certificate checking.
|
||||||
|
|
||||||
Try the same request to this server:
|
|
||||||
|
Now try the same request as above to this server:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl --cacert ./fixtures/ca/server-chain.pem https://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar -v
|
curl --cacert /path/to/ca.crt https://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar -v
|
||||||
```
|
```
|
||||||
|
|
||||||
The request should be rejected by the server.
|
The request should be rejected by the server:
|
||||||
|
|
||||||
```
|
```
|
||||||
...
|
...
|
||||||
@ -79,10 +81,10 @@ routines:SSL3_READ_BYTES:sslv3 alert bad certificate
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
We need to give the CA signed cert to the server.
|
To make it succeed, we need to give the CA signed client certificate to the server:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl --key ./fixtures/ca/server2.key.insecure --cert ./fixtures/ca/server2.crt --cacert ./fixtures/ca/server-chain.pem -L https://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar -v
|
curl --cacert /path/to/ca.crt --cert /path/to/client.crt --key /path/to/client.key -L https://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar -v
|
||||||
```
|
```
|
||||||
|
|
||||||
You should able to see:
|
You should able to see:
|
||||||
@ -108,7 +110,28 @@ And also the response from the server:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Why SSLv3 alert handshake failure when using SSL client auth?
|
## Example 3: Transport security & client certificates in a cluster
|
||||||
|
|
||||||
|
Etcd supports the same model as above for **peer communication**, that means the communication between etcd nodes in a cluster.
|
||||||
|
|
||||||
|
Assuming we have our `ca.crt` and two nodes with their own keypairs (`node1.crt` & `node1.key`, `node2.crt` & `node2.key`) signed by this CA, we launch etcd as follows:
|
||||||
|
|
||||||
|
|
||||||
|
```sh
|
||||||
|
DISCOVERY_URL=... # from https://discovery.etcd.io/new
|
||||||
|
|
||||||
|
# Node1
|
||||||
|
etcd -name node1 -data-dir node1 -ca-file=/path/to/ca.crt -cert-file=/path/to/node1.crt -key-file=/path/to/node1.key -peer-addr ${node1_public_ip}:7001 -discovery ${DISCOVERY_URL}
|
||||||
|
|
||||||
|
# Node2
|
||||||
|
etcd -name node2 -data-dir node2 -ca-file=/path/to/ca.crt -cert-file=/path/to/node2.crt -key-file=/path/to/node2.key -peer-addr ${node2_public_ip}:7001 -discovery ${DISCOVERY_URL}
|
||||||
|
```
|
||||||
|
|
||||||
|
The etcd nodes will form a cluster and all communication between nodes in the cluster will be encrypted and authenticated using the client certificates. You will see in the output of etcd that the addresses it connects to use HTTPS.
|
||||||
|
|
||||||
|
## Frequently Asked Questions
|
||||||
|
|
||||||
|
### I'm seeing a SSLv3 alert handshake failure when using SSL client authentication?
|
||||||
|
|
||||||
The `crypto/tls` package of `golang` checks the key usage of the certificate public key before using it.
|
The `crypto/tls` package of `golang` checks the key usage of the certificate public key before using it.
|
||||||
To use the certificate public key to do client auth, we need to add `clientAuth` to `Extended Key Usage` when creating the certificate public key.
|
To use the certificate public key to do client auth, we need to add `clientAuth` to `Extended Key Usage` when creating the certificate public key.
|
||||||
@ -129,3 +152,8 @@ When creating the cert be sure to reference it in the `-extensions` flag:
|
|||||||
```
|
```
|
||||||
openssl ca -config openssl.cnf -policy policy_anything -extensions ssl_client -out certs/machine.crt -infiles machine.csr
|
openssl ca -config openssl.cnf -policy policy_anything -extensions ssl_client -out certs/machine.crt -infiles machine.csr
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### With peer certificate authentication I receive "certificate is valid for 127.0.0.1, not $MY_IP"
|
||||||
|
Make sure that you sign your certificates with a Subject Name your node's public IP address. The `etcd-ca` tool for example provides an `--ip=` option for its `new-cert` command.
|
||||||
|
|
||||||
|
If you need your certificate to be signed for your node's FQDN in its Subject Name then you could use Subject Alternative Names (short IP SNAs) to add your IP address. This is not [currently supported](https://github.com/coreos/etcd-ca/issues/29) by `etcd-ca` but can be done [with openssl](http://wiki.cacert.org/FAQ/subjectAltName).
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
The default settings in etcd should work well for installations on a local network where the average network latency is low.
|
The default settings in etcd should work well for installations on a local network where the average network latency is low.
|
||||||
However, when using etcd across multiple data centers or over networks with high latency you may need to tweak the heartbeat interval and election timeout settings.
|
However, when using etcd across multiple data centers or over networks with high latency you may need to tweak the heartbeat interval and election timeout settings.
|
||||||
|
|
||||||
|
The network isn't the only source of latency. Each request and response may be impacted by slow disks on both the leader and follower. Each of these timeouts represents the total time from request to successful response from the other machine.
|
||||||
|
|
||||||
### Time Parameters
|
### 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 underlying distributed consensus protocol relies on two separate time parameters to ensure that nodes can handoff leadership if one stalls or goes offline.
|
||||||
@ -41,7 +43,7 @@ Or you can set the values within the configuration file:
|
|||||||
```toml
|
```toml
|
||||||
[peer]
|
[peer]
|
||||||
heartbeat_interval = 100
|
heartbeat_interval = 100
|
||||||
election_timeout = 100
|
election_timeout = 500
|
||||||
```
|
```
|
||||||
|
|
||||||
The values are specified in milliseconds.
|
The values are specified in milliseconds.
|
||||||
|
36
Godeps/Godeps.json
generated
Normal file
36
Godeps/Godeps.json
generated
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"ImportPath": "github.com/coreos/etcd",
|
||||||
|
"GoVersion": "go1.3.1",
|
||||||
|
"Packages": [
|
||||||
|
"./..."
|
||||||
|
],
|
||||||
|
"Deps": [
|
||||||
|
{
|
||||||
|
"ImportPath": "code.google.com/p/go.net/context",
|
||||||
|
"Comment": "null-144",
|
||||||
|
"Rev": "ad01a6fcc8a19d3a4478c836895ffe883bd2ceab"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "code.google.com/p/gogoprotobuf/proto",
|
||||||
|
"Rev": "7fd1620f09261338b6b1ca1289ace83aee0ec946"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/codegangsta/cli",
|
||||||
|
"Comment": "1.2.0-26-gf7ebb76",
|
||||||
|
"Rev": "f7ebb761e83e21225d1d8954fde853bf8edd46c4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/coreos/go-etcd/etcd",
|
||||||
|
"Comment": "v0.2.0-rc1-127-g6fe04d5",
|
||||||
|
"Rev": "6fe04d580dfb71c9e34cbce2f4df9eefd1e1241e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/jonboulle/clockwork",
|
||||||
|
"Rev": "72f9bd7c4e0c2a40055ab3d0f09654f730cce982"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/stretchr/testify/assert",
|
||||||
|
"Rev": "9cc77fa25329013ce07362c7742952ff887361f2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
5
Godeps/Readme
generated
Normal file
5
Godeps/Readme
generated
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
This directory tree is generated automatically by godep.
|
||||||
|
|
||||||
|
Please do not edit.
|
||||||
|
|
||||||
|
See https://github.com/tools/godep for more information.
|
2
Godeps/_workspace/.gitignore
generated
vendored
Normal file
2
Godeps/_workspace/.gitignore
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/pkg
|
||||||
|
/bin
|
431
Godeps/_workspace/src/code.google.com/p/go.net/context/context.go
generated
vendored
Normal file
431
Godeps/_workspace/src/code.google.com/p/go.net/context/context.go
generated
vendored
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package context defines the Context type, which carries deadlines,
|
||||||
|
// cancelation signals, and other request-scoped values across API boundaries
|
||||||
|
// and between processes.
|
||||||
|
//
|
||||||
|
// Incoming requests to a server should create a Context, and outgoing calls to
|
||||||
|
// servers should accept a Context. The chain of function calls between must
|
||||||
|
// propagate the Context, optionally replacing it with a modified copy created
|
||||||
|
// using WithDeadline, WithTimeout, WithCancel, or WithValue.
|
||||||
|
//
|
||||||
|
// Programs that use Contexts should follow these rules to keep interfaces
|
||||||
|
// consistent across packages and enable static analysis tools to check context
|
||||||
|
// propagation:
|
||||||
|
//
|
||||||
|
// Do not store Contexts inside a struct type; instead, pass a Context
|
||||||
|
// explicitly to each function that needs it. The Context should be the first
|
||||||
|
// parameter, typically named ctx:
|
||||||
|
//
|
||||||
|
// func DoSomething(ctx context.Context, arg Arg) error {
|
||||||
|
// // ... use ctx ...
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Do not pass a nil Context, even if a function permits it. Pass context.TODO
|
||||||
|
// if you are unsure about which Context to use.
|
||||||
|
//
|
||||||
|
// Use context Values only for request-scoped data that transits processes and
|
||||||
|
// APIs, not for passing optional parameters to functions.
|
||||||
|
//
|
||||||
|
// The same Context may be passed to functions running in different goroutines;
|
||||||
|
// Contexts are safe for simultaneous use by multiple goroutines.
|
||||||
|
//
|
||||||
|
// See http://blog.golang.org/context for example code for a server that uses
|
||||||
|
// Contexts.
|
||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Context carries a deadline, a cancelation signal, and other values across
|
||||||
|
// API boundaries.
|
||||||
|
//
|
||||||
|
// Context's methods may be called by multiple goroutines simultaneously.
|
||||||
|
type Context interface {
|
||||||
|
// Deadline returns the time when work done on behalf of this context
|
||||||
|
// should be canceled. Deadline returns ok==false when no deadline is
|
||||||
|
// set. Successive calls to Deadline return the same results.
|
||||||
|
Deadline() (deadline time.Time, ok bool)
|
||||||
|
|
||||||
|
// Done returns a channel that's closed when work done on behalf of this
|
||||||
|
// context should be canceled. Done may return nil if this context can
|
||||||
|
// never be canceled. Successive calls to Done return the same value.
|
||||||
|
//
|
||||||
|
// WithCancel arranges for Done to be closed when cancel is called;
|
||||||
|
// WithDeadline arranges for Done to be closed when the deadline
|
||||||
|
// expires; WithTimeout arranges for Done to be closed when the timeout
|
||||||
|
// elapses.
|
||||||
|
//
|
||||||
|
// Done is provided for use in select statements:
|
||||||
|
//
|
||||||
|
// // DoSomething calls DoSomethingSlow and returns as soon as
|
||||||
|
// // it returns or ctx.Done is closed.
|
||||||
|
// func DoSomething(ctx context.Context) (Result, error) {
|
||||||
|
// c := make(chan Result, 1)
|
||||||
|
// go func() { c <- DoSomethingSlow(ctx) }()
|
||||||
|
// select {
|
||||||
|
// case res := <-c:
|
||||||
|
// return res, nil
|
||||||
|
// case <-ctx.Done():
|
||||||
|
// return nil, ctx.Err()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// See http://blog.golang.org/pipelines for more examples of how to use
|
||||||
|
// a Done channel for cancelation.
|
||||||
|
Done() <-chan struct{}
|
||||||
|
|
||||||
|
// Err returns a non-nil error value after Done is closed. Err returns
|
||||||
|
// Canceled if the context was canceled or DeadlineExceeded if the
|
||||||
|
// context's deadline passed. No other values for Err are defined.
|
||||||
|
// After Done is closed, successive calls to Err return the same value.
|
||||||
|
Err() error
|
||||||
|
|
||||||
|
// Value returns the value associated with this context for key, or nil
|
||||||
|
// if no value is associated with key. Successive calls to Value with
|
||||||
|
// the same key returns the same result.
|
||||||
|
//
|
||||||
|
// Use context values only for request-scoped data that transits
|
||||||
|
// processes and API boundaries, not for passing optional parameters to
|
||||||
|
// functions.
|
||||||
|
//
|
||||||
|
// A key identifies a specific value in a Context. Functions that wish
|
||||||
|
// to store values in Context typically allocate a key in a global
|
||||||
|
// variable then use that key as the argument to context.WithValue and
|
||||||
|
// Context.Value. A key can be any type that supports equality;
|
||||||
|
// packages should define keys as an unexported type to avoid
|
||||||
|
// collisions.
|
||||||
|
//
|
||||||
|
// Packages that define a Context key should provide type-safe accessors
|
||||||
|
// for the values stores using that key:
|
||||||
|
//
|
||||||
|
// // Package user defines a User type that's stored in Contexts.
|
||||||
|
// package user
|
||||||
|
//
|
||||||
|
// import "code.google.com/p/go.net/context"
|
||||||
|
//
|
||||||
|
// // User is the type of value stored in the Contexts.
|
||||||
|
// type User struct {...}
|
||||||
|
//
|
||||||
|
// // key is an unexported type for keys defined in this package.
|
||||||
|
// // This prevents collisions with keys defined in other packages.
|
||||||
|
// type key int
|
||||||
|
//
|
||||||
|
// // userKey is the key for user.User values in Contexts. It is
|
||||||
|
// // unexported; clients use user.NewContext and user.FromContext
|
||||||
|
// // instead of using this key directly.
|
||||||
|
// var userKey key = 0
|
||||||
|
//
|
||||||
|
// // NewContext returns a new Context that carries value u.
|
||||||
|
// func NewContext(ctx context.Context, u *User) context.Context {
|
||||||
|
// return context.WithValue(userKey, u)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // FromContext returns the User value stored in ctx, if any.
|
||||||
|
// func FromContext(ctx context.Context) (*User, bool) {
|
||||||
|
// u, ok := ctx.Value(userKey).(*User)
|
||||||
|
// return u, ok
|
||||||
|
// }
|
||||||
|
Value(key interface{}) interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canceled is the error returned by Context.Err when the context is canceled.
|
||||||
|
var Canceled = errors.New("context canceled")
|
||||||
|
|
||||||
|
// DeadlineExceeded is the error returned by Context.Err when the context's
|
||||||
|
// deadline passes.
|
||||||
|
var DeadlineExceeded = errors.New("context deadline exceeded")
|
||||||
|
|
||||||
|
// An emptyCtx is never canceled, has no values, and has no deadline.
|
||||||
|
type emptyCtx int
|
||||||
|
|
||||||
|
func (emptyCtx) Deadline() (deadline time.Time, ok bool) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (emptyCtx) Done() <-chan struct{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (emptyCtx) Err() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (emptyCtx) Value(key interface{}) interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n emptyCtx) String() string {
|
||||||
|
switch n {
|
||||||
|
case background:
|
||||||
|
return "context.Background"
|
||||||
|
case todo:
|
||||||
|
return "context.TODO"
|
||||||
|
}
|
||||||
|
return "unknown empty Context"
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
background emptyCtx = 1
|
||||||
|
todo emptyCtx = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Background returns a non-nil, empty Context. It is never canceled, has no
|
||||||
|
// values, and has no deadline. It is typically used by the main function,
|
||||||
|
// initialization, and tests, and as the top-level Context for incoming
|
||||||
|
// requests.
|
||||||
|
func Background() Context {
|
||||||
|
return background
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO returns a non-nil, empty Context. Code should use context.TODO when
|
||||||
|
// it's unclear which Context to use or it's is not yet available (because the
|
||||||
|
// surrounding function has not yet been extended to accept a Context
|
||||||
|
// parameter). TODO is recognized by static analysis tools that determine
|
||||||
|
// whether Contexts are propagated correctly in a program.
|
||||||
|
func TODO() Context {
|
||||||
|
return todo
|
||||||
|
}
|
||||||
|
|
||||||
|
// A CancelFunc tells an operation to abandon its work.
|
||||||
|
// A CancelFunc does not wait for the work to stop.
|
||||||
|
// After the first call, subsequent calls to a CancelFunc do nothing.
|
||||||
|
type CancelFunc func()
|
||||||
|
|
||||||
|
// WithCancel returns a copy of parent with a new Done channel. The returned
|
||||||
|
// context's Done channel is closed when the returned cancel function is called
|
||||||
|
// or when the parent context's Done channel is closed, whichever happens first.
|
||||||
|
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
|
||||||
|
c := newCancelCtx(parent)
|
||||||
|
propagateCancel(parent, &c)
|
||||||
|
return &c, func() { c.cancel(true, Canceled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// newCancelCtx returns an initialized cancelCtx.
|
||||||
|
func newCancelCtx(parent Context) cancelCtx {
|
||||||
|
return cancelCtx{
|
||||||
|
Context: parent,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// propagateCancel arranges for child to be canceled when parent is.
|
||||||
|
func propagateCancel(parent Context, child canceler) {
|
||||||
|
if parent.Done() == nil {
|
||||||
|
return // parent is never canceled
|
||||||
|
}
|
||||||
|
if p, ok := parentCancelCtx(parent); ok {
|
||||||
|
p.mu.Lock()
|
||||||
|
if p.err != nil {
|
||||||
|
// parent has already been canceled
|
||||||
|
child.cancel(false, p.err)
|
||||||
|
} else {
|
||||||
|
if p.children == nil {
|
||||||
|
p.children = make(map[canceler]bool)
|
||||||
|
}
|
||||||
|
p.children[child] = true
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
} else {
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-parent.Done():
|
||||||
|
child.cancel(false, parent.Err())
|
||||||
|
case <-child.Done():
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parentCancelCtx follows a chain of parent references until it finds a
|
||||||
|
// *cancelCtx. This function understands how each of the concrete types in this
|
||||||
|
// package represents its parent.
|
||||||
|
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
|
||||||
|
for {
|
||||||
|
switch c := parent.(type) {
|
||||||
|
case *cancelCtx:
|
||||||
|
return c, true
|
||||||
|
case *timerCtx:
|
||||||
|
return &c.cancelCtx, true
|
||||||
|
case *valueCtx:
|
||||||
|
parent = c.Context
|
||||||
|
default:
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A canceler is a context type that can be canceled directly. The
|
||||||
|
// implementations are *cancelCtx and *timerCtx.
|
||||||
|
type canceler interface {
|
||||||
|
cancel(removeFromParent bool, err error)
|
||||||
|
Done() <-chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A cancelCtx can be canceled. When canceled, it also cancels any children
|
||||||
|
// that implement canceler.
|
||||||
|
type cancelCtx struct {
|
||||||
|
Context
|
||||||
|
|
||||||
|
done chan struct{} // closed by the first cancel call.
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
children map[canceler]bool // set to nil by the first cancel call
|
||||||
|
err error // set to non-nil by the first cancel call
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cancelCtx) Done() <-chan struct{} {
|
||||||
|
return c.done
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cancelCtx) Err() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cancelCtx) String() string {
|
||||||
|
return fmt.Sprintf("%v.WithCancel", c.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancel closes c.done, cancels each of c's children, and, if
|
||||||
|
// removeFromParent is true, removes c from its parent's children.
|
||||||
|
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
|
||||||
|
if err == nil {
|
||||||
|
panic("context: internal error: missing cancel error")
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.err != nil {
|
||||||
|
c.mu.Unlock()
|
||||||
|
return // already canceled
|
||||||
|
}
|
||||||
|
c.err = err
|
||||||
|
close(c.done)
|
||||||
|
for child := range c.children {
|
||||||
|
// NOTE: acquiring the child's lock while holding parent's lock.
|
||||||
|
child.cancel(false, err)
|
||||||
|
}
|
||||||
|
c.children = nil
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if removeFromParent {
|
||||||
|
if p, ok := parentCancelCtx(c.Context); ok {
|
||||||
|
p.mu.Lock()
|
||||||
|
if p.children != nil {
|
||||||
|
delete(p.children, c)
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDeadline returns a copy of the parent context with the deadline adjusted
|
||||||
|
// to be no later than d. If the parent's deadline is already earlier than d,
|
||||||
|
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
|
||||||
|
// context's Done channel is closed when the deadline expires, when the returned
|
||||||
|
// cancel function is called, or when the parent context's Done channel is
|
||||||
|
// closed, whichever happens first.
|
||||||
|
//
|
||||||
|
// Canceling this context releases resources associated with the deadline
|
||||||
|
// timer, so code should call cancel as soon as the operations running in this
|
||||||
|
// Context complete.
|
||||||
|
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
|
||||||
|
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
|
||||||
|
// The current deadline is already sooner than the new one.
|
||||||
|
return WithCancel(parent)
|
||||||
|
}
|
||||||
|
c := &timerCtx{
|
||||||
|
cancelCtx: newCancelCtx(parent),
|
||||||
|
deadline: deadline,
|
||||||
|
}
|
||||||
|
propagateCancel(parent, c)
|
||||||
|
d := deadline.Sub(time.Now())
|
||||||
|
if d <= 0 {
|
||||||
|
c.cancel(true, DeadlineExceeded) // deadline has already passed
|
||||||
|
return c, func() { c.cancel(true, Canceled) }
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
if c.err == nil {
|
||||||
|
c.timer = time.AfterFunc(d, func() {
|
||||||
|
c.cancel(true, DeadlineExceeded)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c, func() { c.cancel(true, Canceled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
|
||||||
|
// implement Done and Err. It implements cancel by stopping its timer then
|
||||||
|
// delegating to cancelCtx.cancel.
|
||||||
|
type timerCtx struct {
|
||||||
|
cancelCtx
|
||||||
|
timer *time.Timer // Under cancelCtx.mu.
|
||||||
|
|
||||||
|
deadline time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
|
||||||
|
return c.deadline, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *timerCtx) String() string {
|
||||||
|
return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, c.deadline.Sub(time.Now()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *timerCtx) cancel(removeFromParent bool, err error) {
|
||||||
|
c.cancelCtx.cancel(removeFromParent, err)
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.timer != nil {
|
||||||
|
c.timer.Stop()
|
||||||
|
c.timer = nil
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
|
||||||
|
//
|
||||||
|
// Canceling this context releases resources associated with the deadline
|
||||||
|
// timer, so code should call cancel as soon as the operations running in this
|
||||||
|
// Context complete:
|
||||||
|
//
|
||||||
|
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
|
||||||
|
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||||
|
// defer cancel() // releases resources if slowOperation completes before timeout elapses
|
||||||
|
// return slowOperation(ctx)
|
||||||
|
// }
|
||||||
|
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
|
||||||
|
return WithDeadline(parent, time.Now().Add(timeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithValue returns a copy of parent in which the value associated with key is
|
||||||
|
// val.
|
||||||
|
//
|
||||||
|
// Use context Values only for request-scoped data that transits processes and
|
||||||
|
// APIs, not for passing optional parameters to functions.
|
||||||
|
func WithValue(parent Context, key interface{}, val interface{}) Context {
|
||||||
|
return &valueCtx{parent, key, val}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A valueCtx carries a key-value pair. It implements Value for that key and
|
||||||
|
// delegates all other calls to the embedded Context.
|
||||||
|
type valueCtx struct {
|
||||||
|
Context
|
||||||
|
key, val interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *valueCtx) String() string {
|
||||||
|
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *valueCtx) Value(key interface{}) interface{} {
|
||||||
|
if c.key == key {
|
||||||
|
return c.val
|
||||||
|
}
|
||||||
|
return c.Context.Value(key)
|
||||||
|
}
|
553
Godeps/_workspace/src/code.google.com/p/go.net/context/context_test.go
generated
vendored
Normal file
553
Godeps/_workspace/src/code.google.com/p/go.net/context/context_test.go
generated
vendored
Normal file
@ -0,0 +1,553 @@
|
|||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// otherContext is a Context that's not one of the types defined in context.go.
|
||||||
|
// This lets us test code paths that differ based on the underlying type of the
|
||||||
|
// Context.
|
||||||
|
type otherContext struct {
|
||||||
|
Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackground(t *testing.T) {
|
||||||
|
c := Background()
|
||||||
|
if c == nil {
|
||||||
|
t.Fatalf("Background returned nil")
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case x := <-c.Done():
|
||||||
|
t.Errorf("<-c.Done() == %v want nothing (it should block)", x)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if got, want := fmt.Sprint(c), "context.Background"; got != want {
|
||||||
|
t.Errorf("Background().String() = %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTODO(t *testing.T) {
|
||||||
|
c := TODO()
|
||||||
|
if c == nil {
|
||||||
|
t.Fatalf("TODO returned nil")
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case x := <-c.Done():
|
||||||
|
t.Errorf("<-c.Done() == %v want nothing (it should block)", x)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if got, want := fmt.Sprint(c), "context.TODO"; got != want {
|
||||||
|
t.Errorf("TODO().String() = %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithCancel(t *testing.T) {
|
||||||
|
c1, cancel := WithCancel(Background())
|
||||||
|
|
||||||
|
if got, want := fmt.Sprint(c1), "context.Background.WithCancel"; got != want {
|
||||||
|
t.Errorf("c1.String() = %q want %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
o := otherContext{c1}
|
||||||
|
c2, _ := WithCancel(o)
|
||||||
|
contexts := []Context{c1, o, c2}
|
||||||
|
|
||||||
|
for i, c := range contexts {
|
||||||
|
if d := c.Done(); d == nil {
|
||||||
|
t.Errorf("c[%d].Done() == %v want non-nil", i, d)
|
||||||
|
}
|
||||||
|
if e := c.Err(); e != nil {
|
||||||
|
t.Errorf("c[%d].Err() == %v want nil", i, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case x := <-c.Done():
|
||||||
|
t.Errorf("<-c.Done() == %v want nothing (it should block)", x)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
time.Sleep(100 * time.Millisecond) // let cancelation propagate
|
||||||
|
|
||||||
|
for i, c := range contexts {
|
||||||
|
select {
|
||||||
|
case <-c.Done():
|
||||||
|
default:
|
||||||
|
t.Errorf("<-c[%d].Done() blocked, but shouldn't have", i)
|
||||||
|
}
|
||||||
|
if e := c.Err(); e != Canceled {
|
||||||
|
t.Errorf("c[%d].Err() == %v want %v", i, e, Canceled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParentFinishesChild(t *testing.T) {
|
||||||
|
// Context tree:
|
||||||
|
// parent -> cancelChild
|
||||||
|
// parent -> valueChild -> timerChild
|
||||||
|
parent, cancel := WithCancel(Background())
|
||||||
|
cancelChild, stop := WithCancel(parent)
|
||||||
|
defer stop()
|
||||||
|
valueChild := WithValue(parent, "key", "value")
|
||||||
|
timerChild, stop := WithTimeout(valueChild, 10000*time.Hour)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case x := <-parent.Done():
|
||||||
|
t.Errorf("<-parent.Done() == %v want nothing (it should block)", x)
|
||||||
|
case x := <-cancelChild.Done():
|
||||||
|
t.Errorf("<-cancelChild.Done() == %v want nothing (it should block)", x)
|
||||||
|
case x := <-timerChild.Done():
|
||||||
|
t.Errorf("<-timerChild.Done() == %v want nothing (it should block)", x)
|
||||||
|
case x := <-valueChild.Done():
|
||||||
|
t.Errorf("<-valueChild.Done() == %v want nothing (it should block)", x)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// The parent's children should contain the two cancelable children.
|
||||||
|
pc := parent.(*cancelCtx)
|
||||||
|
cc := cancelChild.(*cancelCtx)
|
||||||
|
tc := timerChild.(*timerCtx)
|
||||||
|
pc.mu.Lock()
|
||||||
|
if len(pc.children) != 2 || !pc.children[cc] || !pc.children[tc] {
|
||||||
|
t.Errorf("bad linkage: pc.children = %v, want %v and %v",
|
||||||
|
pc.children, cc, tc)
|
||||||
|
}
|
||||||
|
pc.mu.Unlock()
|
||||||
|
|
||||||
|
if p, ok := parentCancelCtx(cc.Context); !ok || p != pc {
|
||||||
|
t.Errorf("bad linkage: parentCancelCtx(cancelChild.Context) = %v, %v want %v, true", p, ok, pc)
|
||||||
|
}
|
||||||
|
if p, ok := parentCancelCtx(tc.Context); !ok || p != pc {
|
||||||
|
t.Errorf("bad linkage: parentCancelCtx(timerChild.Context) = %v, %v want %v, true", p, ok, pc)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
pc.mu.Lock()
|
||||||
|
if len(pc.children) != 0 {
|
||||||
|
t.Errorf("pc.cancel didn't clear pc.children = %v", pc.children)
|
||||||
|
}
|
||||||
|
pc.mu.Unlock()
|
||||||
|
|
||||||
|
// parent and children should all be finished.
|
||||||
|
check := func(ctx Context, name string) {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
default:
|
||||||
|
t.Errorf("<-%s.Done() blocked, but shouldn't have", name)
|
||||||
|
}
|
||||||
|
if e := ctx.Err(); e != Canceled {
|
||||||
|
t.Errorf("%s.Err() == %v want %v", name, e, Canceled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(parent, "parent")
|
||||||
|
check(cancelChild, "cancelChild")
|
||||||
|
check(valueChild, "valueChild")
|
||||||
|
check(timerChild, "timerChild")
|
||||||
|
|
||||||
|
// WithCancel should return a canceled context on a canceled parent.
|
||||||
|
precanceledChild := WithValue(parent, "key", "value")
|
||||||
|
select {
|
||||||
|
case <-precanceledChild.Done():
|
||||||
|
default:
|
||||||
|
t.Errorf("<-precanceledChild.Done() blocked, but shouldn't have")
|
||||||
|
}
|
||||||
|
if e := precanceledChild.Err(); e != Canceled {
|
||||||
|
t.Errorf("precanceledChild.Err() == %v want %v", e, Canceled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChildFinishesFirst(t *testing.T) {
|
||||||
|
cancelable, stop := WithCancel(Background())
|
||||||
|
defer stop()
|
||||||
|
for _, parent := range []Context{Background(), cancelable} {
|
||||||
|
child, cancel := WithCancel(parent)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case x := <-parent.Done():
|
||||||
|
t.Errorf("<-parent.Done() == %v want nothing (it should block)", x)
|
||||||
|
case x := <-child.Done():
|
||||||
|
t.Errorf("<-child.Done() == %v want nothing (it should block)", x)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := child.(*cancelCtx)
|
||||||
|
pc, pcok := parent.(*cancelCtx) // pcok == false when parent == Background()
|
||||||
|
if p, ok := parentCancelCtx(cc.Context); ok != pcok || (ok && pc != p) {
|
||||||
|
t.Errorf("bad linkage: parentCancelCtx(cc.Context) = %v, %v want %v, %v", p, ok, pc, pcok)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pcok {
|
||||||
|
pc.mu.Lock()
|
||||||
|
if len(pc.children) != 1 || !pc.children[cc] {
|
||||||
|
t.Errorf("bad linkage: pc.children = %v, cc = %v", pc.children, cc)
|
||||||
|
}
|
||||||
|
pc.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if pcok {
|
||||||
|
pc.mu.Lock()
|
||||||
|
if len(pc.children) != 0 {
|
||||||
|
t.Errorf("child's cancel didn't remove self from pc.children = %v", pc.children)
|
||||||
|
}
|
||||||
|
pc.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// child should be finished.
|
||||||
|
select {
|
||||||
|
case <-child.Done():
|
||||||
|
default:
|
||||||
|
t.Errorf("<-child.Done() blocked, but shouldn't have")
|
||||||
|
}
|
||||||
|
if e := child.Err(); e != Canceled {
|
||||||
|
t.Errorf("child.Err() == %v want %v", e, Canceled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parent should not be finished.
|
||||||
|
select {
|
||||||
|
case x := <-parent.Done():
|
||||||
|
t.Errorf("<-parent.Done() == %v want nothing (it should block)", x)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if e := parent.Err(); e != nil {
|
||||||
|
t.Errorf("parent.Err() == %v want nil", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeadline(c Context, wait time.Duration, t *testing.T) {
|
||||||
|
select {
|
||||||
|
case <-time.After(wait):
|
||||||
|
t.Fatalf("context should have timed out")
|
||||||
|
case <-c.Done():
|
||||||
|
}
|
||||||
|
if e := c.Err(); e != DeadlineExceeded {
|
||||||
|
t.Errorf("c.Err() == %v want %v", e, DeadlineExceeded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeadline(t *testing.T) {
|
||||||
|
c, _ := WithDeadline(Background(), time.Now().Add(100*time.Millisecond))
|
||||||
|
if got, prefix := fmt.Sprint(c), "context.Background.WithDeadline("; !strings.HasPrefix(got, prefix) {
|
||||||
|
t.Errorf("c.String() = %q want prefix %q", got, prefix)
|
||||||
|
}
|
||||||
|
testDeadline(c, 200*time.Millisecond, t)
|
||||||
|
|
||||||
|
c, _ = WithDeadline(Background(), time.Now().Add(100*time.Millisecond))
|
||||||
|
o := otherContext{c}
|
||||||
|
testDeadline(o, 200*time.Millisecond, t)
|
||||||
|
|
||||||
|
c, _ = WithDeadline(Background(), time.Now().Add(100*time.Millisecond))
|
||||||
|
o = otherContext{c}
|
||||||
|
c, _ = WithDeadline(o, time.Now().Add(300*time.Millisecond))
|
||||||
|
testDeadline(c, 200*time.Millisecond, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeout(t *testing.T) {
|
||||||
|
c, _ := WithTimeout(Background(), 100*time.Millisecond)
|
||||||
|
if got, prefix := fmt.Sprint(c), "context.Background.WithDeadline("; !strings.HasPrefix(got, prefix) {
|
||||||
|
t.Errorf("c.String() = %q want prefix %q", got, prefix)
|
||||||
|
}
|
||||||
|
testDeadline(c, 200*time.Millisecond, t)
|
||||||
|
|
||||||
|
c, _ = WithTimeout(Background(), 100*time.Millisecond)
|
||||||
|
o := otherContext{c}
|
||||||
|
testDeadline(o, 200*time.Millisecond, t)
|
||||||
|
|
||||||
|
c, _ = WithTimeout(Background(), 100*time.Millisecond)
|
||||||
|
o = otherContext{c}
|
||||||
|
c, _ = WithTimeout(o, 300*time.Millisecond)
|
||||||
|
testDeadline(c, 200*time.Millisecond, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanceledTimeout(t *testing.T) {
|
||||||
|
c, _ := WithTimeout(Background(), 200*time.Millisecond)
|
||||||
|
o := otherContext{c}
|
||||||
|
c, cancel := WithTimeout(o, 400*time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
time.Sleep(100 * time.Millisecond) // let cancelation propagate
|
||||||
|
select {
|
||||||
|
case <-c.Done():
|
||||||
|
default:
|
||||||
|
t.Errorf("<-c.Done() blocked, but shouldn't have")
|
||||||
|
}
|
||||||
|
if e := c.Err(); e != Canceled {
|
||||||
|
t.Errorf("c.Err() == %v want %v", e, Canceled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type key1 int
|
||||||
|
type key2 int
|
||||||
|
|
||||||
|
var k1 = key1(1)
|
||||||
|
var k2 = key2(1) // same int as k1, different type
|
||||||
|
var k3 = key2(3) // same type as k2, different int
|
||||||
|
|
||||||
|
func TestValues(t *testing.T) {
|
||||||
|
check := func(c Context, nm, v1, v2, v3 string) {
|
||||||
|
if v, ok := c.Value(k1).(string); ok == (len(v1) == 0) || v != v1 {
|
||||||
|
t.Errorf(`%s.Value(k1).(string) = %q, %t want %q, %t`, nm, v, ok, v1, len(v1) != 0)
|
||||||
|
}
|
||||||
|
if v, ok := c.Value(k2).(string); ok == (len(v2) == 0) || v != v2 {
|
||||||
|
t.Errorf(`%s.Value(k2).(string) = %q, %t want %q, %t`, nm, v, ok, v2, len(v2) != 0)
|
||||||
|
}
|
||||||
|
if v, ok := c.Value(k3).(string); ok == (len(v3) == 0) || v != v3 {
|
||||||
|
t.Errorf(`%s.Value(k3).(string) = %q, %t want %q, %t`, nm, v, ok, v3, len(v3) != 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c0 := Background()
|
||||||
|
check(c0, "c0", "", "", "")
|
||||||
|
|
||||||
|
c1 := WithValue(Background(), k1, "c1k1")
|
||||||
|
check(c1, "c1", "c1k1", "", "")
|
||||||
|
|
||||||
|
if got, want := fmt.Sprint(c1), `context.Background.WithValue(1, "c1k1")`; got != want {
|
||||||
|
t.Errorf("c.String() = %q want %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
c2 := WithValue(c1, k2, "c2k2")
|
||||||
|
check(c2, "c2", "c1k1", "c2k2", "")
|
||||||
|
|
||||||
|
c3 := WithValue(c2, k3, "c3k3")
|
||||||
|
check(c3, "c2", "c1k1", "c2k2", "c3k3")
|
||||||
|
|
||||||
|
c4 := WithValue(c3, k1, nil)
|
||||||
|
check(c4, "c4", "", "c2k2", "c3k3")
|
||||||
|
|
||||||
|
o0 := otherContext{Background()}
|
||||||
|
check(o0, "o0", "", "", "")
|
||||||
|
|
||||||
|
o1 := otherContext{WithValue(Background(), k1, "c1k1")}
|
||||||
|
check(o1, "o1", "c1k1", "", "")
|
||||||
|
|
||||||
|
o2 := WithValue(o1, k2, "o2k2")
|
||||||
|
check(o2, "o2", "c1k1", "o2k2", "")
|
||||||
|
|
||||||
|
o3 := otherContext{c4}
|
||||||
|
check(o3, "o3", "", "c2k2", "c3k3")
|
||||||
|
|
||||||
|
o4 := WithValue(o3, k3, nil)
|
||||||
|
check(o4, "o4", "", "c2k2", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllocs(t *testing.T) {
|
||||||
|
bg := Background()
|
||||||
|
for _, test := range []struct {
|
||||||
|
desc string
|
||||||
|
f func()
|
||||||
|
limit float64
|
||||||
|
gccgoLimit float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Background()",
|
||||||
|
f: func() { Background() },
|
||||||
|
limit: 0,
|
||||||
|
gccgoLimit: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: fmt.Sprintf("WithValue(bg, %v, nil)", k1),
|
||||||
|
f: func() {
|
||||||
|
c := WithValue(bg, k1, nil)
|
||||||
|
c.Value(k1)
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
gccgoLimit: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "WithTimeout(bg, 15*time.Millisecond)",
|
||||||
|
f: func() {
|
||||||
|
c, _ := WithTimeout(bg, 15*time.Millisecond)
|
||||||
|
<-c.Done()
|
||||||
|
},
|
||||||
|
limit: 8,
|
||||||
|
gccgoLimit: 13,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "WithCancel(bg)",
|
||||||
|
f: func() {
|
||||||
|
c, cancel := WithCancel(bg)
|
||||||
|
cancel()
|
||||||
|
<-c.Done()
|
||||||
|
},
|
||||||
|
limit: 5,
|
||||||
|
gccgoLimit: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "WithTimeout(bg, 100*time.Millisecond)",
|
||||||
|
f: func() {
|
||||||
|
c, cancel := WithTimeout(bg, 100*time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
<-c.Done()
|
||||||
|
},
|
||||||
|
limit: 8,
|
||||||
|
gccgoLimit: 25,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
limit := test.limit
|
||||||
|
if runtime.Compiler == "gccgo" {
|
||||||
|
// gccgo does not yet do escape analysis.
|
||||||
|
// TOOD(iant): Remove this when gccgo does do escape analysis.
|
||||||
|
limit = test.gccgoLimit
|
||||||
|
}
|
||||||
|
if n := testing.AllocsPerRun(100, test.f); n > limit {
|
||||||
|
t.Errorf("%s allocs = %f want %d", test.desc, n, int(limit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimultaneousCancels(t *testing.T) {
|
||||||
|
root, cancel := WithCancel(Background())
|
||||||
|
m := map[Context]CancelFunc{root: cancel}
|
||||||
|
q := []Context{root}
|
||||||
|
// Create a tree of contexts.
|
||||||
|
for len(q) != 0 && len(m) < 100 {
|
||||||
|
parent := q[0]
|
||||||
|
q = q[1:]
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
ctx, cancel := WithCancel(parent)
|
||||||
|
m[ctx] = cancel
|
||||||
|
q = append(q, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Start all the cancels in a random order.
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(len(m))
|
||||||
|
for _, cancel := range m {
|
||||||
|
go func(cancel CancelFunc) {
|
||||||
|
cancel()
|
||||||
|
wg.Done()
|
||||||
|
}(cancel)
|
||||||
|
}
|
||||||
|
// Wait on all the contexts in a random order.
|
||||||
|
for ctx := range m {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
buf := make([]byte, 10<<10)
|
||||||
|
n := runtime.Stack(buf, true)
|
||||||
|
t.Fatalf("timed out waiting for <-ctx.Done(); stacks:\n%s", buf[:n])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Wait for all the cancel functions to return.
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
buf := make([]byte, 10<<10)
|
||||||
|
n := runtime.Stack(buf, true)
|
||||||
|
t.Fatalf("timed out waiting for cancel functions; stacks:\n%s", buf[:n])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterlockedCancels(t *testing.T) {
|
||||||
|
parent, cancelParent := WithCancel(Background())
|
||||||
|
child, cancelChild := WithCancel(parent)
|
||||||
|
go func() {
|
||||||
|
parent.Done()
|
||||||
|
cancelChild()
|
||||||
|
}()
|
||||||
|
cancelParent()
|
||||||
|
select {
|
||||||
|
case <-child.Done():
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
buf := make([]byte, 10<<10)
|
||||||
|
n := runtime.Stack(buf, true)
|
||||||
|
t.Fatalf("timed out waiting for child.Done(); stacks:\n%s", buf[:n])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLayersCancel(t *testing.T) {
|
||||||
|
testLayers(t, time.Now().UnixNano(), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLayersTimeout(t *testing.T) {
|
||||||
|
testLayers(t, time.Now().UnixNano(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLayers(t *testing.T, seed int64, testTimeout bool) {
|
||||||
|
rand.Seed(seed)
|
||||||
|
errorf := func(format string, a ...interface{}) {
|
||||||
|
t.Errorf(fmt.Sprintf("seed=%d: %s", seed, format), a...)
|
||||||
|
}
|
||||||
|
const (
|
||||||
|
timeout = 200 * time.Millisecond
|
||||||
|
minLayers = 30
|
||||||
|
)
|
||||||
|
type value int
|
||||||
|
var (
|
||||||
|
vals []*value
|
||||||
|
cancels []CancelFunc
|
||||||
|
numTimers int
|
||||||
|
ctx = Background()
|
||||||
|
)
|
||||||
|
for i := 0; i < minLayers || numTimers == 0 || len(cancels) == 0 || len(vals) == 0; i++ {
|
||||||
|
switch rand.Intn(3) {
|
||||||
|
case 0:
|
||||||
|
v := new(value)
|
||||||
|
ctx = WithValue(ctx, v, v)
|
||||||
|
vals = append(vals, v)
|
||||||
|
case 1:
|
||||||
|
var cancel CancelFunc
|
||||||
|
ctx, cancel = WithCancel(ctx)
|
||||||
|
cancels = append(cancels, cancel)
|
||||||
|
case 2:
|
||||||
|
var cancel CancelFunc
|
||||||
|
ctx, cancel = WithTimeout(ctx, timeout)
|
||||||
|
cancels = append(cancels, cancel)
|
||||||
|
numTimers++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkValues := func(when string) {
|
||||||
|
for _, key := range vals {
|
||||||
|
if val := ctx.Value(key).(*value); key != val {
|
||||||
|
errorf("%s: ctx.Value(%p) = %p want %p", when, key, val, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
errorf("ctx should not be canceled yet")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if s, prefix := fmt.Sprint(ctx), "context.Background."; !strings.HasPrefix(s, prefix) {
|
||||||
|
t.Errorf("ctx.String() = %q want prefix %q", s, prefix)
|
||||||
|
}
|
||||||
|
t.Log(ctx)
|
||||||
|
checkValues("before cancel")
|
||||||
|
if testTimeout {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-time.After(timeout + timeout/10):
|
||||||
|
errorf("ctx should have timed out")
|
||||||
|
}
|
||||||
|
checkValues("after timeout")
|
||||||
|
} else {
|
||||||
|
cancel := cancels[rand.Intn(len(cancels))]
|
||||||
|
cancel()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
default:
|
||||||
|
errorf("ctx should be canceled")
|
||||||
|
}
|
||||||
|
checkValues("after cancel")
|
||||||
|
}
|
||||||
|
}
|
26
Godeps/_workspace/src/code.google.com/p/go.net/context/withtimeout_test.go
generated
vendored
Normal file
26
Godeps/_workspace/src/code.google.com/p/go.net/context/withtimeout_test.go
generated
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package context_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/go.net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleWithTimeout() {
|
||||||
|
// Pass a context with a timeout to tell a blocking function that it
|
||||||
|
// should abandon its work after the timeout elapses.
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||||
|
select {
|
||||||
|
case <-time.After(200 * time.Millisecond):
|
||||||
|
fmt.Println("overslept")
|
||||||
|
case <-ctx.Done():
|
||||||
|
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
|
||||||
|
}
|
||||||
|
// Output:
|
||||||
|
// context deadline exceeded
|
||||||
|
}
|
@ -44,7 +44,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
. "./testdata"
|
. "./testdata"
|
||||||
. "github.com/coreos/etcd/third_party/code.google.com/p/gogoprotobuf/proto"
|
. "github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
var globalO *Buffer
|
var globalO *Buffer
|
@ -34,7 +34,7 @@ package proto_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/coreos/etcd/third_party/code.google.com/p/gogoprotobuf/proto"
|
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto"
|
||||||
|
|
||||||
pb "./testdata"
|
pb "./testdata"
|
||||||
)
|
)
|
@ -35,7 +35,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
pb "./testdata"
|
pb "./testdata"
|
||||||
. "github.com/coreos/etcd/third_party/code.google.com/p/gogoprotobuf/proto"
|
. "github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Four identical base messages.
|
// Four identical base messages.
|
@ -89,7 +89,7 @@
|
|||||||
|
|
||||||
package example
|
package example
|
||||||
|
|
||||||
import "github.com/coreos/etcd/third_party/code.google.com/p/gogoprotobuf/proto"
|
import "code.google.com/p/gogoprotobuf/proto"
|
||||||
|
|
||||||
type FOO int32
|
type FOO int32
|
||||||
const (
|
const (
|
||||||
@ -168,7 +168,7 @@
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/coreos/etcd/third_party/code.google.com/p/gogoprotobuf/proto"
|
"code.google.com/p/gogoprotobuf/proto"
|
||||||
"./example.pb"
|
"./example.pb"
|
||||||
)
|
)
|
||||||
|
|
@ -36,7 +36,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
pb "./testdata"
|
pb "./testdata"
|
||||||
. "github.com/coreos/etcd/third_party/code.google.com/p/gogoprotobuf/proto"
|
. "github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
var messageWithExtension1 = &pb.MyMessage{Count: Int32(7)}
|
var messageWithExtension1 = &pb.MyMessage{Count: Int32(7)}
|
@ -36,7 +36,7 @@ It has these top-level messages:
|
|||||||
*/
|
*/
|
||||||
package testdata
|
package testdata
|
||||||
|
|
||||||
import proto "github.com/coreos/etcd/third_party/github.com/coreos/etcd/third_party/code.google.com/p/gogoprotobuf/proto"
|
import proto "github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto"
|
||||||
import json "encoding/json"
|
import json "encoding/json"
|
||||||
import math "math"
|
import math "math"
|
||||||
|
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
package testdata
|
package testdata
|
||||||
|
|
||||||
import proto "github.com/coreos/etcd/third_party/code.google.com/p/gogoprotobuf/proto"
|
import proto "code.google.com/p/gogoprotobuf/proto"
|
||||||
import json "encoding/json"
|
import json "encoding/json"
|
||||||
import math "math"
|
import math "math"
|
||||||
|
|
@ -37,7 +37,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "./testdata"
|
. "./testdata"
|
||||||
. "github.com/coreos/etcd/third_party/code.google.com/p/gogoprotobuf/proto"
|
. "github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UnmarshalTextTest struct {
|
type UnmarshalTextTest struct {
|
@ -39,7 +39,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/coreos/etcd/third_party/code.google.com/p/gogoprotobuf/proto"
|
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto"
|
||||||
|
|
||||||
pb "./testdata"
|
pb "./testdata"
|
||||||
)
|
)
|
6
Godeps/_workspace/src/github.com/codegangsta/cli/.travis.yml
generated
vendored
Normal file
6
Godeps/_workspace/src/github.com/codegangsta/cli/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
language: go
|
||||||
|
go: 1.1
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go vet ./...
|
||||||
|
- go test -v ./...
|
21
Godeps/_workspace/src/github.com/codegangsta/cli/LICENSE
generated
vendored
Normal file
21
Godeps/_workspace/src/github.com/codegangsta/cli/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
Copyright (C) 2013 Jeremy Saenz
|
||||||
|
All Rights Reserved.
|
||||||
|
|
||||||
|
MIT LICENSE
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
287
Godeps/_workspace/src/github.com/codegangsta/cli/README.md
generated
vendored
Normal file
287
Godeps/_workspace/src/github.com/codegangsta/cli/README.md
generated
vendored
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
[](https://travis-ci.org/codegangsta/cli)
|
||||||
|
|
||||||
|
# cli.go
|
||||||
|
cli.go is simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way.
|
||||||
|
|
||||||
|
You can view the API docs here:
|
||||||
|
http://godoc.org/github.com/codegangsta/cli
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Command line apps are usually so tiny that there is absolutely no reason why your code should *not* be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app.
|
||||||
|
|
||||||
|
**This is where cli.go comes into play.** cli.go makes command line programming fun, organized, and expressive!
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
Make sure you have a working Go environment (go 1.1 is *required*). [See the install instructions](http://golang.org/doc/install.html).
|
||||||
|
|
||||||
|
To install `cli.go`, simply run:
|
||||||
|
```
|
||||||
|
$ go get github.com/codegangsta/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure your `PATH` includes to the `$GOPATH/bin` directory so your commands can be easily used:
|
||||||
|
```
|
||||||
|
export PATH=$PATH:$GOPATH/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
One of the philosophies behind cli.go is that an API should be playful and full of discovery. So a cli.go app can be as little as one line of code in `main()`.
|
||||||
|
|
||||||
|
``` go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"github.com/codegangsta/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cli.NewApp().Run(os.Args)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This app will run and show help text, but is not very useful. Let's give an action to execute and some help documentation:
|
||||||
|
|
||||||
|
``` go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"github.com/codegangsta/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "boom"
|
||||||
|
app.Usage = "make an explosive entrance"
|
||||||
|
app.Action = func(c *cli.Context) {
|
||||||
|
println("boom! I say!")
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run(os.Args)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Running this already gives you a ton of functionality, plus support for things like subcommands and flags, which are covered below.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Being a programmer can be a lonely job. Thankfully by the power of automation that is not the case! Let's create a greeter app to fend off our demons of loneliness!
|
||||||
|
|
||||||
|
``` go
|
||||||
|
/* greet.go */
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"github.com/codegangsta/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "greet"
|
||||||
|
app.Usage = "fight the loneliness!"
|
||||||
|
app.Action = func(c *cli.Context) {
|
||||||
|
println("Hello friend!")
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run(os.Args)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Install our command to the `$GOPATH/bin` directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go install
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally run our new command:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ greet
|
||||||
|
Hello friend!
|
||||||
|
```
|
||||||
|
|
||||||
|
cli.go also generates some bitchass help text:
|
||||||
|
```
|
||||||
|
$ greet help
|
||||||
|
NAME:
|
||||||
|
greet - fight the loneliness!
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
greet [global options] command [command options] [arguments...]
|
||||||
|
|
||||||
|
VERSION:
|
||||||
|
0.0.0
|
||||||
|
|
||||||
|
COMMANDS:
|
||||||
|
help, h Shows a list of commands or help for one command
|
||||||
|
|
||||||
|
GLOBAL OPTIONS
|
||||||
|
--version Shows version information
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
You can lookup arguments by calling the `Args` function on `cli.Context`.
|
||||||
|
|
||||||
|
``` go
|
||||||
|
...
|
||||||
|
app.Action = func(c *cli.Context) {
|
||||||
|
println("Hello", c.Args()[0])
|
||||||
|
}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
Setting and querying flags is simple.
|
||||||
|
``` go
|
||||||
|
...
|
||||||
|
app.Flags = []cli.Flag {
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "lang",
|
||||||
|
Value: "english",
|
||||||
|
Usage: "language for the greeting",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app.Action = func(c *cli.Context) {
|
||||||
|
name := "someone"
|
||||||
|
if len(c.Args()) > 0 {
|
||||||
|
name = c.Args()[0]
|
||||||
|
}
|
||||||
|
if c.String("lang") == "spanish" {
|
||||||
|
println("Hola", name)
|
||||||
|
} else {
|
||||||
|
println("Hello", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Alternate Names
|
||||||
|
|
||||||
|
You can set alternate (or short) names for flags by providing a comma-delimited list for the `Name`. e.g.
|
||||||
|
|
||||||
|
``` go
|
||||||
|
app.Flags = []cli.Flag {
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "lang, l",
|
||||||
|
Value: "english",
|
||||||
|
Usage: "language for the greeting",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Values from the Environment
|
||||||
|
|
||||||
|
You can also have the default value set from the environment via `EnvVar`. e.g.
|
||||||
|
|
||||||
|
``` go
|
||||||
|
app.Flags = []cli.Flag {
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "lang, l",
|
||||||
|
Value: "english",
|
||||||
|
Usage: "language for the greeting",
|
||||||
|
EnvVar: "APP_LANG",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That flag can then be set with `--lang spanish` or `-l spanish`. Note that giving two different forms of the same flag in the same command invocation is an error.
|
||||||
|
|
||||||
|
### Subcommands
|
||||||
|
|
||||||
|
Subcommands can be defined for a more git-like command line app.
|
||||||
|
```go
|
||||||
|
...
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "add",
|
||||||
|
ShortName: "a",
|
||||||
|
Usage: "add a task to the list",
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
println("added task: ", c.Args().First())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "complete",
|
||||||
|
ShortName: "c",
|
||||||
|
Usage: "complete a task on the list",
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
println("completed task: ", c.Args().First())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "template",
|
||||||
|
ShortName: "r",
|
||||||
|
Usage: "options for task templates",
|
||||||
|
Subcommands: []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "add",
|
||||||
|
Usage: "add a new template",
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
println("new task template: ", c.Args().First())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "remove",
|
||||||
|
Usage: "remove an existing template",
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
println("removed task template: ", c.Args().First())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bash Completion
|
||||||
|
|
||||||
|
You can enable completion commands by setting the `EnableBashCompletion`
|
||||||
|
flag on the `App` object. By default, this setting will only auto-complete to
|
||||||
|
show an app's subcommands, but you can write your own completion methods for
|
||||||
|
the App or its subcommands.
|
||||||
|
```go
|
||||||
|
...
|
||||||
|
var tasks = []string{"cook", "clean", "laundry", "eat", "sleep", "code"}
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.EnableBashCompletion = true
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "complete",
|
||||||
|
ShortName: "c",
|
||||||
|
Usage: "complete a task on the list",
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
println("completed task: ", c.Args().First())
|
||||||
|
},
|
||||||
|
BashComplete: func(c *cli.Context) {
|
||||||
|
// This will complete if no args are passed
|
||||||
|
if len(c.Args()) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, t := range tasks {
|
||||||
|
fmt.Println(t)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### To Enable
|
||||||
|
|
||||||
|
Source the `autocomplete/bash_autocomplete` file in your `.bashrc` file while
|
||||||
|
setting the `PROG` variable to the name of your program:
|
||||||
|
|
||||||
|
`PROG=myprogram source /.../cli/autocomplete/bash_autocomplete`
|
||||||
|
|
||||||
|
|
||||||
|
## Contribution Guidelines
|
||||||
|
Feel free to put up a pull request to fix a bug or maybe add a feature. I will give it a code review and make sure that it does not break backwards compatibility. If I or any other collaborators agree that it is in line with the vision of the project, we will work with you to get the code into a mergeable state and merge it into the master branch.
|
||||||
|
|
||||||
|
If you are have contributed something significant to the project, I will most likely add you as a collaborator. As a collaborator you are given the ability to merge others pull requests. It is very important that new code does not break existing code, so be careful about what code you do choose to merge. If you have any questions feel free to link @codegangsta to the issue in question and we can review it together.
|
||||||
|
|
||||||
|
If you feel like you have contributed to the project but have not yet been added as a collaborator, I probably forgot to add you. Hit @codegangsta up over email and we will get it figured out.
|
||||||
|
|
||||||
|
## About
|
||||||
|
cli.go is written by none other than the [Code Gangsta](http://codegangsta.io)
|
246
Godeps/_workspace/src/github.com/codegangsta/cli/app.go
generated
vendored
Normal file
246
Godeps/_workspace/src/github.com/codegangsta/cli/app.go
generated
vendored
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App is the main structure of a cli application. It is recomended that
|
||||||
|
// and app be created with the cli.NewApp() function
|
||||||
|
type App struct {
|
||||||
|
// The name of the program. Defaults to os.Args[0]
|
||||||
|
Name string
|
||||||
|
// Description of the program.
|
||||||
|
Usage string
|
||||||
|
// Version of the program
|
||||||
|
Version string
|
||||||
|
// List of commands to execute
|
||||||
|
Commands []Command
|
||||||
|
// List of flags to parse
|
||||||
|
Flags []Flag
|
||||||
|
// Boolean to enable bash completion commands
|
||||||
|
EnableBashCompletion bool
|
||||||
|
// Boolean to hide built-in help command
|
||||||
|
HideHelp bool
|
||||||
|
// An action to execute when the bash-completion flag is set
|
||||||
|
BashComplete func(context *Context)
|
||||||
|
// An action to execute before any subcommands are run, but after the context is ready
|
||||||
|
// If a non-nil error is returned, no subcommands are run
|
||||||
|
Before func(context *Context) error
|
||||||
|
// The action to execute when no subcommands are specified
|
||||||
|
Action func(context *Context)
|
||||||
|
// Execute this function if the proper command cannot be found
|
||||||
|
CommandNotFound func(context *Context, command string)
|
||||||
|
// Compilation date
|
||||||
|
Compiled time.Time
|
||||||
|
// Author
|
||||||
|
Author string
|
||||||
|
// Author e-mail
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tries to find out when this binary was compiled.
|
||||||
|
// Returns the current time if it fails to find it.
|
||||||
|
func compileTime() time.Time {
|
||||||
|
info, err := os.Stat(os.Args[0])
|
||||||
|
if err != nil {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
return info.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new cli Application with some reasonable defaults for Name, Usage, Version and Action.
|
||||||
|
func NewApp() *App {
|
||||||
|
return &App{
|
||||||
|
Name: os.Args[0],
|
||||||
|
Usage: "A new cli application",
|
||||||
|
Version: "0.0.0",
|
||||||
|
BashComplete: DefaultAppComplete,
|
||||||
|
Action: helpCommand.Action,
|
||||||
|
Compiled: compileTime(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination
|
||||||
|
func (a *App) Run(arguments []string) error {
|
||||||
|
// append help to commands
|
||||||
|
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
|
||||||
|
a.Commands = append(a.Commands, helpCommand)
|
||||||
|
a.appendFlag(HelpFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
//append version/help flags
|
||||||
|
if a.EnableBashCompletion {
|
||||||
|
a.appendFlag(BashCompletionFlag)
|
||||||
|
}
|
||||||
|
a.appendFlag(VersionFlag)
|
||||||
|
|
||||||
|
// parse flags
|
||||||
|
set := flagSet(a.Name, a.Flags)
|
||||||
|
set.SetOutput(ioutil.Discard)
|
||||||
|
err := set.Parse(arguments[1:])
|
||||||
|
nerr := normalizeFlags(a.Flags, set)
|
||||||
|
if nerr != nil {
|
||||||
|
fmt.Println(nerr)
|
||||||
|
context := NewContext(a, set, set)
|
||||||
|
ShowAppHelp(context)
|
||||||
|
fmt.Println("")
|
||||||
|
return nerr
|
||||||
|
}
|
||||||
|
context := NewContext(a, set, set)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Incorrect Usage.\n\n")
|
||||||
|
ShowAppHelp(context)
|
||||||
|
fmt.Println("")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkCompletions(context) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkHelp(context) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkVersion(context) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Before != nil {
|
||||||
|
err := a.Before(context)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := context.Args()
|
||||||
|
if args.Present() {
|
||||||
|
name := args.First()
|
||||||
|
c := a.Command(name)
|
||||||
|
if c != nil {
|
||||||
|
return c.Run(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run default Action
|
||||||
|
a.Action(context)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Another entry point to the cli app, takes care of passing arguments and error handling
|
||||||
|
func (a *App) RunAndExitOnError() {
|
||||||
|
if err := a.Run(os.Args); err != nil {
|
||||||
|
os.Stderr.WriteString(fmt.Sprintln(err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags
|
||||||
|
func (a *App) RunAsSubcommand(ctx *Context) error {
|
||||||
|
// append help to commands
|
||||||
|
if len(a.Commands) > 0 {
|
||||||
|
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
|
||||||
|
a.Commands = append(a.Commands, helpCommand)
|
||||||
|
a.appendFlag(HelpFlag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// append flags
|
||||||
|
if a.EnableBashCompletion {
|
||||||
|
a.appendFlag(BashCompletionFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse flags
|
||||||
|
set := flagSet(a.Name, a.Flags)
|
||||||
|
set.SetOutput(ioutil.Discard)
|
||||||
|
err := set.Parse(ctx.Args().Tail())
|
||||||
|
nerr := normalizeFlags(a.Flags, set)
|
||||||
|
context := NewContext(a, set, ctx.globalSet)
|
||||||
|
|
||||||
|
if nerr != nil {
|
||||||
|
fmt.Println(nerr)
|
||||||
|
if len(a.Commands) > 0 {
|
||||||
|
ShowSubcommandHelp(context)
|
||||||
|
} else {
|
||||||
|
ShowCommandHelp(ctx, context.Args().First())
|
||||||
|
}
|
||||||
|
fmt.Println("")
|
||||||
|
return nerr
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Incorrect Usage.\n\n")
|
||||||
|
ShowSubcommandHelp(context)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkCompletions(context) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(a.Commands) > 0 {
|
||||||
|
if checkSubcommandHelp(context) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if checkCommandHelp(ctx, context.Args().First()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Before != nil {
|
||||||
|
err := a.Before(context)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := context.Args()
|
||||||
|
if args.Present() {
|
||||||
|
name := args.First()
|
||||||
|
c := a.Command(name)
|
||||||
|
if c != nil {
|
||||||
|
return c.Run(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run default Action
|
||||||
|
if len(a.Commands) > 0 {
|
||||||
|
a.Action(context)
|
||||||
|
} else {
|
||||||
|
a.Action(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the named command on App. Returns nil if the command does not exist
|
||||||
|
func (a *App) Command(name string) *Command {
|
||||||
|
for _, c := range a.Commands {
|
||||||
|
if c.HasName(name) {
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) hasFlag(flag Flag) bool {
|
||||||
|
for _, f := range a.Flags {
|
||||||
|
if flag == f {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) appendFlag(flag Flag) {
|
||||||
|
if !a.hasFlag(flag) {
|
||||||
|
a.Flags = append(a.Flags, flag)
|
||||||
|
}
|
||||||
|
}
|
423
Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go
generated
vendored
Normal file
423
Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go
generated
vendored
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
package cli_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/codegangsta/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleApp() {
|
||||||
|
// set args for examples sake
|
||||||
|
os.Args = []string{"greet", "--name", "Jeremy"}
|
||||||
|
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "greet"
|
||||||
|
app.Flags = []cli.Flag{
|
||||||
|
cli.StringFlag{Name: "name", Value: "bob", Usage: "a name to say"},
|
||||||
|
}
|
||||||
|
app.Action = func(c *cli.Context) {
|
||||||
|
fmt.Printf("Hello %v\n", c.String("name"))
|
||||||
|
}
|
||||||
|
app.Run(os.Args)
|
||||||
|
// Output:
|
||||||
|
// Hello Jeremy
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleAppSubcommand() {
|
||||||
|
// set args for examples sake
|
||||||
|
os.Args = []string{"say", "hi", "english", "--name", "Jeremy"}
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "say"
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "hello",
|
||||||
|
ShortName: "hi",
|
||||||
|
Usage: "use it to see a description",
|
||||||
|
Description: "This is how we describe hello the function",
|
||||||
|
Subcommands: []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "english",
|
||||||
|
ShortName: "en",
|
||||||
|
Usage: "sends a greeting in english",
|
||||||
|
Description: "greets someone in english",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "name",
|
||||||
|
Value: "Bob",
|
||||||
|
Usage: "Name of the person to greet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
fmt.Println("Hello,", c.String("name"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run(os.Args)
|
||||||
|
// Output:
|
||||||
|
// Hello, Jeremy
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleAppHelp() {
|
||||||
|
// set args for examples sake
|
||||||
|
os.Args = []string{"greet", "h", "describeit"}
|
||||||
|
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "greet"
|
||||||
|
app.Flags = []cli.Flag{
|
||||||
|
cli.StringFlag{Name: "name", Value: "bob", Usage: "a name to say"},
|
||||||
|
}
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "describeit",
|
||||||
|
ShortName: "d",
|
||||||
|
Usage: "use it to see a description",
|
||||||
|
Description: "This is how we describe describeit the function",
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
fmt.Printf("i like to describe things")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app.Run(os.Args)
|
||||||
|
// Output:
|
||||||
|
// NAME:
|
||||||
|
// describeit - use it to see a description
|
||||||
|
//
|
||||||
|
// USAGE:
|
||||||
|
// command describeit [arguments...]
|
||||||
|
//
|
||||||
|
// DESCRIPTION:
|
||||||
|
// This is how we describe describeit the function
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleAppBashComplete() {
|
||||||
|
// set args for examples sake
|
||||||
|
os.Args = []string{"greet", "--generate-bash-completion"}
|
||||||
|
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "greet"
|
||||||
|
app.EnableBashCompletion = true
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "describeit",
|
||||||
|
ShortName: "d",
|
||||||
|
Usage: "use it to see a description",
|
||||||
|
Description: "This is how we describe describeit the function",
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
fmt.Printf("i like to describe things")
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Name: "next",
|
||||||
|
Usage: "next example",
|
||||||
|
Description: "more stuff to see when generating bash completion",
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
fmt.Printf("the next example")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run(os.Args)
|
||||||
|
// Output:
|
||||||
|
// describeit
|
||||||
|
// d
|
||||||
|
// next
|
||||||
|
// help
|
||||||
|
// h
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApp_Run(t *testing.T) {
|
||||||
|
s := ""
|
||||||
|
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Action = func(c *cli.Context) {
|
||||||
|
s = s + c.Args().First()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := app.Run([]string{"command", "foo"})
|
||||||
|
expect(t, err, nil)
|
||||||
|
err = app.Run([]string{"command", "bar"})
|
||||||
|
expect(t, err, nil)
|
||||||
|
expect(t, s, "foobar")
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandAppTests = []struct {
|
||||||
|
name string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"foobar", true},
|
||||||
|
{"batbaz", true},
|
||||||
|
{"b", true},
|
||||||
|
{"f", true},
|
||||||
|
{"bat", false},
|
||||||
|
{"nothing", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApp_Command(t *testing.T) {
|
||||||
|
app := cli.NewApp()
|
||||||
|
fooCommand := cli.Command{Name: "foobar", ShortName: "f"}
|
||||||
|
batCommand := cli.Command{Name: "batbaz", ShortName: "b"}
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
fooCommand,
|
||||||
|
batCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range commandAppTests {
|
||||||
|
expect(t, app.Command(test.name) != nil, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApp_CommandWithArgBeforeFlags(t *testing.T) {
|
||||||
|
var parsedOption, firstArg string
|
||||||
|
|
||||||
|
app := cli.NewApp()
|
||||||
|
command := cli.Command{
|
||||||
|
Name: "cmd",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringFlag{Name: "option", Value: "", Usage: "some option"},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
parsedOption = c.String("option")
|
||||||
|
firstArg = c.Args().First()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app.Commands = []cli.Command{command}
|
||||||
|
|
||||||
|
app.Run([]string{"", "cmd", "my-arg", "--option", "my-option"})
|
||||||
|
|
||||||
|
expect(t, parsedOption, "my-option")
|
||||||
|
expect(t, firstArg, "my-arg")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApp_Float64Flag(t *testing.T) {
|
||||||
|
var meters float64
|
||||||
|
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Flags = []cli.Flag{
|
||||||
|
cli.Float64Flag{Name: "height", Value: 1.5, Usage: "Set the height, in meters"},
|
||||||
|
}
|
||||||
|
app.Action = func(c *cli.Context) {
|
||||||
|
meters = c.Float64("height")
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run([]string{"", "--height", "1.93"})
|
||||||
|
expect(t, meters, 1.93)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApp_ParseSliceFlags(t *testing.T) {
|
||||||
|
var parsedOption, firstArg string
|
||||||
|
var parsedIntSlice []int
|
||||||
|
var parsedStringSlice []string
|
||||||
|
|
||||||
|
app := cli.NewApp()
|
||||||
|
command := cli.Command{
|
||||||
|
Name: "cmd",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.IntSliceFlag{Name: "p", Value: &cli.IntSlice{}, Usage: "set one or more ip addr"},
|
||||||
|
cli.StringSliceFlag{Name: "ip", Value: &cli.StringSlice{}, Usage: "set one or more ports to open"},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
parsedIntSlice = c.IntSlice("p")
|
||||||
|
parsedStringSlice = c.StringSlice("ip")
|
||||||
|
parsedOption = c.String("option")
|
||||||
|
firstArg = c.Args().First()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app.Commands = []cli.Command{command}
|
||||||
|
|
||||||
|
app.Run([]string{"", "cmd", "my-arg", "-p", "22", "-p", "80", "-ip", "8.8.8.8", "-ip", "8.8.4.4"})
|
||||||
|
|
||||||
|
IntsEquals := func(a, b []int) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, v := range a {
|
||||||
|
if v != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
StrsEquals := func(a, b []string) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, v := range a {
|
||||||
|
if v != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var expectedIntSlice = []int{22, 80}
|
||||||
|
var expectedStringSlice = []string{"8.8.8.8", "8.8.4.4"}
|
||||||
|
|
||||||
|
if !IntsEquals(parsedIntSlice, expectedIntSlice) {
|
||||||
|
t.Errorf("%v does not match %v", parsedIntSlice, expectedIntSlice)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !StrsEquals(parsedStringSlice, expectedStringSlice) {
|
||||||
|
t.Errorf("%v does not match %v", parsedStringSlice, expectedStringSlice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApp_BeforeFunc(t *testing.T) {
|
||||||
|
beforeRun, subcommandRun := false, false
|
||||||
|
beforeError := fmt.Errorf("fail")
|
||||||
|
var err error
|
||||||
|
|
||||||
|
app := cli.NewApp()
|
||||||
|
|
||||||
|
app.Before = func(c *cli.Context) error {
|
||||||
|
beforeRun = true
|
||||||
|
s := c.String("opt")
|
||||||
|
if s == "fail" {
|
||||||
|
return beforeError
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
cli.Command{
|
||||||
|
Name: "sub",
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
subcommandRun = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Flags = []cli.Flag{
|
||||||
|
cli.StringFlag{Name: "opt"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// run with the Before() func succeeding
|
||||||
|
err = app.Run([]string{"command", "--opt", "succeed", "sub"})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Run error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if beforeRun == false {
|
||||||
|
t.Errorf("Before() not executed when expected")
|
||||||
|
}
|
||||||
|
|
||||||
|
if subcommandRun == false {
|
||||||
|
t.Errorf("Subcommand not executed when expected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset
|
||||||
|
beforeRun, subcommandRun = false, false
|
||||||
|
|
||||||
|
// run with the Before() func failing
|
||||||
|
err = app.Run([]string{"command", "--opt", "fail", "sub"})
|
||||||
|
|
||||||
|
// should be the same error produced by the Before func
|
||||||
|
if err != beforeError {
|
||||||
|
t.Errorf("Run error expected, but not received")
|
||||||
|
}
|
||||||
|
|
||||||
|
if beforeRun == false {
|
||||||
|
t.Errorf("Before() not executed when expected")
|
||||||
|
}
|
||||||
|
|
||||||
|
if subcommandRun == true {
|
||||||
|
t.Errorf("Subcommand executed when NOT expected")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppHelpPrinter(t *testing.T) {
|
||||||
|
oldPrinter := cli.HelpPrinter
|
||||||
|
defer func() {
|
||||||
|
cli.HelpPrinter = oldPrinter
|
||||||
|
}()
|
||||||
|
|
||||||
|
var wasCalled = false
|
||||||
|
cli.HelpPrinter = func(template string, data interface{}) {
|
||||||
|
wasCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Run([]string{"-h"})
|
||||||
|
|
||||||
|
if wasCalled == false {
|
||||||
|
t.Errorf("Help printer expected to be called, but was not")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppVersionPrinter(t *testing.T) {
|
||||||
|
oldPrinter := cli.VersionPrinter
|
||||||
|
defer func() {
|
||||||
|
cli.VersionPrinter = oldPrinter
|
||||||
|
}()
|
||||||
|
|
||||||
|
var wasCalled = false
|
||||||
|
cli.VersionPrinter = func(c *cli.Context) {
|
||||||
|
wasCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
app := cli.NewApp()
|
||||||
|
ctx := cli.NewContext(app, nil, nil)
|
||||||
|
cli.ShowVersion(ctx)
|
||||||
|
|
||||||
|
if wasCalled == false {
|
||||||
|
t.Errorf("Version printer expected to be called, but was not")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppCommandNotFound(t *testing.T) {
|
||||||
|
beforeRun, subcommandRun := false, false
|
||||||
|
app := cli.NewApp()
|
||||||
|
|
||||||
|
app.CommandNotFound = func(c *cli.Context, command string) {
|
||||||
|
beforeRun = true
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
cli.Command{
|
||||||
|
Name: "bar",
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
subcommandRun = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run([]string{"command", "foo"})
|
||||||
|
|
||||||
|
expect(t, beforeRun, true)
|
||||||
|
expect(t, subcommandRun, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobalFlagsInSubcommands(t *testing.T) {
|
||||||
|
subcommandRun := false
|
||||||
|
app := cli.NewApp()
|
||||||
|
|
||||||
|
app.Flags = []cli.Flag{
|
||||||
|
cli.BoolFlag{Name: "debug, d", Usage: "Enable debugging"},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
cli.Command{
|
||||||
|
Name: "foo",
|
||||||
|
Subcommands: []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "bar",
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
if c.GlobalBool("debug") {
|
||||||
|
subcommandRun = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run([]string{"command", "-d", "foo", "bar"})
|
||||||
|
|
||||||
|
expect(t, subcommandRun, true)
|
||||||
|
}
|
13
Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/bash_autocomplete
generated
vendored
Normal file
13
Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/bash_autocomplete
generated
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
_cli_bash_autocomplete() {
|
||||||
|
local cur prev opts base
|
||||||
|
COMPREPLY=()
|
||||||
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
|
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||||
|
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
complete -F _cli_bash_autocomplete $PROG
|
5
Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/zsh_autocomplete
generated
vendored
Normal file
5
Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/zsh_autocomplete
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
autoload -U compinit && compinit
|
||||||
|
autoload -U bashcompinit && bashcompinit
|
||||||
|
|
||||||
|
script_dir=$(dirname $0)
|
||||||
|
source ${script_dir}/bash_autocomplete
|
19
Godeps/_workspace/src/github.com/codegangsta/cli/cli.go
generated
vendored
Normal file
19
Godeps/_workspace/src/github.com/codegangsta/cli/cli.go
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// Package cli provides a minimal framework for creating and organizing command line
|
||||||
|
// Go applications. cli is designed to be easy to understand and write, the most simple
|
||||||
|
// cli application can be written as follows:
|
||||||
|
// func main() {
|
||||||
|
// cli.NewApp().Run(os.Args)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Of course this application does not do much, so let's make this an actual application:
|
||||||
|
// func main() {
|
||||||
|
// app := cli.NewApp()
|
||||||
|
// app.Name = "greet"
|
||||||
|
// app.Usage = "say a greeting"
|
||||||
|
// app.Action = func(c *cli.Context) {
|
||||||
|
// println("Greetings")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// app.Run(os.Args)
|
||||||
|
// }
|
||||||
|
package cli
|
100
Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go
generated
vendored
Normal file
100
Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go
generated
vendored
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package cli_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/codegangsta/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example() {
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "todo"
|
||||||
|
app.Usage = "task list on the command line"
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "add",
|
||||||
|
ShortName: "a",
|
||||||
|
Usage: "add a task to the list",
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
println("added task: ", c.Args().First())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "complete",
|
||||||
|
ShortName: "c",
|
||||||
|
Usage: "complete a task on the list",
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
println("completed task: ", c.Args().First())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run(os.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleSubcommand() {
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "say"
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "hello",
|
||||||
|
ShortName: "hi",
|
||||||
|
Usage: "use it to see a description",
|
||||||
|
Description: "This is how we describe hello the function",
|
||||||
|
Subcommands: []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "english",
|
||||||
|
ShortName: "en",
|
||||||
|
Usage: "sends a greeting in english",
|
||||||
|
Description: "greets someone in english",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "name",
|
||||||
|
Value: "Bob",
|
||||||
|
Usage: "Name of the person to greet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
println("Hello, ", c.String("name"))
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Name: "spanish",
|
||||||
|
ShortName: "sp",
|
||||||
|
Usage: "sends a greeting in spanish",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "surname",
|
||||||
|
Value: "Jones",
|
||||||
|
Usage: "Surname of the person to greet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
println("Hola, ", c.String("surname"))
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Name: "french",
|
||||||
|
ShortName: "fr",
|
||||||
|
Usage: "sends a greeting in french",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "nickname",
|
||||||
|
Value: "Stevie",
|
||||||
|
Usage: "Nickname of the person to greet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
println("Bonjour, ", c.String("nickname"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Name: "bye",
|
||||||
|
Usage: "says goodbye",
|
||||||
|
Action: func(c *cli.Context) {
|
||||||
|
println("bye")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run(os.Args)
|
||||||
|
}
|
144
Godeps/_workspace/src/github.com/codegangsta/cli/command.go
generated
vendored
Normal file
144
Godeps/_workspace/src/github.com/codegangsta/cli/command.go
generated
vendored
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command is a subcommand for a cli.App.
|
||||||
|
type Command struct {
|
||||||
|
// The name of the command
|
||||||
|
Name string
|
||||||
|
// short name of the command. Typically one character
|
||||||
|
ShortName string
|
||||||
|
// A short description of the usage of this command
|
||||||
|
Usage string
|
||||||
|
// A longer explanation of how the command works
|
||||||
|
Description string
|
||||||
|
// The function to call when checking for bash command completions
|
||||||
|
BashComplete func(context *Context)
|
||||||
|
// An action to execute before any sub-subcommands are run, but after the context is ready
|
||||||
|
// If a non-nil error is returned, no sub-subcommands are run
|
||||||
|
Before func(context *Context) error
|
||||||
|
// The function to call when this command is invoked
|
||||||
|
Action func(context *Context)
|
||||||
|
// List of child commands
|
||||||
|
Subcommands []Command
|
||||||
|
// List of flags to parse
|
||||||
|
Flags []Flag
|
||||||
|
// Treat all flags as normal arguments if true
|
||||||
|
SkipFlagParsing bool
|
||||||
|
// Boolean to hide built-in help command
|
||||||
|
HideHelp bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invokes the command given the context, parses ctx.Args() to generate command-specific flags
|
||||||
|
func (c Command) Run(ctx *Context) error {
|
||||||
|
|
||||||
|
if len(c.Subcommands) > 0 || c.Before != nil {
|
||||||
|
return c.startApp(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.HideHelp {
|
||||||
|
// append help to flags
|
||||||
|
c.Flags = append(
|
||||||
|
c.Flags,
|
||||||
|
HelpFlag,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.App.EnableBashCompletion {
|
||||||
|
c.Flags = append(c.Flags, BashCompletionFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
set := flagSet(c.Name, c.Flags)
|
||||||
|
set.SetOutput(ioutil.Discard)
|
||||||
|
|
||||||
|
firstFlagIndex := -1
|
||||||
|
for index, arg := range ctx.Args() {
|
||||||
|
if strings.HasPrefix(arg, "-") {
|
||||||
|
firstFlagIndex = index
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if firstFlagIndex > -1 && !c.SkipFlagParsing {
|
||||||
|
args := ctx.Args()
|
||||||
|
regularArgs := args[1:firstFlagIndex]
|
||||||
|
flagArgs := args[firstFlagIndex:]
|
||||||
|
err = set.Parse(append(flagArgs, regularArgs...))
|
||||||
|
} else {
|
||||||
|
err = set.Parse(ctx.Args().Tail())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Incorrect Usage.\n\n")
|
||||||
|
ShowCommandHelp(ctx, c.Name)
|
||||||
|
fmt.Println("")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nerr := normalizeFlags(c.Flags, set)
|
||||||
|
if nerr != nil {
|
||||||
|
fmt.Println(nerr)
|
||||||
|
fmt.Println("")
|
||||||
|
ShowCommandHelp(ctx, c.Name)
|
||||||
|
fmt.Println("")
|
||||||
|
return nerr
|
||||||
|
}
|
||||||
|
context := NewContext(ctx.App, set, ctx.globalSet)
|
||||||
|
|
||||||
|
if checkCommandCompletions(context, c.Name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkCommandHelp(context, c.Name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
context.Command = c
|
||||||
|
c.Action(context)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if Command.Name or Command.ShortName matches given name
|
||||||
|
func (c Command) HasName(name string) bool {
|
||||||
|
return c.Name == name || c.ShortName == name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Command) startApp(ctx *Context) error {
|
||||||
|
app := NewApp()
|
||||||
|
|
||||||
|
// set the name and usage
|
||||||
|
app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name)
|
||||||
|
if c.Description != "" {
|
||||||
|
app.Usage = c.Description
|
||||||
|
} else {
|
||||||
|
app.Usage = c.Usage
|
||||||
|
}
|
||||||
|
|
||||||
|
// set CommandNotFound
|
||||||
|
app.CommandNotFound = ctx.App.CommandNotFound
|
||||||
|
|
||||||
|
// set the flags and commands
|
||||||
|
app.Commands = c.Subcommands
|
||||||
|
app.Flags = c.Flags
|
||||||
|
app.HideHelp = c.HideHelp
|
||||||
|
|
||||||
|
// bash completion
|
||||||
|
app.EnableBashCompletion = ctx.App.EnableBashCompletion
|
||||||
|
if c.BashComplete != nil {
|
||||||
|
app.BashComplete = c.BashComplete
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the actions
|
||||||
|
app.Before = c.Before
|
||||||
|
if c.Action != nil {
|
||||||
|
app.Action = c.Action
|
||||||
|
} else {
|
||||||
|
app.Action = helpSubcommand.Action
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.RunAsSubcommand(ctx)
|
||||||
|
}
|
49
Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go
generated
vendored
Normal file
49
Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go
generated
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package cli_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/codegangsta/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommandDoNotIgnoreFlags(t *testing.T) {
|
||||||
|
app := cli.NewApp()
|
||||||
|
set := flag.NewFlagSet("test", 0)
|
||||||
|
test := []string{"blah", "blah", "-break"}
|
||||||
|
set.Parse(test)
|
||||||
|
|
||||||
|
c := cli.NewContext(app, set, set)
|
||||||
|
|
||||||
|
command := cli.Command{
|
||||||
|
Name: "test-cmd",
|
||||||
|
ShortName: "tc",
|
||||||
|
Usage: "this is for testing",
|
||||||
|
Description: "testing",
|
||||||
|
Action: func(_ *cli.Context) {},
|
||||||
|
}
|
||||||
|
err := command.Run(c)
|
||||||
|
|
||||||
|
expect(t, err.Error(), "flag provided but not defined: -break")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandIgnoreFlags(t *testing.T) {
|
||||||
|
app := cli.NewApp()
|
||||||
|
set := flag.NewFlagSet("test", 0)
|
||||||
|
test := []string{"blah", "blah"}
|
||||||
|
set.Parse(test)
|
||||||
|
|
||||||
|
c := cli.NewContext(app, set, set)
|
||||||
|
|
||||||
|
command := cli.Command{
|
||||||
|
Name: "test-cmd",
|
||||||
|
ShortName: "tc",
|
||||||
|
Usage: "this is for testing",
|
||||||
|
Description: "testing",
|
||||||
|
Action: func(_ *cli.Context) {},
|
||||||
|
SkipFlagParsing: true,
|
||||||
|
}
|
||||||
|
err := command.Run(c)
|
||||||
|
|
||||||
|
expect(t, err, nil)
|
||||||
|
}
|
315
Godeps/_workspace/src/github.com/codegangsta/cli/context.go
generated
vendored
Normal file
315
Godeps/_workspace/src/github.com/codegangsta/cli/context.go
generated
vendored
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context is a type that is passed through to
|
||||||
|
// each Handler action in a cli application. Context
|
||||||
|
// can be used to retrieve context-specific Args and
|
||||||
|
// parsed command-line options.
|
||||||
|
type Context struct {
|
||||||
|
App *App
|
||||||
|
Command Command
|
||||||
|
flagSet *flag.FlagSet
|
||||||
|
globalSet *flag.FlagSet
|
||||||
|
setFlags map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new context. For use in when invoking an App or Command action.
|
||||||
|
func NewContext(app *App, set *flag.FlagSet, globalSet *flag.FlagSet) *Context {
|
||||||
|
return &Context{App: app, flagSet: set, globalSet: globalSet}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a local int flag, returns 0 if no int flag exists
|
||||||
|
func (c *Context) Int(name string) int {
|
||||||
|
return lookupInt(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a local time.Duration flag, returns 0 if no time.Duration flag exists
|
||||||
|
func (c *Context) Duration(name string) time.Duration {
|
||||||
|
return lookupDuration(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a local float64 flag, returns 0 if no float64 flag exists
|
||||||
|
func (c *Context) Float64(name string) float64 {
|
||||||
|
return lookupFloat64(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a local bool flag, returns false if no bool flag exists
|
||||||
|
func (c *Context) Bool(name string) bool {
|
||||||
|
return lookupBool(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a local boolT flag, returns false if no bool flag exists
|
||||||
|
func (c *Context) BoolT(name string) bool {
|
||||||
|
return lookupBoolT(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a local string flag, returns "" if no string flag exists
|
||||||
|
func (c *Context) String(name string) string {
|
||||||
|
return lookupString(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a local string slice flag, returns nil if no string slice flag exists
|
||||||
|
func (c *Context) StringSlice(name string) []string {
|
||||||
|
return lookupStringSlice(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a local int slice flag, returns nil if no int slice flag exists
|
||||||
|
func (c *Context) IntSlice(name string) []int {
|
||||||
|
return lookupIntSlice(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a local generic flag, returns nil if no generic flag exists
|
||||||
|
func (c *Context) Generic(name string) interface{} {
|
||||||
|
return lookupGeneric(name, c.flagSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a global int flag, returns 0 if no int flag exists
|
||||||
|
func (c *Context) GlobalInt(name string) int {
|
||||||
|
return lookupInt(name, c.globalSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a global time.Duration flag, returns 0 if no time.Duration flag exists
|
||||||
|
func (c *Context) GlobalDuration(name string) time.Duration {
|
||||||
|
return lookupDuration(name, c.globalSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a global bool flag, returns false if no bool flag exists
|
||||||
|
func (c *Context) GlobalBool(name string) bool {
|
||||||
|
return lookupBool(name, c.globalSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a global string flag, returns "" if no string flag exists
|
||||||
|
func (c *Context) GlobalString(name string) string {
|
||||||
|
return lookupString(name, c.globalSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a global string slice flag, returns nil if no string slice flag exists
|
||||||
|
func (c *Context) GlobalStringSlice(name string) []string {
|
||||||
|
return lookupStringSlice(name, c.globalSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a global int slice flag, returns nil if no int slice flag exists
|
||||||
|
func (c *Context) GlobalIntSlice(name string) []int {
|
||||||
|
return lookupIntSlice(name, c.globalSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the value of a global generic flag, returns nil if no generic flag exists
|
||||||
|
func (c *Context) GlobalGeneric(name string) interface{} {
|
||||||
|
return lookupGeneric(name, c.globalSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if the flag was actually set exists
|
||||||
|
func (c *Context) IsSet(name string) bool {
|
||||||
|
if c.setFlags == nil {
|
||||||
|
c.setFlags = make(map[string]bool)
|
||||||
|
c.flagSet.Visit(func(f *flag.Flag) {
|
||||||
|
c.setFlags[f.Name] = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.setFlags[name] == true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a slice of flag names used in this context.
|
||||||
|
func (c *Context) FlagNames() (names []string) {
|
||||||
|
for _, flag := range c.Command.Flags {
|
||||||
|
name := strings.Split(flag.getName(), ",")[0]
|
||||||
|
if name == "help" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type Args []string
|
||||||
|
|
||||||
|
// Returns the command line arguments associated with the context.
|
||||||
|
func (c *Context) Args() Args {
|
||||||
|
args := Args(c.flagSet.Args())
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the nth argument, or else a blank string
|
||||||
|
func (a Args) Get(n int) string {
|
||||||
|
if len(a) > n {
|
||||||
|
return a[n]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the first argument, or else a blank string
|
||||||
|
func (a Args) First() string {
|
||||||
|
return a.Get(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the rest of the arguments (not the first one)
|
||||||
|
// or else an empty string slice
|
||||||
|
func (a Args) Tail() []string {
|
||||||
|
if len(a) >= 2 {
|
||||||
|
return []string(a)[1:]
|
||||||
|
}
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if there are any arguments present
|
||||||
|
func (a Args) Present() bool {
|
||||||
|
return len(a) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swaps arguments at the given indexes
|
||||||
|
func (a Args) Swap(from, to int) error {
|
||||||
|
if from >= len(a) || to >= len(a) {
|
||||||
|
return errors.New("index out of range")
|
||||||
|
}
|
||||||
|
a[from], a[to] = a[to], a[from]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupInt(name string, set *flag.FlagSet) int {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
val, err := strconv.Atoi(f.Value.String())
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupDuration(name string, set *flag.FlagSet) time.Duration {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
val, err := time.ParseDuration(f.Value.String())
|
||||||
|
if err == nil {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupFloat64(name string, set *flag.FlagSet) float64 {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
val, err := strconv.ParseFloat(f.Value.String(), 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupString(name string, set *flag.FlagSet) string {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
return f.Value.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupStringSlice(name string, set *flag.FlagSet) []string {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
return (f.Value.(*StringSlice)).Value()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupIntSlice(name string, set *flag.FlagSet) []int {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
return (f.Value.(*IntSlice)).Value()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupGeneric(name string, set *flag.FlagSet) interface{} {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
return f.Value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupBool(name string, set *flag.FlagSet) bool {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
val, err := strconv.ParseBool(f.Value.String())
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupBoolT(name string, set *flag.FlagSet) bool {
|
||||||
|
f := set.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
val, err := strconv.ParseBool(f.Value.String())
|
||||||
|
if err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) {
|
||||||
|
switch ff.Value.(type) {
|
||||||
|
case *StringSlice:
|
||||||
|
default:
|
||||||
|
set.Set(name, ff.Value.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeFlags(flags []Flag, set *flag.FlagSet) error {
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
set.Visit(func(f *flag.Flag) {
|
||||||
|
visited[f.Name] = true
|
||||||
|
})
|
||||||
|
for _, f := range flags {
|
||||||
|
parts := strings.Split(f.getName(), ",")
|
||||||
|
if len(parts) == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var ff *flag.Flag
|
||||||
|
for _, name := range parts {
|
||||||
|
name = strings.Trim(name, " ")
|
||||||
|
if visited[name] {
|
||||||
|
if ff != nil {
|
||||||
|
return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name)
|
||||||
|
}
|
||||||
|
ff = set.Lookup(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ff == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, name := range parts {
|
||||||
|
name = strings.Trim(name, " ")
|
||||||
|
if !visited[name] {
|
||||||
|
copyFlag(name, ff, set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
77
Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go
generated
vendored
Normal file
77
Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go
generated
vendored
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package cli_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/codegangsta/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewContext(t *testing.T) {
|
||||||
|
set := flag.NewFlagSet("test", 0)
|
||||||
|
set.Int("myflag", 12, "doc")
|
||||||
|
globalSet := flag.NewFlagSet("test", 0)
|
||||||
|
globalSet.Int("myflag", 42, "doc")
|
||||||
|
command := cli.Command{Name: "mycommand"}
|
||||||
|
c := cli.NewContext(nil, set, globalSet)
|
||||||
|
c.Command = command
|
||||||
|
expect(t, c.Int("myflag"), 12)
|
||||||
|
expect(t, c.GlobalInt("myflag"), 42)
|
||||||
|
expect(t, c.Command.Name, "mycommand")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext_Int(t *testing.T) {
|
||||||
|
set := flag.NewFlagSet("test", 0)
|
||||||
|
set.Int("myflag", 12, "doc")
|
||||||
|
c := cli.NewContext(nil, set, set)
|
||||||
|
expect(t, c.Int("myflag"), 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext_Duration(t *testing.T) {
|
||||||
|
set := flag.NewFlagSet("test", 0)
|
||||||
|
set.Duration("myflag", time.Duration(12*time.Second), "doc")
|
||||||
|
c := cli.NewContext(nil, set, set)
|
||||||
|
expect(t, c.Duration("myflag"), time.Duration(12*time.Second))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext_String(t *testing.T) {
|
||||||
|
set := flag.NewFlagSet("test", 0)
|
||||||
|
set.String("myflag", "hello world", "doc")
|
||||||
|
c := cli.NewContext(nil, set, set)
|
||||||
|
expect(t, c.String("myflag"), "hello world")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext_Bool(t *testing.T) {
|
||||||
|
set := flag.NewFlagSet("test", 0)
|
||||||
|
set.Bool("myflag", false, "doc")
|
||||||
|
c := cli.NewContext(nil, set, set)
|
||||||
|
expect(t, c.Bool("myflag"), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext_BoolT(t *testing.T) {
|
||||||
|
set := flag.NewFlagSet("test", 0)
|
||||||
|
set.Bool("myflag", true, "doc")
|
||||||
|
c := cli.NewContext(nil, set, set)
|
||||||
|
expect(t, c.BoolT("myflag"), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext_Args(t *testing.T) {
|
||||||
|
set := flag.NewFlagSet("test", 0)
|
||||||
|
set.Bool("myflag", false, "doc")
|
||||||
|
c := cli.NewContext(nil, set, set)
|
||||||
|
set.Parse([]string{"--myflag", "bat", "baz"})
|
||||||
|
expect(t, len(c.Args()), 2)
|
||||||
|
expect(t, c.Bool("myflag"), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext_IsSet(t *testing.T) {
|
||||||
|
set := flag.NewFlagSet("test", 0)
|
||||||
|
set.Bool("myflag", false, "doc")
|
||||||
|
set.String("otherflag", "hello world", "doc")
|
||||||
|
c := cli.NewContext(nil, set, set)
|
||||||
|
set.Parse([]string{"--myflag", "bat", "baz"})
|
||||||
|
expect(t, c.IsSet("myflag"), true)
|
||||||
|
expect(t, c.IsSet("otherflag"), false)
|
||||||
|
expect(t, c.IsSet("bogusflag"), false)
|
||||||
|
}
|
410
Godeps/_workspace/src/github.com/codegangsta/cli/flag.go
generated
vendored
Normal file
410
Godeps/_workspace/src/github.com/codegangsta/cli/flag.go
generated
vendored
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This flag enables bash-completion for all commands and subcommands
|
||||||
|
var BashCompletionFlag = BoolFlag{
|
||||||
|
Name: "generate-bash-completion",
|
||||||
|
}
|
||||||
|
|
||||||
|
// This flag prints the version for the application
|
||||||
|
var VersionFlag = BoolFlag{
|
||||||
|
Name: "version, v",
|
||||||
|
Usage: "print the version",
|
||||||
|
}
|
||||||
|
|
||||||
|
// This flag prints the help for all commands and subcommands
|
||||||
|
var HelpFlag = BoolFlag{
|
||||||
|
Name: "help, h",
|
||||||
|
Usage: "show help",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag is a common interface related to parsing flags in cli.
|
||||||
|
// For more advanced flag parsing techniques, it is recomended that
|
||||||
|
// this interface be implemented.
|
||||||
|
type Flag interface {
|
||||||
|
fmt.Stringer
|
||||||
|
// Apply Flag settings to the given flag set
|
||||||
|
Apply(*flag.FlagSet)
|
||||||
|
getName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func flagSet(name string, flags []Flag) *flag.FlagSet {
|
||||||
|
set := flag.NewFlagSet(name, flag.ContinueOnError)
|
||||||
|
|
||||||
|
for _, f := range flags {
|
||||||
|
f.Apply(set)
|
||||||
|
}
|
||||||
|
return set
|
||||||
|
}
|
||||||
|
|
||||||
|
func eachName(longName string, fn func(string)) {
|
||||||
|
parts := strings.Split(longName, ",")
|
||||||
|
for _, name := range parts {
|
||||||
|
name = strings.Trim(name, " ")
|
||||||
|
fn(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic is a generic parseable type identified by a specific flag
|
||||||
|
type Generic interface {
|
||||||
|
Set(value string) error
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenericFlag is the flag type for types implementing Generic
|
||||||
|
type GenericFlag struct {
|
||||||
|
Name string
|
||||||
|
Value Generic
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f GenericFlag) String() string {
|
||||||
|
return withEnvHint(f.EnvVar, fmt.Sprintf("%s%s %v\t`%v` %s", prefixFor(f.Name), f.Name, f.Value, "-"+f.Name+" option -"+f.Name+" option", f.Usage))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f GenericFlag) Apply(set *flag.FlagSet) {
|
||||||
|
val := f.Value
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
if envVal := os.Getenv(f.EnvVar); envVal != "" {
|
||||||
|
val.Set(envVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
set.Var(f.Value, name, f.Usage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f GenericFlag) getName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
type StringSlice []string
|
||||||
|
|
||||||
|
func (f *StringSlice) Set(value string) error {
|
||||||
|
*f = append(*f, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *StringSlice) String() string {
|
||||||
|
return fmt.Sprintf("%s", *f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *StringSlice) Value() []string {
|
||||||
|
return *f
|
||||||
|
}
|
||||||
|
|
||||||
|
type StringSliceFlag struct {
|
||||||
|
Name string
|
||||||
|
Value *StringSlice
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f StringSliceFlag) String() string {
|
||||||
|
firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ")
|
||||||
|
pref := prefixFor(firstName)
|
||||||
|
return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f StringSliceFlag) Apply(set *flag.FlagSet) {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
if envVal := os.Getenv(f.EnvVar); envVal != "" {
|
||||||
|
newVal := &StringSlice{}
|
||||||
|
for _, s := range strings.Split(envVal, ",") {
|
||||||
|
newVal.Set(s)
|
||||||
|
}
|
||||||
|
f.Value = newVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
set.Var(f.Value, name, f.Usage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f StringSliceFlag) getName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntSlice []int
|
||||||
|
|
||||||
|
func (f *IntSlice) Set(value string) error {
|
||||||
|
|
||||||
|
tmp, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
*f = append(*f, tmp)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *IntSlice) String() string {
|
||||||
|
return fmt.Sprintf("%d", *f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *IntSlice) Value() []int {
|
||||||
|
return *f
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntSliceFlag struct {
|
||||||
|
Name string
|
||||||
|
Value *IntSlice
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f IntSliceFlag) String() string {
|
||||||
|
firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ")
|
||||||
|
pref := prefixFor(firstName)
|
||||||
|
return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f IntSliceFlag) Apply(set *flag.FlagSet) {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
if envVal := os.Getenv(f.EnvVar); envVal != "" {
|
||||||
|
newVal := &IntSlice{}
|
||||||
|
for _, s := range strings.Split(envVal, ",") {
|
||||||
|
err := newVal.Set(s)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.Value = newVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
set.Var(f.Value, name, f.Usage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f IntSliceFlag) getName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
type BoolFlag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f BoolFlag) String() string {
|
||||||
|
return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f BoolFlag) Apply(set *flag.FlagSet) {
|
||||||
|
val := false
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
if envVal := os.Getenv(f.EnvVar); envVal != "" {
|
||||||
|
envValBool, err := strconv.ParseBool(envVal)
|
||||||
|
if err == nil {
|
||||||
|
val = envValBool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
set.Bool(name, val, f.Usage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f BoolFlag) getName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
type BoolTFlag struct {
|
||||||
|
Name string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f BoolTFlag) String() string {
|
||||||
|
return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f BoolTFlag) Apply(set *flag.FlagSet) {
|
||||||
|
val := true
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
if envVal := os.Getenv(f.EnvVar); envVal != "" {
|
||||||
|
envValBool, err := strconv.ParseBool(envVal)
|
||||||
|
if err == nil {
|
||||||
|
val = envValBool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
set.Bool(name, val, f.Usage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f BoolTFlag) getName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
type StringFlag struct {
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f StringFlag) String() string {
|
||||||
|
var fmtString string
|
||||||
|
fmtString = "%s %v\t%v"
|
||||||
|
|
||||||
|
if len(f.Value) > 0 {
|
||||||
|
fmtString = "%s '%v'\t%v"
|
||||||
|
} else {
|
||||||
|
fmtString = "%s %v\t%v"
|
||||||
|
}
|
||||||
|
|
||||||
|
return withEnvHint(f.EnvVar, fmt.Sprintf(fmtString, prefixedNames(f.Name), f.Value, f.Usage))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f StringFlag) Apply(set *flag.FlagSet) {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
if envVal := os.Getenv(f.EnvVar); envVal != "" {
|
||||||
|
f.Value = envVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
set.String(name, f.Value, f.Usage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f StringFlag) getName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntFlag struct {
|
||||||
|
Name string
|
||||||
|
Value int
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f IntFlag) String() string {
|
||||||
|
return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f IntFlag) Apply(set *flag.FlagSet) {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
if envVal := os.Getenv(f.EnvVar); envVal != "" {
|
||||||
|
envValInt, err := strconv.ParseUint(envVal, 10, 64)
|
||||||
|
if err == nil {
|
||||||
|
f.Value = int(envValInt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
set.Int(name, f.Value, f.Usage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f IntFlag) getName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
type DurationFlag struct {
|
||||||
|
Name string
|
||||||
|
Value time.Duration
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f DurationFlag) String() string {
|
||||||
|
return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f DurationFlag) Apply(set *flag.FlagSet) {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
if envVal := os.Getenv(f.EnvVar); envVal != "" {
|
||||||
|
envValDuration, err := time.ParseDuration(envVal)
|
||||||
|
if err == nil {
|
||||||
|
f.Value = envValDuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
set.Duration(name, f.Value, f.Usage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f DurationFlag) getName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
type Float64Flag struct {
|
||||||
|
Name string
|
||||||
|
Value float64
|
||||||
|
Usage string
|
||||||
|
EnvVar string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Float64Flag) String() string {
|
||||||
|
return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Float64Flag) Apply(set *flag.FlagSet) {
|
||||||
|
if f.EnvVar != "" {
|
||||||
|
if envVal := os.Getenv(f.EnvVar); envVal != "" {
|
||||||
|
envValFloat, err := strconv.ParseFloat(envVal, 10)
|
||||||
|
if err == nil {
|
||||||
|
f.Value = float64(envValFloat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eachName(f.Name, func(name string) {
|
||||||
|
set.Float64(name, f.Value, f.Usage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Float64Flag) getName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func prefixFor(name string) (prefix string) {
|
||||||
|
if len(name) == 1 {
|
||||||
|
prefix = "-"
|
||||||
|
} else {
|
||||||
|
prefix = "--"
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func prefixedNames(fullName string) (prefixed string) {
|
||||||
|
parts := strings.Split(fullName, ",")
|
||||||
|
for i, name := range parts {
|
||||||
|
name = strings.Trim(name, " ")
|
||||||
|
prefixed += prefixFor(name) + name
|
||||||
|
if i < len(parts)-1 {
|
||||||
|
prefixed += ", "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func withEnvHint(envVar, str string) string {
|
||||||
|
envText := ""
|
||||||
|
if envVar != "" {
|
||||||
|
envText = fmt.Sprintf(" [$%s]", envVar)
|
||||||
|
}
|
||||||
|
return str + envText
|
||||||
|
}
|
587
Godeps/_workspace/src/github.com/codegangsta/cli/flag_test.go
generated
vendored
Normal file
587
Godeps/_workspace/src/github.com/codegangsta/cli/flag_test.go
generated
vendored
Normal file
@ -0,0 +1,587 @@
|
|||||||
|
package cli_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/codegangsta/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var boolFlagTests = []struct {
|
||||||
|
name string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"help", "--help\t"},
|
||||||
|
{"h", "-h\t"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolFlagHelpOutput(t *testing.T) {
|
||||||
|
|
||||||
|
for _, test := range boolFlagTests {
|
||||||
|
flag := cli.BoolFlag{Name: test.name}
|
||||||
|
output := flag.String()
|
||||||
|
|
||||||
|
if output != test.expected {
|
||||||
|
t.Errorf("%s does not match %s", output, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stringFlagTests = []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"help", "", "--help \t"},
|
||||||
|
{"h", "", "-h \t"},
|
||||||
|
{"h", "", "-h \t"},
|
||||||
|
{"test", "Something", "--test 'Something'\t"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringFlagHelpOutput(t *testing.T) {
|
||||||
|
|
||||||
|
for _, test := range stringFlagTests {
|
||||||
|
flag := cli.StringFlag{Name: test.name, Value: test.value}
|
||||||
|
output := flag.String()
|
||||||
|
|
||||||
|
if output != test.expected {
|
||||||
|
t.Errorf("%s does not match %s", output, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||||
|
|
||||||
|
os.Setenv("APP_FOO", "derp")
|
||||||
|
for _, test := range stringFlagTests {
|
||||||
|
flag := cli.StringFlag{Name: test.name, Value: test.value, EnvVar: "APP_FOO"}
|
||||||
|
output := flag.String()
|
||||||
|
|
||||||
|
if !strings.HasSuffix(output, " [$APP_FOO]") {
|
||||||
|
t.Errorf("%s does not end with [$APP_FOO]", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stringSliceFlagTests = []struct {
|
||||||
|
name string
|
||||||
|
value *cli.StringSlice
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"help", func() *cli.StringSlice {
|
||||||
|
s := &cli.StringSlice{}
|
||||||
|
s.Set("")
|
||||||
|
return s
|
||||||
|
}(), "--help '--help option --help option'\t"},
|
||||||
|
{"h", func() *cli.StringSlice {
|
||||||
|
s := &cli.StringSlice{}
|
||||||
|
s.Set("")
|
||||||
|
return s
|
||||||
|
}(), "-h '-h option -h option'\t"},
|
||||||
|
{"h", func() *cli.StringSlice {
|
||||||
|
s := &cli.StringSlice{}
|
||||||
|
s.Set("")
|
||||||
|
return s
|
||||||
|
}(), "-h '-h option -h option'\t"},
|
||||||
|
{"test", func() *cli.StringSlice {
|
||||||
|
s := &cli.StringSlice{}
|
||||||
|
s.Set("Something")
|
||||||
|
return s
|
||||||
|
}(), "--test '--test option --test option'\t"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringSliceFlagHelpOutput(t *testing.T) {
|
||||||
|
|
||||||
|
for _, test := range stringSliceFlagTests {
|
||||||
|
flag := cli.StringSliceFlag{Name: test.name, Value: test.value}
|
||||||
|
output := flag.String()
|
||||||
|
|
||||||
|
if output != test.expected {
|
||||||
|
t.Errorf("%q does not match %q", output, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||||
|
|
||||||
|
os.Setenv("APP_QWWX", "11,4")
|
||||||
|
for _, test := range stringSliceFlagTests {
|
||||||
|
flag := cli.StringSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_QWWX"}
|
||||||
|
output := flag.String()
|
||||||
|
|
||||||
|
if !strings.HasSuffix(output, " [$APP_QWWX]") {
|
||||||
|
t.Errorf("%q does not end with [$APP_QWWX]", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var intFlagTests = []struct {
|
||||||
|
name string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"help", "--help '0'\t"},
|
||||||
|
{"h", "-h '0'\t"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntFlagHelpOutput(t *testing.T) {
|
||||||
|
|
||||||
|
for _, test := range intFlagTests {
|
||||||
|
flag := cli.IntFlag{Name: test.name}
|
||||||
|
output := flag.String()
|
||||||
|
|
||||||
|
if output != test.expected {
|
||||||
|
t.Errorf("%s does not match %s", output, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||||
|
|
||||||
|
os.Setenv("APP_BAR", "2")
|
||||||
|
for _, test := range intFlagTests {
|
||||||
|
flag := cli.IntFlag{Name: test.name, EnvVar: "APP_BAR"}
|
||||||
|
output := flag.String()
|
||||||
|
|
||||||
|
if !strings.HasSuffix(output, " [$APP_BAR]") {
|
||||||
|
t.Errorf("%s does not end with [$APP_BAR]", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationFlagTests = []struct {
|
||||||
|
name string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"help", "--help '0'\t"},
|
||||||
|
{"h", "-h '0'\t"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDurationFlagHelpOutput(t *testing.T) {
|
||||||
|
|
||||||
|
for _, test := range durationFlagTests {
|
||||||
|
flag := cli.DurationFlag{Name: test.name}
|
||||||
|
output := flag.String()
|
||||||
|
|
||||||
|
if output != test.expected {
|
||||||
|
t.Errorf("%s does not match %s", output, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||||
|
|
||||||
|
os.Setenv("APP_BAR", "2h3m6s")
|
||||||
|
for _, test := range durationFlagTests {
|
||||||
|
flag := cli.DurationFlag{Name: test.name, EnvVar: "APP_BAR"}
|
||||||
|
output := flag.String()
|
||||||
|
|
||||||
|
if !strings.HasSuffix(output, " [$APP_BAR]") {
|
||||||
|
t.Errorf("%s does not end with [$APP_BAR]", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var intSliceFlagTests = []struct {
|
||||||
|
name string
|
||||||
|
value *cli.IntSlice
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"help", &cli.IntSlice{}, "--help '--help option --help option'\t"},
|
||||||
|
{"h", &cli.IntSlice{}, "-h '-h option -h option'\t"},
|
||||||
|
{"h", &cli.IntSlice{}, "-h '-h option -h option'\t"},
|
||||||
|
{"test", func() *cli.IntSlice {
|
||||||
|
i := &cli.IntSlice{}
|
||||||
|
i.Set("9")
|
||||||
|
return i
|
||||||
|
}(), "--test '--test option --test option'\t"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntSliceFlagHelpOutput(t *testing.T) {
|
||||||
|
|
||||||
|
for _, test := range intSliceFlagTests {
|
||||||
|
flag := cli.IntSliceFlag{Name: test.name, Value: test.value}
|
||||||
|
output := flag.String()
|
||||||
|
|
||||||
|
if output != test.expected {
|
||||||
|
t.Errorf("%q does not match %q", output, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||||
|
|
||||||
|
os.Setenv("APP_SMURF", "42,3")
|
||||||
|
for _, test := range intSliceFlagTests {
|
||||||
|
flag := cli.IntSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_SMURF"}
|
||||||
|
output := flag.String()
|
||||||
|
|
||||||
|
if !strings.HasSuffix(output, " [$APP_SMURF]") {
|
||||||
|
t.Errorf("%q does not end with [$APP_SMURF]", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var float64FlagTests = []struct {
|
||||||
|
name string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"help", "--help '0'\t"},
|
||||||
|
{"h", "-h '0'\t"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFloat64FlagHelpOutput(t *testing.T) {
|
||||||
|
|
||||||
|
for _, test := range float64FlagTests {
|
||||||
|
flag := cli.Float64Flag{Name: test.name}
|
||||||
|
output := flag.String()
|
||||||
|
|
||||||
|
if output != test.expected {
|
||||||
|
t.Errorf("%s does not match %s", output, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) {
|
||||||
|
|
||||||
|
os.Setenv("APP_BAZ", "99.4")
|
||||||
|
for _, test := range float64FlagTests {
|
||||||
|
flag := cli.Float64Flag{Name: test.name, EnvVar: "APP_BAZ"}
|
||||||
|
output := flag.String()
|
||||||
|
|
||||||
|
if !strings.HasSuffix(output, " [$APP_BAZ]") {
|
||||||
|
t.Errorf("%s does not end with [$APP_BAZ]", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var genericFlagTests = []struct {
|
||||||
|
name string
|
||||||
|
value cli.Generic
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"help", &Parser{}, "--help <nil>\t`-help option -help option` "},
|
||||||
|
{"h", &Parser{}, "-h <nil>\t`-h option -h option` "},
|
||||||
|
{"test", &Parser{}, "--test <nil>\t`-test option -test option` "},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenericFlagHelpOutput(t *testing.T) {
|
||||||
|
|
||||||
|
for _, test := range genericFlagTests {
|
||||||
|
flag := cli.GenericFlag{Name: test.name}
|
||||||
|
output := flag.String()
|
||||||
|
|
||||||
|
if output != test.expected {
|
||||||
|
t.Errorf("%q does not match %q", output, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||||
|
|
||||||
|
os.Setenv("APP_ZAP", "3")
|
||||||
|
for _, test := range genericFlagTests {
|
||||||
|
flag := cli.GenericFlag{Name: test.name, EnvVar: "APP_ZAP"}
|
||||||
|
output := flag.String()
|
||||||
|
|
||||||
|
if !strings.HasSuffix(output, " [$APP_ZAP]") {
|
||||||
|
t.Errorf("%s does not end with [$APP_ZAP]", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMultiString(t *testing.T) {
|
||||||
|
(&cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringFlag{Name: "serve, s"},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if ctx.String("serve") != "10" {
|
||||||
|
t.Errorf("main name not set")
|
||||||
|
}
|
||||||
|
if ctx.String("s") != "10" {
|
||||||
|
t.Errorf("short name not set")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}).Run([]string{"run", "-s", "10"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMultiStringFromEnv(t *testing.T) {
|
||||||
|
os.Setenv("APP_COUNT", "20")
|
||||||
|
(&cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringFlag{Name: "count, c", EnvVar: "APP_COUNT"},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if ctx.String("count") != "20" {
|
||||||
|
t.Errorf("main name not set")
|
||||||
|
}
|
||||||
|
if ctx.String("c") != "20" {
|
||||||
|
t.Errorf("short name not set")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}).Run([]string{"run"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMultiStringSlice(t *testing.T) {
|
||||||
|
(&cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringSliceFlag{Name: "serve, s", Value: &cli.StringSlice{}},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if !reflect.DeepEqual(ctx.StringSlice("serve"), []string{"10", "20"}) {
|
||||||
|
t.Errorf("main name not set")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ctx.StringSlice("s"), []string{"10", "20"}) {
|
||||||
|
t.Errorf("short name not set")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}).Run([]string{"run", "-s", "10", "-s", "20"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMultiStringSliceFromEnv(t *testing.T) {
|
||||||
|
os.Setenv("APP_INTERVALS", "20,30,40")
|
||||||
|
|
||||||
|
(&cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringSliceFlag{Name: "intervals, i", Value: &cli.StringSlice{}, EnvVar: "APP_INTERVALS"},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if !reflect.DeepEqual(ctx.StringSlice("intervals"), []string{"20", "30", "40"}) {
|
||||||
|
t.Errorf("main name not set from env")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ctx.StringSlice("i"), []string{"20", "30", "40"}) {
|
||||||
|
t.Errorf("short name not set from env")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}).Run([]string{"run"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMultiInt(t *testing.T) {
|
||||||
|
a := cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.IntFlag{Name: "serve, s"},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if ctx.Int("serve") != 10 {
|
||||||
|
t.Errorf("main name not set")
|
||||||
|
}
|
||||||
|
if ctx.Int("s") != 10 {
|
||||||
|
t.Errorf("short name not set")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
a.Run([]string{"run", "-s", "10"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMultiIntFromEnv(t *testing.T) {
|
||||||
|
os.Setenv("APP_TIMEOUT_SECONDS", "10")
|
||||||
|
a := cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.IntFlag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if ctx.Int("timeout") != 10 {
|
||||||
|
t.Errorf("main name not set")
|
||||||
|
}
|
||||||
|
if ctx.Int("t") != 10 {
|
||||||
|
t.Errorf("short name not set")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
a.Run([]string{"run"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMultiIntSlice(t *testing.T) {
|
||||||
|
(&cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.IntSliceFlag{Name: "serve, s", Value: &cli.IntSlice{}},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if !reflect.DeepEqual(ctx.IntSlice("serve"), []int{10, 20}) {
|
||||||
|
t.Errorf("main name not set")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ctx.IntSlice("s"), []int{10, 20}) {
|
||||||
|
t.Errorf("short name not set")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}).Run([]string{"run", "-s", "10", "-s", "20"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMultiIntSliceFromEnv(t *testing.T) {
|
||||||
|
os.Setenv("APP_INTERVALS", "20,30,40")
|
||||||
|
|
||||||
|
(&cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.IntSliceFlag{Name: "intervals, i", Value: &cli.IntSlice{}, EnvVar: "APP_INTERVALS"},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if !reflect.DeepEqual(ctx.IntSlice("intervals"), []int{20, 30, 40}) {
|
||||||
|
t.Errorf("main name not set from env")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ctx.IntSlice("i"), []int{20, 30, 40}) {
|
||||||
|
t.Errorf("short name not set from env")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}).Run([]string{"run"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMultiFloat64(t *testing.T) {
|
||||||
|
a := cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.Float64Flag{Name: "serve, s"},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if ctx.Float64("serve") != 10.2 {
|
||||||
|
t.Errorf("main name not set")
|
||||||
|
}
|
||||||
|
if ctx.Float64("s") != 10.2 {
|
||||||
|
t.Errorf("short name not set")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
a.Run([]string{"run", "-s", "10.2"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMultiFloat64FromEnv(t *testing.T) {
|
||||||
|
os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
|
||||||
|
a := cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if ctx.Float64("timeout") != 15.5 {
|
||||||
|
t.Errorf("main name not set")
|
||||||
|
}
|
||||||
|
if ctx.Float64("t") != 15.5 {
|
||||||
|
t.Errorf("short name not set")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
a.Run([]string{"run"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMultiBool(t *testing.T) {
|
||||||
|
a := cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.BoolFlag{Name: "serve, s"},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if ctx.Bool("serve") != true {
|
||||||
|
t.Errorf("main name not set")
|
||||||
|
}
|
||||||
|
if ctx.Bool("s") != true {
|
||||||
|
t.Errorf("short name not set")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
a.Run([]string{"run", "--serve"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMultiBoolFromEnv(t *testing.T) {
|
||||||
|
os.Setenv("APP_DEBUG", "1")
|
||||||
|
a := cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.BoolFlag{Name: "debug, d", EnvVar: "APP_DEBUG"},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if ctx.Bool("debug") != true {
|
||||||
|
t.Errorf("main name not set from env")
|
||||||
|
}
|
||||||
|
if ctx.Bool("d") != true {
|
||||||
|
t.Errorf("short name not set from env")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
a.Run([]string{"run"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMultiBoolT(t *testing.T) {
|
||||||
|
a := cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.BoolTFlag{Name: "serve, s"},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if ctx.BoolT("serve") != true {
|
||||||
|
t.Errorf("main name not set")
|
||||||
|
}
|
||||||
|
if ctx.BoolT("s") != true {
|
||||||
|
t.Errorf("short name not set")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
a.Run([]string{"run", "--serve"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMultiBoolTFromEnv(t *testing.T) {
|
||||||
|
os.Setenv("APP_DEBUG", "0")
|
||||||
|
a := cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.BoolTFlag{Name: "debug, d", EnvVar: "APP_DEBUG"},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if ctx.BoolT("debug") != false {
|
||||||
|
t.Errorf("main name not set from env")
|
||||||
|
}
|
||||||
|
if ctx.BoolT("d") != false {
|
||||||
|
t.Errorf("short name not set from env")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
a.Run([]string{"run"})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Parser [2]string
|
||||||
|
|
||||||
|
func (p *Parser) Set(value string) error {
|
||||||
|
parts := strings.Split(value, ",")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return fmt.Errorf("invalid format")
|
||||||
|
}
|
||||||
|
|
||||||
|
(*p)[0] = parts[0]
|
||||||
|
(*p)[1] = parts[1]
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) String() string {
|
||||||
|
return fmt.Sprintf("%s,%s", p[0], p[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseGeneric(t *testing.T) {
|
||||||
|
a := cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.GenericFlag{Name: "serve, s", Value: &Parser{}},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if !reflect.DeepEqual(ctx.Generic("serve"), &Parser{"10", "20"}) {
|
||||||
|
t.Errorf("main name not set")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ctx.Generic("s"), &Parser{"10", "20"}) {
|
||||||
|
t.Errorf("short name not set")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
a.Run([]string{"run", "-s", "10,20"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseGenericFromEnv(t *testing.T) {
|
||||||
|
os.Setenv("APP_SERVE", "20,30")
|
||||||
|
a := cli.App{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.GenericFlag{Name: "serve, s", Value: &Parser{}, EnvVar: "APP_SERVE"},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
if !reflect.DeepEqual(ctx.Generic("serve"), &Parser{"20", "30"}) {
|
||||||
|
t.Errorf("main name not set from env")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ctx.Generic("s"), &Parser{"20", "30"}) {
|
||||||
|
t.Errorf("short name not set from env")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
a.Run([]string{"run"})
|
||||||
|
}
|
224
Godeps/_workspace/src/github.com/codegangsta/cli/help.go
generated
vendored
Normal file
224
Godeps/_workspace/src/github.com/codegangsta/cli/help.go
generated
vendored
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/tabwriter"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The text template for the Default help topic.
|
||||||
|
// cli.go uses text/template to render templates. You can
|
||||||
|
// render custom help text by setting this variable.
|
||||||
|
var AppHelpTemplate = `NAME:
|
||||||
|
{{.Name}} - {{.Usage}}
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
{{.Name}} {{if .Flags}}[global options] {{end}}command{{if .Flags}} [command options]{{end}} [arguments...]
|
||||||
|
|
||||||
|
VERSION:
|
||||||
|
{{.Version}}{{if or .Author .Email}}
|
||||||
|
|
||||||
|
AUTHOR:{{if .Author}}
|
||||||
|
{{.Author}}{{if .Email}} - <{{.Email}}>{{end}}{{else}}
|
||||||
|
{{.Email}}{{end}}{{end}}
|
||||||
|
|
||||||
|
COMMANDS:
|
||||||
|
{{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}
|
||||||
|
{{end}}{{if .Flags}}
|
||||||
|
GLOBAL OPTIONS:
|
||||||
|
{{range .Flags}}{{.}}
|
||||||
|
{{end}}{{end}}
|
||||||
|
`
|
||||||
|
|
||||||
|
// The text template for the command help topic.
|
||||||
|
// cli.go uses text/template to render templates. You can
|
||||||
|
// render custom help text by setting this variable.
|
||||||
|
var CommandHelpTemplate = `NAME:
|
||||||
|
{{.Name}} - {{.Usage}}
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
command {{.Name}}{{if .Flags}} [command options]{{end}} [arguments...]{{if .Description}}
|
||||||
|
|
||||||
|
DESCRIPTION:
|
||||||
|
{{.Description}}{{end}}{{if .Flags}}
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
{{range .Flags}}{{.}}
|
||||||
|
{{end}}{{ end }}
|
||||||
|
`
|
||||||
|
|
||||||
|
// The text template for the subcommand help topic.
|
||||||
|
// cli.go uses text/template to render templates. You can
|
||||||
|
// render custom help text by setting this variable.
|
||||||
|
var SubcommandHelpTemplate = `NAME:
|
||||||
|
{{.Name}} - {{.Usage}}
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
{{.Name}} command{{if .Flags}} [command options]{{end}} [arguments...]
|
||||||
|
|
||||||
|
COMMANDS:
|
||||||
|
{{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}
|
||||||
|
{{end}}{{if .Flags}}
|
||||||
|
OPTIONS:
|
||||||
|
{{range .Flags}}{{.}}
|
||||||
|
{{end}}{{end}}
|
||||||
|
`
|
||||||
|
|
||||||
|
var helpCommand = Command{
|
||||||
|
Name: "help",
|
||||||
|
ShortName: "h",
|
||||||
|
Usage: "Shows a list of commands or help for one command",
|
||||||
|
Action: func(c *Context) {
|
||||||
|
args := c.Args()
|
||||||
|
if args.Present() {
|
||||||
|
ShowCommandHelp(c, args.First())
|
||||||
|
} else {
|
||||||
|
ShowAppHelp(c)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var helpSubcommand = Command{
|
||||||
|
Name: "help",
|
||||||
|
ShortName: "h",
|
||||||
|
Usage: "Shows a list of commands or help for one command",
|
||||||
|
Action: func(c *Context) {
|
||||||
|
args := c.Args()
|
||||||
|
if args.Present() {
|
||||||
|
ShowCommandHelp(c, args.First())
|
||||||
|
} else {
|
||||||
|
ShowSubcommandHelp(c)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prints help for the App
|
||||||
|
var HelpPrinter = printHelp
|
||||||
|
|
||||||
|
// Prints version for the App
|
||||||
|
var VersionPrinter = printVersion
|
||||||
|
|
||||||
|
func ShowAppHelp(c *Context) {
|
||||||
|
HelpPrinter(AppHelpTemplate, c.App)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prints the list of subcommands as the default app completion method
|
||||||
|
func DefaultAppComplete(c *Context) {
|
||||||
|
for _, command := range c.App.Commands {
|
||||||
|
fmt.Println(command.Name)
|
||||||
|
if command.ShortName != "" {
|
||||||
|
fmt.Println(command.ShortName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prints help for the given command
|
||||||
|
func ShowCommandHelp(c *Context, command string) {
|
||||||
|
for _, c := range c.App.Commands {
|
||||||
|
if c.HasName(command) {
|
||||||
|
HelpPrinter(CommandHelpTemplate, c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.App.CommandNotFound != nil {
|
||||||
|
c.App.CommandNotFound(c, command)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("No help topic for '%v'\n", command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prints help for the given subcommand
|
||||||
|
func ShowSubcommandHelp(c *Context) {
|
||||||
|
HelpPrinter(SubcommandHelpTemplate, c.App)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prints the version number of the App
|
||||||
|
func ShowVersion(c *Context) {
|
||||||
|
VersionPrinter(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printVersion(c *Context) {
|
||||||
|
fmt.Printf("%v version %v\n", c.App.Name, c.App.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prints the lists of commands within a given context
|
||||||
|
func ShowCompletions(c *Context) {
|
||||||
|
a := c.App
|
||||||
|
if a != nil && a.BashComplete != nil {
|
||||||
|
a.BashComplete(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prints the custom completions for a given command
|
||||||
|
func ShowCommandCompletions(ctx *Context, command string) {
|
||||||
|
c := ctx.App.Command(command)
|
||||||
|
if c != nil && c.BashComplete != nil {
|
||||||
|
c.BashComplete(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHelp(templ string, data interface{}) {
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
|
||||||
|
t := template.Must(template.New("help").Parse(templ))
|
||||||
|
err := t.Execute(w, data)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkVersion(c *Context) bool {
|
||||||
|
if c.GlobalBool("version") {
|
||||||
|
ShowVersion(c)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkHelp(c *Context) bool {
|
||||||
|
if c.GlobalBool("h") || c.GlobalBool("help") {
|
||||||
|
ShowAppHelp(c)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCommandHelp(c *Context, name string) bool {
|
||||||
|
if c.Bool("h") || c.Bool("help") {
|
||||||
|
ShowCommandHelp(c, name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkSubcommandHelp(c *Context) bool {
|
||||||
|
if c.GlobalBool("h") || c.GlobalBool("help") {
|
||||||
|
ShowSubcommandHelp(c)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCompletions(c *Context) bool {
|
||||||
|
if c.GlobalBool(BashCompletionFlag.Name) && c.App.EnableBashCompletion {
|
||||||
|
ShowCompletions(c)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCommandCompletions(c *Context, name string) bool {
|
||||||
|
if c.Bool(BashCompletionFlag.Name) && c.App.EnableBashCompletion {
|
||||||
|
ShowCommandCompletions(c, name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
19
Godeps/_workspace/src/github.com/codegangsta/cli/helpers_test.go
generated
vendored
Normal file
19
Godeps/_workspace/src/github.com/codegangsta/cli/helpers_test.go
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package cli_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Test Helpers */
|
||||||
|
func expect(t *testing.T, a interface{}, b interface{}) {
|
||||||
|
if a != b {
|
||||||
|
t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func refute(t *testing.T, a interface{}, b interface{}) {
|
||||||
|
if a == b {
|
||||||
|
t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@ func (c *Client) AddChildDir(key string, ttl uint64) (*Response, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.toResponse()
|
return raw.Unmarshal()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a new file with a random etcd-generated key under the given path.
|
// Add a new file with a random etcd-generated key under the given path.
|
||||||
@ -19,5 +19,5 @@ func (c *Client) AddChild(key string, value string, ttl uint64) (*Response, erro
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.toResponse()
|
return raw.Unmarshal()
|
||||||
}
|
}
|
@ -32,8 +32,8 @@ type Config struct {
|
|||||||
CertFile string `json:"certFile"`
|
CertFile string `json:"certFile"`
|
||||||
KeyFile string `json:"keyFile"`
|
KeyFile string `json:"keyFile"`
|
||||||
CaCertFile []string `json:"caCertFiles"`
|
CaCertFile []string `json:"caCertFiles"`
|
||||||
Timeout time.Duration `json:"timeout"`
|
DialTimeout time.Duration `json:"timeout"`
|
||||||
Consistency string `json: "consistency"`
|
Consistency string `json:"consistency"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@ -42,7 +42,20 @@ type Client struct {
|
|||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
persistence io.Writer
|
persistence io.Writer
|
||||||
cURLch chan string
|
cURLch chan string
|
||||||
keyPrefix string
|
// CheckRetry can be used to control the policy for failed requests
|
||||||
|
// and modify the cluster if needed.
|
||||||
|
// The client calls it before sending requests again, and
|
||||||
|
// stops retrying if CheckRetry returns some error. The cases that
|
||||||
|
// this function needs to handle include no response and unexpected
|
||||||
|
// http status code of response.
|
||||||
|
// If CheckRetry is nil, client will call the default one
|
||||||
|
// `DefaultCheckRetry`.
|
||||||
|
// Argument cluster is the etcd.Cluster object that these requests have been made on.
|
||||||
|
// Argument numReqs is the number of http.Requests that have been made so far.
|
||||||
|
// Argument lastResp is the http.Responses from the last request.
|
||||||
|
// Argument err is the reason of the failure.
|
||||||
|
CheckRetry func(cluster *Cluster, numReqs int,
|
||||||
|
lastResp http.Response, err error) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient create a basic client that is configured to be used
|
// NewClient create a basic client that is configured to be used
|
||||||
@ -50,7 +63,7 @@ type Client struct {
|
|||||||
func NewClient(machines []string) *Client {
|
func NewClient(machines []string) *Client {
|
||||||
config := Config{
|
config := Config{
|
||||||
// default timeout is one second
|
// default timeout is one second
|
||||||
Timeout: time.Second,
|
DialTimeout: time.Second,
|
||||||
// default consistency level is STRONG
|
// default consistency level is STRONG
|
||||||
Consistency: STRONG_CONSISTENCY,
|
Consistency: STRONG_CONSISTENCY,
|
||||||
}
|
}
|
||||||
@ -58,7 +71,6 @@ func NewClient(machines []string) *Client {
|
|||||||
client := &Client{
|
client := &Client{
|
||||||
cluster: NewCluster(machines),
|
cluster: NewCluster(machines),
|
||||||
config: config,
|
config: config,
|
||||||
keyPrefix: path.Join(version, "keys"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client.initHTTPClient()
|
client.initHTTPClient()
|
||||||
@ -76,7 +88,7 @@ func NewTLSClient(machines []string, cert, key, caCert string) (*Client, error)
|
|||||||
|
|
||||||
config := Config{
|
config := Config{
|
||||||
// default timeout is one second
|
// default timeout is one second
|
||||||
Timeout: time.Second,
|
DialTimeout: time.Second,
|
||||||
// default consistency level is STRONG
|
// default consistency level is STRONG
|
||||||
Consistency: STRONG_CONSISTENCY,
|
Consistency: STRONG_CONSISTENCY,
|
||||||
CertFile: cert,
|
CertFile: cert,
|
||||||
@ -87,7 +99,6 @@ func NewTLSClient(machines []string, cert, key, caCert string) (*Client, error)
|
|||||||
client := &Client{
|
client := &Client{
|
||||||
cluster: NewCluster(machines),
|
cluster: NewCluster(machines),
|
||||||
config: config,
|
config: config,
|
||||||
keyPrefix: path.Join(version, "keys"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := client.initHTTPSClient(cert, key)
|
err := client.initHTTPSClient(cert, key)
|
||||||
@ -157,16 +168,10 @@ func (c *Client) SetTransport(tr *http.Transport) {
|
|||||||
c.httpClient.Transport = tr
|
c.httpClient.Transport = tr
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetKeyPrefix changes the key prefix from the default `/v2/keys` to whatever
|
|
||||||
// is set.
|
|
||||||
func (c *Client) SetKeyPrefix(prefix string) {
|
|
||||||
c.keyPrefix = prefix
|
|
||||||
}
|
|
||||||
|
|
||||||
// initHTTPClient initializes a HTTP client for etcd client
|
// initHTTPClient initializes a HTTP client for etcd client
|
||||||
func (c *Client) initHTTPClient() {
|
func (c *Client) initHTTPClient() {
|
||||||
tr := &http.Transport{
|
tr := &http.Transport{
|
||||||
Dial: dialTimeout,
|
Dial: c.dial,
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
},
|
},
|
||||||
@ -192,7 +197,7 @@ func (c *Client) initHTTPSClient(cert, key string) error {
|
|||||||
|
|
||||||
tr := &http.Transport{
|
tr := &http.Transport{
|
||||||
TLSClientConfig: tlsConfig,
|
TLSClientConfig: tlsConfig,
|
||||||
Dial: dialTimeout,
|
Dial: c.dial,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.httpClient = &http.Client{Transport: tr}
|
c.httpClient = &http.Client{Transport: tr}
|
||||||
@ -226,6 +231,11 @@ func (c *Client) SetConsistency(consistency string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets the DialTimeout value
|
||||||
|
func (c *Client) SetDialTimeout(d time.Duration) {
|
||||||
|
c.config.DialTimeout = d
|
||||||
|
}
|
||||||
|
|
||||||
// AddRootCA adds a root CA cert for the etcd client
|
// AddRootCA adds a root CA cert for the etcd client
|
||||||
func (c *Client) AddRootCA(caCert string) error {
|
func (c *Client) AddRootCA(caCert string) error {
|
||||||
if c.httpClient == nil {
|
if c.httpClient == nil {
|
||||||
@ -326,9 +336,29 @@ func (c *Client) createHttpPath(serverName string, _path string) string {
|
|||||||
return u.String()
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dial with timeout.
|
// dial attempts to open a TCP connection to the provided address, explicitly
|
||||||
func dialTimeout(network, addr string) (net.Conn, error) {
|
// enabling keep-alives with a one-second interval.
|
||||||
return net.DialTimeout(network, addr, time.Second)
|
func (c *Client) dial(network, addr string) (net.Conn, error) {
|
||||||
|
conn, err := net.DialTimeout(network, addr, c.config.DialTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpConn, ok := conn.(*net.TCPConn)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("Failed type-assertion of net.Conn as *net.TCPConn")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep TCP alive to check whether or not the remote machine is down
|
||||||
|
if err = tcpConn.SetKeepAlive(true); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tcpConn.SetKeepAlivePeriod(time.Second); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tcpConn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) OpenCURL() {
|
func (c *Client) OpenCURL() {
|
||||||
@ -391,8 +421,8 @@ func (c *Client) MarshalJSON() ([]byte, error) {
|
|||||||
// as defined by the standard JSON package.
|
// as defined by the standard JSON package.
|
||||||
func (c *Client) UnmarshalJSON(b []byte) error {
|
func (c *Client) UnmarshalJSON(b []byte) error {
|
||||||
temp := struct {
|
temp := struct {
|
||||||
Config Config `json: "config"`
|
Config Config `json:"config"`
|
||||||
Cluster *Cluster `json: "cluster"`
|
Cluster *Cluster `json:"cluster"`
|
||||||
}{}
|
}{}
|
||||||
err := json.Unmarshal(b, &temp)
|
err := json.Unmarshal(b, &temp)
|
||||||
if err != nil {
|
if err != nil {
|
@ -8,7 +8,7 @@ func (c *Client) CompareAndDelete(key string, prevValue string, prevIndex uint64
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.toResponse()
|
return raw.Unmarshal()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RawCompareAndDelete(key string, prevValue string, prevIndex uint64) (*RawResponse, error) {
|
func (c *Client) RawCompareAndDelete(key string, prevValue string, prevIndex uint64) (*RawResponse, error) {
|
||||||
@ -16,7 +16,7 @@ func (c *Client) RawCompareAndDelete(key string, prevValue string, prevIndex uin
|
|||||||
return nil, fmt.Errorf("You must give either prevValue or prevIndex.")
|
return nil, fmt.Errorf("You must give either prevValue or prevIndex.")
|
||||||
}
|
}
|
||||||
|
|
||||||
options := options{}
|
options := Options{}
|
||||||
if prevValue != "" {
|
if prevValue != "" {
|
||||||
options["prevValue"] = prevValue
|
options["prevValue"] = prevValue
|
||||||
}
|
}
|
@ -9,7 +9,7 @@ func (c *Client) CompareAndSwap(key string, value string, ttl uint64,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.toResponse()
|
return raw.Unmarshal()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RawCompareAndSwap(key string, value string, ttl uint64,
|
func (c *Client) RawCompareAndSwap(key string, value string, ttl uint64,
|
||||||
@ -18,7 +18,7 @@ func (c *Client) RawCompareAndSwap(key string, value string, ttl uint64,
|
|||||||
return nil, fmt.Errorf("You must give either prevValue or prevIndex.")
|
return nil, fmt.Errorf("You must give either prevValue or prevIndex.")
|
||||||
}
|
}
|
||||||
|
|
||||||
options := options{}
|
options := Options{}
|
||||||
if prevValue != "" {
|
if prevValue != "" {
|
||||||
options["prevValue"] = prevValue
|
options["prevValue"] = prevValue
|
||||||
}
|
}
|
55
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/debug.go
generated
vendored
Normal file
55
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/debug.go
generated
vendored
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package etcd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger *etcdLogger
|
||||||
|
|
||||||
|
func SetLogger(l *log.Logger) {
|
||||||
|
logger = &etcdLogger{l}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLogger() *log.Logger {
|
||||||
|
return logger.log
|
||||||
|
}
|
||||||
|
|
||||||
|
type etcdLogger struct {
|
||||||
|
log *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *etcdLogger) Debug(args ...interface{}) {
|
||||||
|
msg := "DEBUG: " + fmt.Sprint(args...)
|
||||||
|
p.log.Println(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *etcdLogger) Debugf(f string, args ...interface{}) {
|
||||||
|
msg := "DEBUG: " + fmt.Sprintf(f, args...)
|
||||||
|
// Append newline if necessary
|
||||||
|
if !strings.HasSuffix(msg, "\n") {
|
||||||
|
msg = msg + "\n"
|
||||||
|
}
|
||||||
|
p.log.Print(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *etcdLogger) Warning(args ...interface{}) {
|
||||||
|
msg := "WARNING: " + fmt.Sprint(args...)
|
||||||
|
p.log.Println(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *etcdLogger) Warningf(f string, args ...interface{}) {
|
||||||
|
msg := "WARNING: " + fmt.Sprintf(f, args...)
|
||||||
|
// Append newline if necessary
|
||||||
|
if !strings.HasSuffix(msg, "\n") {
|
||||||
|
msg = msg + "\n"
|
||||||
|
}
|
||||||
|
p.log.Print(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Default logger uses the go default log.
|
||||||
|
SetLogger(log.New(ioutil.Discard, "go-etcd", log.LstdFlags))
|
||||||
|
}
|
28
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/debug_test.go
generated
vendored
Normal file
28
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/debug_test.go
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package etcd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Foo struct{}
|
||||||
|
type Bar struct {
|
||||||
|
one string
|
||||||
|
two int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that logs don't panic with arbitrary interfaces
|
||||||
|
func TestDebug(t *testing.T) {
|
||||||
|
f := &Foo{}
|
||||||
|
b := &Bar{"asfd", 3}
|
||||||
|
for _, test := range []interface{}{
|
||||||
|
1234,
|
||||||
|
"asdf",
|
||||||
|
f,
|
||||||
|
b,
|
||||||
|
} {
|
||||||
|
logger.Debug(test)
|
||||||
|
logger.Debugf("something, %s", test)
|
||||||
|
logger.Warning(test)
|
||||||
|
logger.Warningf("something, %s", test)
|
||||||
|
}
|
||||||
|
}
|
@ -16,7 +16,7 @@ func (c *Client) Delete(key string, recursive bool) (*Response, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.toResponse()
|
return raw.Unmarshal()
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteDir deletes an empty directory or a key value pair
|
// DeleteDir deletes an empty directory or a key value pair
|
||||||
@ -27,11 +27,11 @@ func (c *Client) DeleteDir(key string) (*Response, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.toResponse()
|
return raw.Unmarshal()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RawDelete(key string, recursive bool, dir bool) (*RawResponse, error) {
|
func (c *Client) RawDelete(key string, recursive bool, dir bool) (*RawResponse, error) {
|
||||||
ops := options{
|
ops := Options{
|
||||||
"recursive": recursive,
|
"recursive": recursive,
|
||||||
"dir": dir,
|
"dir": dir,
|
||||||
}
|
}
|
@ -14,11 +14,11 @@ func (c *Client) Get(key string, sort, recursive bool) (*Response, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.toResponse()
|
return raw.Unmarshal()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RawGet(key string, sort, recursive bool) (*RawResponse, error) {
|
func (c *Client) RawGet(key string, sort, recursive bool) (*RawResponse, error) {
|
||||||
ops := options{
|
ops := Options{
|
||||||
"recursive": recursive,
|
"recursive": recursive,
|
||||||
"sorted": sort,
|
"sorted": sort,
|
||||||
}
|
}
|
@ -18,9 +18,9 @@ func cleanResult(result *Response) {
|
|||||||
// TODO(philips): make this recursive.
|
// TODO(philips): make this recursive.
|
||||||
cleanNode(result.Node)
|
cleanNode(result.Node)
|
||||||
for i, _ := range result.Node.Nodes {
|
for i, _ := range result.Node.Nodes {
|
||||||
cleanNode(&result.Node.Nodes[i])
|
cleanNode(result.Node.Nodes[i])
|
||||||
for j, _ := range result.Node.Nodes[i].Nodes {
|
for j, _ := range result.Node.Nodes[i].Nodes {
|
||||||
cleanNode(&result.Node.Nodes[i].Nodes[j])
|
cleanNode(result.Node.Nodes[i].Nodes[j])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,12 +67,12 @@ func TestGetAll(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expected := Nodes{
|
expected := Nodes{
|
||||||
Node{
|
&Node{
|
||||||
Key: "/fooDir/k0",
|
Key: "/fooDir/k0",
|
||||||
Value: "v0",
|
Value: "v0",
|
||||||
TTL: 5,
|
TTL: 5,
|
||||||
},
|
},
|
||||||
Node{
|
&Node{
|
||||||
Key: "/fooDir/k1",
|
Key: "/fooDir/k1",
|
||||||
Value: "v1",
|
Value: "v1",
|
||||||
TTL: 5,
|
TTL: 5,
|
||||||
@ -99,11 +99,11 @@ func TestGetAll(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expected = Nodes{
|
expected = Nodes{
|
||||||
Node{
|
&Node{
|
||||||
Key: "/fooDir/childDir",
|
Key: "/fooDir/childDir",
|
||||||
Dir: true,
|
Dir: true,
|
||||||
Nodes: Nodes{
|
Nodes: Nodes{
|
||||||
Node{
|
&Node{
|
||||||
Key: "/fooDir/childDir/k2",
|
Key: "/fooDir/childDir/k2",
|
||||||
Value: "v2",
|
Value: "v2",
|
||||||
TTL: 5,
|
TTL: 5,
|
||||||
@ -111,12 +111,12 @@ func TestGetAll(t *testing.T) {
|
|||||||
},
|
},
|
||||||
TTL: 5,
|
TTL: 5,
|
||||||
},
|
},
|
||||||
Node{
|
&Node{
|
||||||
Key: "/fooDir/k0",
|
Key: "/fooDir/k0",
|
||||||
Value: "v0",
|
Value: "v0",
|
||||||
TTL: 5,
|
TTL: 5,
|
||||||
},
|
},
|
||||||
Node{
|
&Node{
|
||||||
Key: "/fooDir/k1",
|
Key: "/fooDir/k1",
|
||||||
Value: "v1",
|
Value: "v1",
|
||||||
TTL: 5,
|
TTL: 5,
|
@ -6,7 +6,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
type options map[string]interface{}
|
type Options map[string]interface{}
|
||||||
|
|
||||||
// An internally-used data structure that represents a mapping
|
// An internally-used data structure that represents a mapping
|
||||||
// between valid options and their kinds
|
// between valid options and their kinds
|
||||||
@ -42,7 +42,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Convert options to a string of HTML parameters
|
// Convert options to a string of HTML parameters
|
||||||
func (ops options) toParameters(validOps validOptions) (string, error) {
|
func (ops Options) toParameters(validOps validOptions) (string, error) {
|
||||||
p := "?"
|
p := "?"
|
||||||
values := url.Values{}
|
values := url.Values{}
|
||||||
|
|
396
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/requests.go
generated
vendored
Normal file
396
Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/requests.go
generated
vendored
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
package etcd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Errors introduced by handling requests
|
||||||
|
var (
|
||||||
|
ErrRequestCancelled = errors.New("sending request is cancelled")
|
||||||
|
)
|
||||||
|
|
||||||
|
type RawRequest struct {
|
||||||
|
Method string
|
||||||
|
RelativePath string
|
||||||
|
Values url.Values
|
||||||
|
Cancel <-chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRawRequest returns a new RawRequest
|
||||||
|
func NewRawRequest(method, relativePath string, values url.Values, cancel <-chan bool) *RawRequest {
|
||||||
|
return &RawRequest{
|
||||||
|
Method: method,
|
||||||
|
RelativePath: relativePath,
|
||||||
|
Values: values,
|
||||||
|
Cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCancelable issues a cancelable GET request
|
||||||
|
func (c *Client) getCancelable(key string, options Options,
|
||||||
|
cancel <-chan bool) (*RawResponse, error) {
|
||||||
|
logger.Debugf("get %s [%s]", key, c.cluster.Leader)
|
||||||
|
p := keyToPath(key)
|
||||||
|
|
||||||
|
// If consistency level is set to STRONG, append
|
||||||
|
// the `consistent` query string.
|
||||||
|
if c.config.Consistency == STRONG_CONSISTENCY {
|
||||||
|
options["consistent"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
str, err := options.toParameters(VALID_GET_OPTIONS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p += str
|
||||||
|
|
||||||
|
req := NewRawRequest("GET", p, nil, cancel)
|
||||||
|
resp, err := c.SendRequest(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// get issues a GET request
|
||||||
|
func (c *Client) get(key string, options Options) (*RawResponse, error) {
|
||||||
|
return c.getCancelable(key, options, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// put issues a PUT request
|
||||||
|
func (c *Client) put(key string, value string, ttl uint64,
|
||||||
|
options Options) (*RawResponse, error) {
|
||||||
|
|
||||||
|
logger.Debugf("put %s, %s, ttl: %d, [%s]", key, value, ttl, c.cluster.Leader)
|
||||||
|
p := keyToPath(key)
|
||||||
|
|
||||||
|
str, err := options.toParameters(VALID_PUT_OPTIONS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p += str
|
||||||
|
|
||||||
|
req := NewRawRequest("PUT", p, buildValues(value, ttl), nil)
|
||||||
|
resp, err := c.SendRequest(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// post issues a POST request
|
||||||
|
func (c *Client) post(key string, value string, ttl uint64) (*RawResponse, error) {
|
||||||
|
logger.Debugf("post %s, %s, ttl: %d, [%s]", key, value, ttl, c.cluster.Leader)
|
||||||
|
p := keyToPath(key)
|
||||||
|
|
||||||
|
req := NewRawRequest("POST", p, buildValues(value, ttl), nil)
|
||||||
|
resp, err := c.SendRequest(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete issues a DELETE request
|
||||||
|
func (c *Client) delete(key string, options Options) (*RawResponse, error) {
|
||||||
|
logger.Debugf("delete %s [%s]", key, c.cluster.Leader)
|
||||||
|
p := keyToPath(key)
|
||||||
|
|
||||||
|
str, err := options.toParameters(VALID_DELETE_OPTIONS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p += str
|
||||||
|
|
||||||
|
req := NewRawRequest("DELETE", p, nil, nil)
|
||||||
|
resp, err := c.SendRequest(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRequest sends a HTTP request and returns a Response as defined by etcd
|
||||||
|
func (c *Client) SendRequest(rr *RawRequest) (*RawResponse, error) {
|
||||||
|
|
||||||
|
var req *http.Request
|
||||||
|
var resp *http.Response
|
||||||
|
var httpPath string
|
||||||
|
var err error
|
||||||
|
var respBody []byte
|
||||||
|
|
||||||
|
var numReqs = 1
|
||||||
|
|
||||||
|
checkRetry := c.CheckRetry
|
||||||
|
if checkRetry == nil {
|
||||||
|
checkRetry = DefaultCheckRetry
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelled := make(chan bool, 1)
|
||||||
|
reqLock := new(sync.Mutex)
|
||||||
|
|
||||||
|
if rr.Cancel != nil {
|
||||||
|
cancelRoutine := make(chan bool)
|
||||||
|
defer close(cancelRoutine)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-rr.Cancel:
|
||||||
|
cancelled <- true
|
||||||
|
logger.Debug("send.request is cancelled")
|
||||||
|
case <-cancelRoutine:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeat canceling request until this thread is stopped
|
||||||
|
// because we have no idea about whether it succeeds.
|
||||||
|
for {
|
||||||
|
reqLock.Lock()
|
||||||
|
c.httpClient.Transport.(*http.Transport).CancelRequest(req)
|
||||||
|
reqLock.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
case <-cancelRoutine:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we connect to a follower and consistency is required, retry until
|
||||||
|
// we connect to a leader
|
||||||
|
sleep := 25 * time.Millisecond
|
||||||
|
maxSleep := time.Second
|
||||||
|
|
||||||
|
for attempt := 0; ; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
select {
|
||||||
|
case <-cancelled:
|
||||||
|
return nil, ErrRequestCancelled
|
||||||
|
case <-time.After(sleep):
|
||||||
|
sleep = sleep * 2
|
||||||
|
if sleep > maxSleep {
|
||||||
|
sleep = maxSleep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("Connecting to etcd: attempt ", attempt+1, " for ", rr.RelativePath)
|
||||||
|
|
||||||
|
if rr.Method == "GET" && c.config.Consistency == WEAK_CONSISTENCY {
|
||||||
|
// If it's a GET and consistency level is set to WEAK,
|
||||||
|
// then use a random machine.
|
||||||
|
httpPath = c.getHttpPath(true, rr.RelativePath)
|
||||||
|
} else {
|
||||||
|
// Else use the leader.
|
||||||
|
httpPath = c.getHttpPath(false, rr.RelativePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a cURL command if curlChan is set
|
||||||
|
if c.cURLch != nil {
|
||||||
|
command := fmt.Sprintf("curl -X %s %s", rr.Method, httpPath)
|
||||||
|
for key, value := range rr.Values {
|
||||||
|
command += fmt.Sprintf(" -d %s=%s", key, value[0])
|
||||||
|
}
|
||||||
|
c.sendCURL(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("send.request.to ", httpPath, " | method ", rr.Method)
|
||||||
|
|
||||||
|
req, err := func() (*http.Request, error) {
|
||||||
|
reqLock.Lock()
|
||||||
|
defer reqLock.Unlock()
|
||||||
|
|
||||||
|
if rr.Values == nil {
|
||||||
|
if req, err = http.NewRequest(rr.Method, httpPath, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body := strings.NewReader(rr.Values.Encode())
|
||||||
|
if req, err = http.NewRequest(rr.Method, httpPath, body); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type",
|
||||||
|
"application/x-www-form-urlencoded; param=value")
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = c.httpClient.Do(req)
|
||||||
|
defer func() {
|
||||||
|
if resp != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// If the request was cancelled, return ErrRequestCancelled directly
|
||||||
|
select {
|
||||||
|
case <-cancelled:
|
||||||
|
return nil, ErrRequestCancelled
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
numReqs++
|
||||||
|
|
||||||
|
// network error, change a machine!
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug("network error: ", err.Error())
|
||||||
|
lastResp := http.Response{}
|
||||||
|
if checkErr := checkRetry(c.cluster, numReqs, lastResp, err); checkErr != nil {
|
||||||
|
return nil, checkErr
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cluster.switchLeader(attempt % len(c.cluster.Machines))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is no error, it should receive response
|
||||||
|
logger.Debug("recv.response.from ", httpPath)
|
||||||
|
|
||||||
|
if validHttpStatusCode[resp.StatusCode] {
|
||||||
|
// try to read byte code and break the loop
|
||||||
|
respBody, err = ioutil.ReadAll(resp.Body)
|
||||||
|
if err == nil {
|
||||||
|
logger.Debug("recv.success ", httpPath)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// ReadAll error may be caused due to cancel request
|
||||||
|
select {
|
||||||
|
case <-cancelled:
|
||||||
|
return nil, ErrRequestCancelled
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == io.ErrUnexpectedEOF {
|
||||||
|
// underlying connection was closed prematurely, probably by timeout
|
||||||
|
// TODO: empty body or unexpectedEOF can cause http.Transport to get hosed;
|
||||||
|
// this allows the client to detect that and take evasive action. Need
|
||||||
|
// to revisit once code.google.com/p/go/issues/detail?id=8648 gets fixed.
|
||||||
|
respBody = []byte{}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if resp is TemporaryRedirect, set the new leader and retry
|
||||||
|
if resp.StatusCode == http.StatusTemporaryRedirect {
|
||||||
|
u, err := resp.Location()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
} else {
|
||||||
|
// Update cluster leader based on redirect location
|
||||||
|
// because it should point to the leader address
|
||||||
|
c.cluster.updateLeaderFromURL(u)
|
||||||
|
logger.Debug("recv.response.relocate ", u.String())
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkErr := checkRetry(c.cluster, numReqs, *resp,
|
||||||
|
errors.New("Unexpected HTTP status code")); checkErr != nil {
|
||||||
|
return nil, checkErr
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &RawResponse{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Body: respBody,
|
||||||
|
Header: resp.Header,
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultCheckRetry defines the retrying behaviour for bad HTTP requests
|
||||||
|
// If we have retried 2 * machine number, stop retrying.
|
||||||
|
// If status code is InternalServerError, sleep for 200ms.
|
||||||
|
func DefaultCheckRetry(cluster *Cluster, numReqs int, lastResp http.Response,
|
||||||
|
err error) error {
|
||||||
|
|
||||||
|
if numReqs >= 2*len(cluster.Machines) {
|
||||||
|
return newError(ErrCodeEtcdNotReachable,
|
||||||
|
"Tried to connect to each peer twice and failed", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
code := lastResp.StatusCode
|
||||||
|
if code == http.StatusInternalServerError {
|
||||||
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Warning("bad response status code", code)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getHttpPath(random bool, s ...string) string {
|
||||||
|
var machine string
|
||||||
|
if random {
|
||||||
|
machine = c.cluster.Machines[rand.Intn(len(c.cluster.Machines))]
|
||||||
|
} else {
|
||||||
|
machine = c.cluster.Leader
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := machine + "/" + version
|
||||||
|
for _, seg := range s {
|
||||||
|
fullPath = fullPath + "/" + seg
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildValues builds a url.Values map according to the given value and ttl
|
||||||
|
func buildValues(value string, ttl uint64) url.Values {
|
||||||
|
v := url.Values{}
|
||||||
|
|
||||||
|
if value != "" {
|
||||||
|
v.Set("value", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ttl > 0 {
|
||||||
|
v.Set("ttl", fmt.Sprintf("%v", ttl))
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert key string to http path exclude version
|
||||||
|
// for example: key[foo] -> path[keys/foo]
|
||||||
|
// key[/] -> path[keys/]
|
||||||
|
func keyToPath(key string) string {
|
||||||
|
p := path.Join("keys", key)
|
||||||
|
|
||||||
|
// corner case: if key is "/" or "//" ect
|
||||||
|
// path join will clear the tailing "/"
|
||||||
|
// we need to add it back
|
||||||
|
if p == "keys" {
|
||||||
|
p = "keys/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
@ -31,7 +31,8 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rr *RawResponse) toResponse() (*Response, error) {
|
// Unmarshal parses RawResponse and stores the result in Response
|
||||||
|
func (rr *RawResponse) Unmarshal() (*Response, error) {
|
||||||
if rr.StatusCode != http.StatusOK && rr.StatusCode != http.StatusCreated {
|
if rr.StatusCode != http.StatusOK && rr.StatusCode != http.StatusCreated {
|
||||||
return nil, handleError(rr.Body)
|
return nil, handleError(rr.Body)
|
||||||
}
|
}
|
||||||
@ -72,7 +73,7 @@ type Node struct {
|
|||||||
CreatedIndex uint64 `json:"createdIndex,omitempty"`
|
CreatedIndex uint64 `json:"createdIndex,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Nodes []Node
|
type Nodes []*Node
|
||||||
|
|
||||||
// interfaces for sorting
|
// interfaces for sorting
|
||||||
func (ns Nodes) Len() int {
|
func (ns Nodes) Len() int {
|
@ -10,10 +10,10 @@ func (c *Client) Set(key string, value string, ttl uint64) (*Response, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.toResponse()
|
return raw.Unmarshal()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set sets the given key to a directory.
|
// SetDir sets the given key to a directory.
|
||||||
// It will create a new directory or replace the old key value pair by a directory.
|
// It will create a new directory or replace the old key value pair by a directory.
|
||||||
// It will not replace a existing directory.
|
// It will not replace a existing directory.
|
||||||
func (c *Client) SetDir(key string, ttl uint64) (*Response, error) {
|
func (c *Client) SetDir(key string, ttl uint64) (*Response, error) {
|
||||||
@ -23,7 +23,7 @@ func (c *Client) SetDir(key string, ttl uint64) (*Response, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.toResponse()
|
return raw.Unmarshal()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateDir creates a directory. It succeeds only if
|
// CreateDir creates a directory. It succeeds only if
|
||||||
@ -35,7 +35,7 @@ func (c *Client) CreateDir(key string, ttl uint64) (*Response, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.toResponse()
|
return raw.Unmarshal()
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateDir updates the given directory. It succeeds only if the
|
// UpdateDir updates the given directory. It succeeds only if the
|
||||||
@ -47,7 +47,7 @@ func (c *Client) UpdateDir(key string, ttl uint64) (*Response, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.toResponse()
|
return raw.Unmarshal()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create creates a file with the given value under the given key. It succeeds
|
// Create creates a file with the given value under the given key. It succeeds
|
||||||
@ -59,7 +59,19 @@ func (c *Client) Create(key string, value string, ttl uint64) (*Response, error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.toResponse()
|
return raw.Unmarshal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateInOrder creates a file with a key that's guaranteed to be higher than other
|
||||||
|
// keys in the given directory. It is useful for creating queues.
|
||||||
|
func (c *Client) CreateInOrder(dir string, value string, ttl uint64) (*Response, error) {
|
||||||
|
raw, err := c.RawCreateInOrder(dir, value, ttl)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw.Unmarshal()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update updates the given key to the given value. It succeeds only if the
|
// Update updates the given key to the given value. It succeeds only if the
|
||||||
@ -71,11 +83,11 @@ func (c *Client) Update(key string, value string, ttl uint64) (*Response, error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.toResponse()
|
return raw.Unmarshal()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RawUpdateDir(key string, ttl uint64) (*RawResponse, error) {
|
func (c *Client) RawUpdateDir(key string, ttl uint64) (*RawResponse, error) {
|
||||||
ops := options{
|
ops := Options{
|
||||||
"prevExist": true,
|
"prevExist": true,
|
||||||
"dir": true,
|
"dir": true,
|
||||||
}
|
}
|
||||||
@ -84,7 +96,7 @@ func (c *Client) RawUpdateDir(key string, ttl uint64) (*RawResponse, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RawCreateDir(key string, ttl uint64) (*RawResponse, error) {
|
func (c *Client) RawCreateDir(key string, ttl uint64) (*RawResponse, error) {
|
||||||
ops := options{
|
ops := Options{
|
||||||
"prevExist": false,
|
"prevExist": false,
|
||||||
"dir": true,
|
"dir": true,
|
||||||
}
|
}
|
||||||
@ -97,7 +109,7 @@ func (c *Client) RawSet(key string, value string, ttl uint64) (*RawResponse, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RawSetDir(key string, ttl uint64) (*RawResponse, error) {
|
func (c *Client) RawSetDir(key string, ttl uint64) (*RawResponse, error) {
|
||||||
ops := options{
|
ops := Options{
|
||||||
"dir": true,
|
"dir": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +117,7 @@ func (c *Client) RawSetDir(key string, ttl uint64) (*RawResponse, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RawUpdate(key string, value string, ttl uint64) (*RawResponse, error) {
|
func (c *Client) RawUpdate(key string, value string, ttl uint64) (*RawResponse, error) {
|
||||||
ops := options{
|
ops := Options{
|
||||||
"prevExist": true,
|
"prevExist": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,9 +125,13 @@ func (c *Client) RawUpdate(key string, value string, ttl uint64) (*RawResponse,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RawCreate(key string, value string, ttl uint64) (*RawResponse, error) {
|
func (c *Client) RawCreate(key string, value string, ttl uint64) (*RawResponse, error) {
|
||||||
ops := options{
|
ops := Options{
|
||||||
"prevExist": false,
|
"prevExist": false,
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.put(key, value, ttl, ops)
|
return c.put(key, value, ttl, ops)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) RawCreateInOrder(dir string, value string, ttl uint64) (*RawResponse, error) {
|
||||||
|
return c.post(dir, value, ttl)
|
||||||
|
}
|
@ -98,6 +98,43 @@ func TestCreate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateInOrder(t *testing.T) {
|
||||||
|
c := NewClient(nil)
|
||||||
|
dir := "/queue"
|
||||||
|
defer func() {
|
||||||
|
c.DeleteDir(dir)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var firstKey, secondKey string
|
||||||
|
|
||||||
|
resp, err := c.CreateInOrder(dir, "1", 5)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(resp.Action == "create" && resp.Node.Value == "1" && resp.Node.TTL == 5) {
|
||||||
|
t.Fatalf("Create 1 failed: %#v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
firstKey = resp.Node.Key
|
||||||
|
|
||||||
|
resp, err = c.CreateInOrder(dir, "2", 5)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(resp.Action == "create" && resp.Node.Value == "2" && resp.Node.TTL == 5) {
|
||||||
|
t.Fatalf("Create 2 failed: %#v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
secondKey = resp.Node.Key
|
||||||
|
|
||||||
|
if firstKey >= secondKey {
|
||||||
|
t.Fatalf("Expected first key to be greater than second key, but %s is not greater than %s",
|
||||||
|
firstKey, secondKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSetDir(t *testing.T) {
|
func TestSetDir(t *testing.T) {
|
||||||
c := NewClient(nil)
|
c := NewClient(nil)
|
||||||
defer func() {
|
defer func() {
|
@ -31,8 +31,9 @@ func (c *Client) Watch(prefix string, waitIndex uint64, recursive bool,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.toResponse()
|
return raw.Unmarshal()
|
||||||
}
|
}
|
||||||
|
defer close(receiver)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
raw, err := c.watchOnce(prefix, waitIndex, recursive, stop)
|
raw, err := c.watchOnce(prefix, waitIndex, recursive, stop)
|
||||||
@ -41,7 +42,7 @@ func (c *Client) Watch(prefix string, waitIndex uint64, recursive bool,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := raw.toResponse()
|
resp, err := raw.Unmarshal()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -50,8 +51,6 @@ func (c *Client) Watch(prefix string, waitIndex uint64, recursive bool,
|
|||||||
waitIndex = resp.Node.ModifiedIndex + 1
|
waitIndex = resp.Node.ModifiedIndex + 1
|
||||||
receiver <- resp
|
receiver <- resp
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RawWatch(prefix string, waitIndex uint64, recursive bool,
|
func (c *Client) RawWatch(prefix string, waitIndex uint64, recursive bool,
|
||||||
@ -69,7 +68,7 @@ func (c *Client) RawWatch(prefix string, waitIndex uint64, recursive bool,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := raw.toResponse()
|
resp, err := raw.Unmarshal()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -78,19 +77,13 @@ func (c *Client) RawWatch(prefix string, waitIndex uint64, recursive bool,
|
|||||||
waitIndex = resp.Node.ModifiedIndex + 1
|
waitIndex = resp.Node.ModifiedIndex + 1
|
||||||
receiver <- raw
|
receiver <- raw
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper func
|
// helper func
|
||||||
// return when there is change under the given prefix
|
// return when there is change under the given prefix
|
||||||
func (c *Client) watchOnce(key string, waitIndex uint64, recursive bool, stop chan bool) (*RawResponse, error) {
|
func (c *Client) watchOnce(key string, waitIndex uint64, recursive bool, stop chan bool) (*RawResponse, error) {
|
||||||
|
|
||||||
respChan := make(chan *RawResponse, 1)
|
options := Options{
|
||||||
errChan := make(chan error)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
options := options{
|
|
||||||
"wait": true,
|
"wait": true,
|
||||||
}
|
}
|
||||||
if waitIndex > 0 {
|
if waitIndex > 0 {
|
||||||
@ -100,22 +93,11 @@ func (c *Client) watchOnce(key string, waitIndex uint64, recursive bool, stop ch
|
|||||||
options["recursive"] = true
|
options["recursive"] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := c.get(key, options)
|
resp, err := c.getCancelable(key, options, stop)
|
||||||
|
|
||||||
if err != nil {
|
if err == ErrRequestCancelled {
|
||||||
errChan <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
respChan <- resp
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case resp := <-respChan:
|
|
||||||
return resp, nil
|
|
||||||
case err := <-errChan:
|
|
||||||
return nil, err
|
|
||||||
case <-stop:
|
|
||||||
return nil, ErrWatchStoppedByUser
|
return nil, ErrWatchStoppedByUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user