Compare commits
1783 Commits
v0.3.0
...
v0.5.0-alp
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
49e0dff2b8 | |||
686837227e | |||
f2652f005e | |||
5490eb5406 | |||
70dda950ed | |||
a884f2a18a | |||
bdeb96be0f | |||
c00594e680 | |||
919cd380ec | |||
b83aec6b87 | |||
05bfb369ef | |||
0639c4c86d | |||
877b3d51bb | |||
d9df58beb8 | |||
1cffdb3a48 | |||
0593a52107 | |||
f7854c4ab9 | |||
13b0e72304 | |||
43791a2f41 | |||
a8d966d8f3 | |||
ad7194d5d0 | |||
084dcb5596 | |||
8a0266a806 | |||
2338481bb1 | |||
ce1e19ae2f | |||
a288333e6f | |||
097aac79f5 | |||
cd820269a6 | |||
ac7e6bb002 | |||
4b45cd4110 | |||
fb426aec9e | |||
774cb03f83 | |||
4fb6087f4a | |||
5524131a9e | |||
3efb4d837b | |||
20147c5357 | |||
973bde9a07 | |||
494d2c67aa | |||
fb32a999a6 | |||
d2f5934aa1 | |||
4f3fb5a702 | |||
9f5ec7732e | |||
774cf34827 | |||
92df44276d | |||
eb00f200d3 | |||
38d16775ab | |||
690fd12b07 | |||
b31483b2be | |||
e9a21dda4b | |||
2134036942 | |||
6bd2ee4c49 | |||
fcd429467e | |||
e5e759b962 | |||
d8a08f53e3 | |||
3e95bf0fa3 | |||
0d2512cb99 | |||
a29f6fb799 | |||
f4f429d4e3 | |||
fc2afe1ed2 | |||
24a442383b | |||
f387bf8464 | |||
83b06c0715 | |||
75dc10c39d | |||
66acf8a4e9 | |||
1359d29fa4 | |||
dc1f4adcd0 | |||
9970141f76 | |||
16c2bcf951 | |||
868b7f7902 | |||
1c958f8fc3 | |||
dfeecd2537 | |||
ed58193ebe | |||
79c650d900 | |||
a451cf2333 | |||
3455431da3 | |||
9424a10f49 | |||
fbcfe8e1c4 | |||
757bb3af13 | |||
2cd367e9d9 | |||
a974bbfe4f | |||
99dcc8c322 | |||
3d2523e7e0 | |||
25e69d9659 | |||
707174b56a | |||
ce92cc3dc5 | |||
5bfbf3a48c | |||
e04a188358 | |||
a51fda3e5e | |||
ca44801650 | |||
2387ef3f21 | |||
d5bfca9465 | |||
7cb126967c | |||
444e017c05 | |||
356675b70f | |||
d7768635fd | |||
37796ed84c | |||
f007cf321d | |||
ca29691543 | |||
4bebb538eb | |||
c27db1ec5e | |||
a5fc1d214d | |||
1df0b941d7 | |||
3a71eb9d72 | |||
001cceb1cd | |||
98ff4af7f2 | |||
db4c5e0eaa | |||
b3c5ed60bd | |||
673d90728e | |||
22c944d8ef | |||
a2d16b52bb | |||
b637b3a607 | |||
0eba3c9000 | |||
c3aab42959 | |||
62560f9959 | |||
3c04f8b664 | |||
cc37c58103 | |||
07d1eb0edb | |||
9a006d673b | |||
6860bfdd28 | |||
aaedf32c04 | |||
16d89c906b | |||
516ebdb49e | |||
1e7a7b11dd | |||
4c953a7ca2 | |||
934c28d498 | |||
0c8b13c04f | |||
9e5b12f591 | |||
189fece683 | |||
ad27aa0f70 | |||
dffe460210 | |||
b4e4bf4b75 | |||
e9482167a8 | |||
cbcf55dabb | |||
1084e51320 | |||
ad9155c82a | |||
84f71b6c87 | |||
d2d68f96fc | |||
71679bcf56 | |||
a824be4c14 | |||
35cc81e22f | |||
716496ec42 | |||
e5ce4fca2e | |||
9940347eea | |||
d2a2cc13b0 | |||
b7d9fdbd39 | |||
7cf8a4a8d0 | |||
851026362a | |||
fc77b3e9e6 | |||
f6591b95c7 | |||
403f709ebd | |||
cbb706cd47 | |||
c0027bfc78 | |||
735ffb3092 | |||
6a64141962 | |||
5367c1c998 | |||
c6b1a738c3 | |||
6d4f018887 | |||
765cd5d8b3 | |||
baadf63912 | |||
5bd08a327d | |||
f1c13e2d9d | |||
6c950eaf97 | |||
5c7a963cf0 | |||
c92231c91a | |||
e960a0e03c | |||
015d228b04 | |||
e31249fa22 | |||
b3e66ee980 | |||
af33d61774 | |||
bc4a98c386 | |||
fb17ccf5ee | |||
04f09d2fd0 | |||
6450d6dfb9 | |||
0558b546ff | |||
5465201292 | |||
ae81f843f1 | |||
c9ce14c857 | |||
bed20b7837 | |||
206881bfec | |||
cf25650b3c | |||
7dce4c8fbb | |||
b0ffb4fd10 | |||
001b1fcd46 | |||
4e14604e5c | |||
b56aa62bcc | |||
ba36a16bc5 | |||
997e7d3bf4 | |||
c4cd86e094 | |||
17e299995c | |||
e11fd7cd40 | |||
d78116c35b | |||
6516cf854c | |||
e55512f60b | |||
43ad623965 | |||
2d5c7d7fb0 | |||
38b88589da | |||
c6be7887e5 | |||
da3bbc5206 | |||
8b5d9cc1fb | |||
c692a8f0a7 | |||
a563d82f95 | |||
c7116a37c0 | |||
9af9438ed0 | |||
fa54866e99 | |||
ff172ae3a5 | |||
32d23fdb11 | |||
14ae764283 | |||
b87972713e | |||
bd8d45ce28 | |||
7cebc3999a | |||
1b78cc1389 | |||
204f2d38af | |||
215ff2839d | |||
ece25833aa | |||
21693b70f7 | |||
7bf1936df3 | |||
000e3ba651 | |||
9f228550a2 | |||
a2749bad53 | |||
e0fbe27c99 | |||
5602d4a2ee | |||
b742af56ec | |||
b17703a9e4 | |||
0c95e1eabb | |||
b0ac8a4b4b | |||
3ae0a1e1a6 | |||
cc329bfa55 | |||
732fb7c160 | |||
6988676f43 | |||
273c293645 | |||
d368405d05 | |||
82dee82bfd | |||
93d1293b9d | |||
67600603c5 | |||
1eaa9c9899 | |||
a1a91ab75a | |||
9caa801469 | |||
65b872c8b5 | |||
3a5cb26973 | |||
26c77fcf9e | |||
68afedbd16 | |||
63d965133f | |||
adf4acf947 | |||
fa95ca0aa8 | |||
d88b52c5f3 | |||
d73390a674 | |||
66c552d048 | |||
f5ee9b97de | |||
8bcfb2ecaf | |||
03839ca806 | |||
de9c318436 | |||
0b790abd46 | |||
2dc182189a | |||
8d68e2f5f0 | |||
af6d5efb83 | |||
c08bcb6f5b | |||
ef99b9ac49 | |||
689de466de | |||
9e5ada880d | |||
151b9c6ed2 | |||
35ddb6627e | |||
d266f9dec7 | |||
1af51ca0c5 | |||
c8de5eee85 | |||
842dd5cab0 | |||
77270c6b00 | |||
d4b33c7e12 | |||
a05ff7f83d | |||
8071dd054c | |||
4fd9e627c0 | |||
56ef6fbcae | |||
bc70cdc242 | |||
dce461dbd7 | |||
79a89dcb82 | |||
fc84da29e8 | |||
2817baf3f8 | |||
a9dff278b5 | |||
af87fa40c2 | |||
6d08976cbe | |||
11525d357f | |||
2a8c2c6a0b | |||
bda8849c3b | |||
375aeb57af | |||
9540575690 | |||
8c0c427870 | |||
6d77e4dfd6 | |||
fe5eb31ca7 | |||
66ce075ce1 | |||
1eff547af6 | |||
d871324e93 | |||
b06d2796f8 | |||
84ad6ddd79 | |||
4ce8c3499b | |||
76ac1da7ea | |||
89d3df242c | |||
28f19dec60 | |||
3b2d039664 | |||
d66efc550d | |||
2c65e9852c | |||
ef98f6051c | |||
e650329eac | |||
d2c0a008ce | |||
1c40f327be | |||
be9ee58a52 | |||
5987917c0a | |||
12b106c0f0 | |||
dbaa2cbdee | |||
cbcd0aff9e | |||
e624d4ef33 | |||
fd9ad4fa2c | |||
0a4b6570e1 | |||
e01a1f70c3 | |||
2e4ea503b0 | |||
d8a53ca716 | |||
bec7d7f614 | |||
915cc53d83 | |||
f0dee63716 | |||
9129475a51 | |||
41ad9ebb48 | |||
3fece6a716 | |||
1f12e163de | |||
7c1b7d764a | |||
58e1e02688 | |||
d000ba3ed3 | |||
1b59eacb72 | |||
3eb74fc32c | |||
a9e77c022c | |||
dfbabc5a77 | |||
c5632b039a | |||
46623c00a8 | |||
ccb960f47f | |||
561ad16757 | |||
399f491fd7 | |||
c463293d39 | |||
094cc6347d | |||
022908f1fa | |||
2ad9ea8c57 | |||
b2f787a2fd | |||
ea701b3adb | |||
eb21381ddd | |||
534b5feca6 | |||
754159b60f | |||
fa7c77a6e0 | |||
5b4a473f14 | |||
eeca12a4c0 | |||
4e747d24dd | |||
4f1607b775 | |||
140329d927 | |||
b6053d6a86 | |||
1f10c4d352 | |||
4bce3e4810 | |||
99c8b097d1 | |||
f4aaef5955 | |||
85301dbc4e | |||
8afa93ed59 | |||
2bba9a984e | |||
303a3096d4 | |||
e86fe8e749 | |||
a3cbf02597 | |||
802aaf59db | |||
62b89a128a | |||
174b9ff343 | |||
a20ce3e7a5 | |||
aeb9e0633a | |||
38631395b6 | |||
7d4fda550d | |||
4b5765e2c7 | |||
df2b565397 | |||
1a8a6ce872 | |||
bb3b71a43f | |||
803aeb8fae | |||
15126a680f | |||
50d9e6a7fd | |||
838a945862 | |||
79e4c838f4 | |||
3ae792b159 | |||
a9d5cc5a50 | |||
b8f59340af | |||
c8e65c1315 | |||
0ea6141dc8 | |||
e9a1ac15d9 | |||
c0a59b3a27 | |||
074c78d725 | |||
312ac5d066 | |||
c91688315a | |||
653274b9f6 | |||
3fff1a8dcd | |||
12c334a141 | |||
e80a35edbe | |||
c3247755ea | |||
3cd83c61c4 | |||
2a7fdfe189 | |||
ac872b3855 | |||
b1916b30fb | |||
3a1d9e683b | |||
148ac97742 | |||
ddce52c44e | |||
fe4dee03ab | |||
c3b4afc139 | |||
063f3b574c | |||
49253d8fdf | |||
dbed0f7d6a | |||
c8d6b26dfd | |||
f3cc456385 | |||
349a802a82 | |||
69adb78433 | |||
e99bc99dcc | |||
df49265674 | |||
e5797a54a1 | |||
a60481c6b1 | |||
dde2b71850 | |||
fddbf35df2 | |||
23ffafefc0 | |||
f5698d3566 | |||
02a053a4b8 | |||
4c17dc3431 | |||
e6e6f7d8d0 | |||
1d961b8e56 | |||
f922a08a27 | |||
7289224af9 | |||
035bbc6913 | |||
ae8d416c57 | |||
46d817f91b | |||
f434177a9a | |||
8485987b74 | |||
9cfd8c5f0b | |||
04f21b5976 | |||
cf38910601 | |||
3a4df1612c | |||
bd56b15b6e | |||
a72f913a60 | |||
7599a285c3 | |||
efdc741e5c | |||
93a02b619e | |||
e69118ade7 | |||
8bed1e1f15 | |||
e73e61f238 | |||
9247486576 | |||
1cc60df5f3 | |||
b85eb2a44c | |||
5de1caf057 | |||
be069d5918 | |||
bb911e6ab8 | |||
7ad2dadabf | |||
98396e8367 | |||
fccecd4a05 | |||
db1b53fde7 | |||
c423f38700 | |||
193fe2df71 | |||
6f14467383 | |||
7015214fbb | |||
e1af3dbde6 | |||
040c1f591e | |||
f206db2cee | |||
ba8e5d7add | |||
ac5e35101c | |||
68383b1293 | |||
137f87c414 | |||
f5be7c9136 | |||
a00bf38f16 | |||
c34a2b85ed | |||
60a2709213 | |||
1fcbc83503 | |||
08818886b2 | |||
c6910a96de | |||
86546c68b4 | |||
0cc69abc70 | |||
c7aef5fdf2 | |||
c4605160c5 | |||
1273875a00 | |||
4c2942f9f9 | |||
054de85da2 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
.git
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,5 +1,11 @@
|
||||
/pkg
|
||||
/coverage
|
||||
/gopath
|
||||
/go-bindata
|
||||
/machine*
|
||||
/bin
|
||||
/src
|
||||
.vagrant
|
||||
*.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
|
43
CHANGELOG
43
CHANGELOG
@ -1,3 +1,46 @@
|
||||
v0.4.6
|
||||
* Fix long-term timer leak (#900, #875, #868, #904)
|
||||
* Fix `Running` field in standby_info file (#881)
|
||||
* Add `quorum=true` query parameter for GET requests (#866, #883)
|
||||
* Add `Access-Control-Allow-Headers` header for CORS requests (#886)
|
||||
* Various documentation improvements (#907, #882)
|
||||
|
||||
v0.4.5
|
||||
* Flush headers immediatly on `wait=true` requests (#877)
|
||||
* Add `ETCD_HTTP_READ_TIMEOUT` and `ETCD_HTTP_WRITE_TIMEOUT` (#880)
|
||||
* Add `ETCDCTL_PEERS` configuration to etcdctl (#95)
|
||||
* etcdctl takes stdin for mk (#91)
|
||||
|
||||
v0.4.4
|
||||
* Fix `--no-sync` flag in etcdctl (#83)
|
||||
* Improved logging for machine removal (#844)
|
||||
* Various documentation improvements (#858, #851, #847)
|
||||
|
||||
v0.4.3
|
||||
* Avoid panic() on truncated or unexpected log data (#834, #833)
|
||||
* Fix missing stats field (#807)
|
||||
* Lengthen default peer removal delay to 30mins (#835)
|
||||
* Reduce logging on heartbeat timeouts (#836)
|
||||
|
||||
v0.4.2
|
||||
* Improvements to the clustering documents
|
||||
* Set content-type properly on errors (#469)
|
||||
* Standbys re-join if they should be part of the cluster (#810, #815, #818)
|
||||
|
||||
v0.4.1
|
||||
* Re-introduce DELETE on the machines endpoint
|
||||
* Document the machines endpoint
|
||||
|
||||
v0.4.0
|
||||
* Introduced standby mode
|
||||
* Added HEAD requests
|
||||
* Set logs NOCOW flag when BTRFS is detected to avoid fsync overhead
|
||||
* Fix all known data races, and pass Go race detector (TODO: re-run race detector)
|
||||
* Fixed timeouts when using HTTPS
|
||||
* Improved snapshot stability
|
||||
* Migration of machine names to new IPs
|
||||
* Updated peer discovery ordering
|
||||
|
||||
v0.3.0
|
||||
* Add Compare-and-Delete support.
|
||||
* Added prevNode to response objects.
|
||||
|
@ -1,17 +1,13 @@
|
||||
# How to contribute
|
||||
|
||||
etcd is open source, Apache 2.0 licensed and accepts contributions via Github pull requests.
|
||||
This document outlines some of the conventions on commit message formatting, contact points for developers and other resources to make getting your contribution into etcd easier.
|
||||
etcd is Apache 2.0 licensed and accepts contributions via Github pull requests. This document outlines some of the conventions on commit message formatting, contact points for developers and other resources to make getting your contribution into etcd easier.
|
||||
|
||||
# Email and chat
|
||||
|
||||
For simplicity etcd discussions happen on coreos-dev and in #coreos-dev.
|
||||
As the community grows we will move to a dedicated mailing list and IRC channel.
|
||||
|
||||
- Email: [coreos-dev](https://groups.google.com/forum/#!forum/coreos-dev)
|
||||
- Email: [etcd-dev](https://groups.google.com/forum/?hl=en#!forum/etcd-dev)
|
||||
- IRC: #[coreos](irc://irc.freenode.org:6667/#coreos) IRC channel on freenode.org
|
||||
|
||||
## Getting Started
|
||||
## Getting started
|
||||
|
||||
- Fork the repository on GitHub
|
||||
- Read the README.md for build instructions
|
||||
@ -22,54 +18,45 @@ This is a rough outline of what a contributor's workflow looks like:
|
||||
|
||||
- Create a topic branch from where you want to base your work. This is usually master.
|
||||
- Make commits of logical units.
|
||||
- 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.
|
||||
- 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 you contributions!
|
||||
Thanks for your contributions!
|
||||
|
||||
### Format of the commit message
|
||||
### Code style
|
||||
|
||||
etcd follow a rough convention for commit messages borrowed from Angularjs.
|
||||
This is an example of a commit:
|
||||
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.
|
||||
|
||||
### Format of the Commit Message
|
||||
|
||||
We follow a rough convention for commit messages that is designed to answer two
|
||||
questions: what changed and why. The subject line should feature the what and
|
||||
the body of the commit should describe the why.
|
||||
|
||||
```
|
||||
feat(scripts/test-cluster): add a cluster test command
|
||||
scripts: add the test-cluster command
|
||||
|
||||
this uses tmux to setup a test cluster that you can easily kill and
|
||||
start for debugging.
|
||||
|
||||
Fixes #38
|
||||
```
|
||||
|
||||
To make it more formal it looks something like this:
|
||||
The format can be described more formally as follows:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
<subsystem>: <what changed>
|
||||
<BLANK LINE>
|
||||
<body>
|
||||
<why this change was made>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
```
|
||||
|
||||
The first line is the subject and should not be longer than 70 characters, the second line is always blank and other lines should be wrapped at 80 characters.
|
||||
This allows the message to be easier to read on github as well as in various git tools.
|
||||
|
||||
### Subject line
|
||||
|
||||
The subject line contains succinct description of the change.
|
||||
|
||||
### Allowed <type>
|
||||
- feat (feature)
|
||||
- fix (bug fix)
|
||||
- docs (documentation)
|
||||
- style (formatting, missing semi colons, …)
|
||||
- refactor
|
||||
- test (when adding missing tests)
|
||||
- chore (maintain)
|
||||
|
||||
### Allowed <scope>
|
||||
|
||||
Scopes could be anything specifying place of the commit change. For example store, api, etc.
|
||||
|
||||
### More details on commits
|
||||
|
||||
For more details see the [angularjs commit style guide](https://docs.google.com/a/coreos.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#).
|
||||
The first line is the subject and should be no longer than 70 characters, the
|
||||
second line is always blank, and other lines should be wrapped at 80 characters.
|
||||
This allows the message to be easier to read on GitHub as well as in various
|
||||
git tools.
|
||||
|
36
DCO
Normal file
36
DCO
Normal file
@ -0,0 +1,36 @@
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
12
Dockerfile
12
Dockerfile
@ -1,12 +0,0 @@
|
||||
FROM ubuntu:12.04
|
||||
# Let's install go just like Docker (from source).
|
||||
RUN apt-get update -q
|
||||
RUN apt-get install -qy build-essential curl git
|
||||
RUN curl -s https://go.googlecode.com/files/go1.2.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"]
|
||||
|
72
Documentation/0.5/admin_api.md
Normal file
72
Documentation/0.5/admin_api.md
Normal file
@ -0,0 +1,72 @@
|
||||
## Admin API
|
||||
|
||||
### GET /v2/admin/members/
|
||||
Return an HTTP 200 OK response code and a representation of all members in the etcd cluster:
|
||||
```
|
||||
Example Request: GET
|
||||
http://localhost:2379/v2/admin/members/
|
||||
Response formats: JSON
|
||||
Example Response:
|
||||
```
|
||||
```json
|
||||
{
|
||||
"members": [
|
||||
{
|
||||
"id":"272e204152",
|
||||
"name":"node1",
|
||||
"peerURLs":[
|
||||
"http://10.0.0.10:2379"
|
||||
],
|
||||
"clientURLs":[
|
||||
"http://10.0.0.10:2380"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id":"2225373f43",
|
||||
"name":"node2",
|
||||
"peerURLs":[
|
||||
"http://127.0.0.11:2379"
|
||||
],
|
||||
"clientURLs":[
|
||||
"http://127.0.0.11:2380"
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### POST /v2/admin/members/
|
||||
Add a member to the cluster.
|
||||
Returns an HTTP 201 response code and the representation of added member with a newly generated a memberID when successful. Returns a string describing the failure condition when unsuccessful.
|
||||
|
||||
If the POST body is malformed an HTTP 400 will be returned. If the member exists in the cluster or existed in the cluster at some point in the past an HTTP 500(TODO: fix this) will be returned. If the cluster fails to process the request within timeout an HTTP 500 will be returned, though the request may be processed later.
|
||||
```
|
||||
Example Request: POST
|
||||
http://localhost:2379/v2/admin/members/
|
||||
Body:
|
||||
[{"PeerURLs":["http://10.0.0.10:2379"]}]
|
||||
Respose formats: JSON
|
||||
Example Response:
|
||||
```
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id":"3777296169",
|
||||
"peerURLs":[
|
||||
"http://10.0.0.10:2379"
|
||||
],
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
### DELETE /v2/admin/members/:id
|
||||
Remove a member from the cluster.
|
||||
Returns empty when successful. Returns a string describing the failure condition when unsuccessful.
|
||||
|
||||
If the member does not exist in the cluster an HTTP 500(TODO: fix this) will be returned. If the cluster fails to process the request within timeout an HTTP 500 will be returned, though the request may be processed later.
|
||||
```
|
||||
Response formats: JSON
|
||||
Example Request: DELETE
|
||||
http://localhost:2379/v2/admin/members/272e204152
|
||||
Example Response: Empty
|
||||
```
|
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
184
Documentation/0.5/clustering.md
Normal file
184
Documentation/0.5/clustering.md
Normal file
@ -0,0 +1,184 @@
|
||||
# Clustering Guide
|
||||
|
||||
This guide will walk you through configuring a three machine etcd cluster with
|
||||
the following details:
|
||||
|
||||
|Name |Address |
|
||||
|-------|---------------|
|
||||
|infra0 |10.0.1.10 |
|
||||
|infra1 |10.0.1.11 |
|
||||
|infra2 |10.0.1.12 |
|
||||
|
||||
## Static
|
||||
|
||||
As we know the cluster members, their addresses and the size of the cluster
|
||||
before starting we can use an offline bootstrap configuration. Each machine
|
||||
will get either the following command line or environment variables:
|
||||
|
||||
```
|
||||
ETCD_INITIAL_CLUSTER=”infra0=http://10.0.1.10:2379,infra1=http://10.0.1.11:2379,infra2=http://10.0.1.12:2379”
|
||||
ETCD_INITIAL_CLUSTER_STATE=new
|
||||
```
|
||||
|
||||
```
|
||||
-initial-cluster infra0=http://10.0.1.10:2379,http://10.0.1.11:2379,infra2=http://10.0.1.12:2379 \
|
||||
-initial-cluster-state new
|
||||
```
|
||||
|
||||
On each machine you would start etcd with these flags:
|
||||
|
||||
```
|
||||
$ etcd -name infra0 -initial-advertise-peer-urls https://10.0.1.10:2379 \
|
||||
-initial-cluster infra0=http://10.0.1.10:2379,infra1=http://10.0.1.11:2379,infra2=http://10.0.1.12:2379 \
|
||||
-initial-cluster-state new
|
||||
$ etcd -name infra1 -initial-advertise-peer-urls https://10.0.1.11:2379 \
|
||||
-initial-cluster infra0=http://10.0.1.10:2379,infra1=http://10.0.1.11:2379,infra2=http://10.0.1.12:2379 \
|
||||
-initial-cluster-state new
|
||||
$ etcd -name infra2 -initial-advertise-peer-urls https://10.0.1.12:2379 \
|
||||
-initial-cluster infra0=http://10.0.1.10:2379,infra1=http://10.0.1.11:2379,infra2=http://10.0.1.12:2379 \
|
||||
-initial-cluster-state new
|
||||
```
|
||||
|
||||
The command line parameters starting with `-initial-cluster` will be ignored on
|
||||
subsequent runs of etcd. You are free to remove the environment variables or
|
||||
command line flags after the initial bootstrap process. If you need to make
|
||||
changes to the configuration later see our guide on runtime configuration.
|
||||
|
||||
### Error Cases
|
||||
|
||||
In the following case we have not included our new host in the list of
|
||||
enumerated nodes. If this is a new cluster, the node must be added to the list
|
||||
of initial cluster members.
|
||||
```
|
||||
$ etcd -name infra1 -initial-advertise-peer-urls http://10.0.1.11:2379 \
|
||||
-initial-cluster infra0=http://10.0.1.10:2379 \
|
||||
-initial-cluster-state new
|
||||
etcd: infra1 not listed in the initial cluster config
|
||||
exit 1
|
||||
```
|
||||
|
||||
In this case we are attempting to map a node (infra0) on a different address
|
||||
(127.0.0.1:2379) than its enumerated address in the cluster list
|
||||
(10.0.1.10:2379). If this node is to listen on multiple addresses, all
|
||||
addresses must be reflected in the “initial-cluster” configuration directive.
|
||||
|
||||
```
|
||||
$ etcd -name infra0 -initial-advertise-peer-urls http://127.0.0.1:2379 \
|
||||
-initial-cluster infra0=http://10.0.1.10:2379,infra1=http://10.0.1.11:2379,infra2=http://10.0.1.12:2379 \
|
||||
-initial-cluster-state=new
|
||||
etcd: infra0 has different advertised URLs in the cluster and advertised peer URLs list
|
||||
exit 1
|
||||
```
|
||||
|
||||
If you configure a peer with a different set of configuration and attempt to
|
||||
join this cluster you will get a cluster ID mismatch and etcd will exit.
|
||||
|
||||
```
|
||||
$ etcd -name infra3 -initial-advertise-peer-urls http://10.0.1.13:2379 \
|
||||
-initial-cluster infra0=http://10.0.1.10:2379,infra1=http://10.0.1.11:2379,infra3=http://10.0.1.13:2379 \
|
||||
-initial-cluster-state=new
|
||||
etcd: conflicting cluster ID to the target cluster (c6ab534d07e8fcc4 != bc25ea2a74fb18b0). Exiting.
|
||||
exit 1
|
||||
```
|
||||
|
||||
|
||||
## Discovery
|
||||
|
||||
In a number of cases you might not know the IPs of your cluster peers ahead of
|
||||
time. This is common when utilizing cloud providers or when your network uses
|
||||
DHCP. In these cases you can use an existing etcd cluster to bootstrap a new
|
||||
one. We call this process “discovery”.
|
||||
|
||||
Discovery uses an existing cluster to bootstrap itself. If you are using your
|
||||
own etcd cluster you can create a URL like so:
|
||||
|
||||
```
|
||||
$ curl -X PUT https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83/_config/size -d value=5
|
||||
```
|
||||
|
||||
The URL you will use in this case will be
|
||||
`https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83`
|
||||
and the machines will use the
|
||||
`https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83`
|
||||
directory for registration as they start.
|
||||
|
||||
If you do not have access to an existing cluster you can use the hosted
|
||||
discovery.etcd.io service. You can create a private discovery URL using the
|
||||
"new" endpoint like so:
|
||||
|
||||
```
|
||||
$ 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 on each machine:
|
||||
|
||||
```
|
||||
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2379 -discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
$ etcd -name infra1 -initial-advertise-peer-urls http://10.0.1.11:2379 -discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
$ etcd -name infra2 -initial-advertise-peer-urls http://10.0.1.12:2379 -discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
```
|
||||
|
||||
This will cause each machine to register itself with the etcd service and begin
|
||||
the cluster once all machines have been registered.
|
||||
|
||||
You can use the environment variable `ETCD_DISCOVERY_PROXY` to cause etcd to use an HTTP proxy to connect to the discovery service.
|
||||
|
||||
### Error and Warning Cases
|
||||
|
||||
#### Discovery Server Errors
|
||||
|
||||
```
|
||||
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2379 -discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
etcd: error: the cluster doesn’t have a size configuration value in https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de/_config
|
||||
exit 1
|
||||
```
|
||||
|
||||
#### User Errors
|
||||
|
||||
```
|
||||
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2379 -discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
etcd: error: the cluster using discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de has already started with all 5 members
|
||||
exit 1
|
||||
```
|
||||
|
||||
#### Warnings
|
||||
|
||||
This is a harmless warning notifying you that the discovery URL will be
|
||||
ignored on this machine.
|
||||
|
||||
```
|
||||
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2379 -discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
|
||||
etcd: warn: ignoring discovery URL: etcd has already been initialized and has a valid log in /var/lib/etcd
|
||||
```
|
||||
|
||||
# 0.4 to 0.5+ Migration Guide
|
||||
|
||||
In etcd 0.5 we introduced the ability to listen on more than one address and to
|
||||
advertise multiple addresses. This makes using etcd easier when you have
|
||||
complex networking, such as private and public networks on various cloud
|
||||
providers.
|
||||
|
||||
To make understanding this feature easier, we changed the naming of some flags,
|
||||
but we support the old flags to make the migration from the old to new version
|
||||
easier.
|
||||
|
||||
|Old Flag |New Flag |Migration Behavior |
|
||||
|-----------------------|-----------------------|---------------------------------------------------------------------------------------|
|
||||
|-peer-addr |-initial-advertise-peer-urls |If specified, peer-addr will be used as the only peer URL. Error if both flags specified.|
|
||||
|-addr |-advertise-client-urls |If specified, addr will be used as the only client URL. Error if both flags specified.|
|
||||
|-peer-bind-addr |-listen-peer-urls |If specified, peer-bind-addr will be used as the only peer bind URL. Error if both flags specified.|
|
||||
|-bind-addr |-listen-client-urls |If specified, bind-addr will be used as the only client bind URL. Error if both flags specified.|
|
||||
|-peers |none |Deprecated. The -initial-cluster flag provides a similar concept with different semantics. Please read this guide on cluster startup.|
|
||||
|-peers-file |none |Deprecated. The -initial-cluster flag provides a similar concept with different semantics. Please read this guide on cluster startup.|
|
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
|
@ -13,6 +13,14 @@ This will bring up etcd listening on default ports (4001 for client communicatio
|
||||
The `-data-dir machine0` argument tells etcd to write machine configuration, logs and snapshots to the `./machine0/` directory.
|
||||
The `-name machine0` tells the rest of the cluster that this machine is named machine0.
|
||||
|
||||
## Getting the etcd version
|
||||
|
||||
The etcd version of a specific instance can be obtained from the `/version` endpoint.
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/version
|
||||
```
|
||||
|
||||
## Key Space Operations
|
||||
|
||||
The primary API of etcd is a hierarchical key space.
|
||||
@ -21,7 +29,7 @@ The key space consists of directories and keys which are generically referred to
|
||||
|
||||
### Setting the value of a key
|
||||
|
||||
Let’s set the first key-value pair in the datastore.
|
||||
Let's set the first key-value pair in the datastore.
|
||||
In this case the key is `/message` and the value is `Hello world`.
|
||||
|
||||
```sh
|
||||
@ -58,7 +66,7 @@ You may notice that in this example the index is `2` even though it is the first
|
||||
This is because there are internal commands that also change the state behind the scenes, like adding and syncing servers.
|
||||
|
||||
5. `node.modifiedIndex`: like `node.createdIndex`, this attribute is also an etcd index.
|
||||
Actions that cause the value to change include `set`, `delete`, `update`, `create` and `compareAndSwap`.
|
||||
Actions that cause the value to change include `set`, `delete`, `update`, `create`, `compareAndSwap` and `compareAndDelete`.
|
||||
Since the `get` and `watch` commands do not change state in the store, they do not change the value of `node.modifiedIndex`.
|
||||
|
||||
|
||||
@ -118,11 +126,10 @@ curl -L http://127.0.0.1:4001/v2/keys/message -XPUT -d value="Hello etcd"
|
||||
"value": "Hello etcd"
|
||||
},
|
||||
"prevNode": {
|
||||
"createdIndex":2
|
||||
"createdIndex": 2,
|
||||
"key": "/message",
|
||||
"value": "Hello world",
|
||||
"modifiedIndex": 2,
|
||||
|
||||
"modifiedIndex": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -199,7 +206,33 @@ If the TTL has expired, the key will have been deleted, and you will be returned
|
||||
"cause": "/foo",
|
||||
"errorCode": 100,
|
||||
"index": 6,
|
||||
"message": "Key Not Found"
|
||||
"message": "Key not found"
|
||||
}
|
||||
```
|
||||
|
||||
The TTL could be unset to avoid expiration through update operation:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar -d ttl= -d prevExist=true
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "update",
|
||||
"node": {
|
||||
"createdIndex": 5,
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 6,
|
||||
"value": "bar"
|
||||
},
|
||||
"prevNode": {
|
||||
"createdIndex": 5,
|
||||
"expiration": "2013-12-04T12:01:21.874888581-08:00",
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 5,
|
||||
"ttl": 3,
|
||||
"value": "bar"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -350,7 +383,7 @@ curl -L http://127.0.0.1:4001/v2/keys/dir -XPUT -d ttl=30 -d dir=true
|
||||
"createdIndex": 17,
|
||||
"dir": true,
|
||||
"expiration": "2013-12-11T10:37:33.689275857-08:00",
|
||||
"key": "/newdir",
|
||||
"key": "/dir",
|
||||
"modifiedIndex": 17,
|
||||
"ttl": 30
|
||||
}
|
||||
@ -384,7 +417,7 @@ curl 'http://127.0.0.1:4001/v2/keys/dir/asdf?consistent=true&wait=true'
|
||||
"dir":true,
|
||||
"modifiedIndex": 17,
|
||||
"expiration": "2013-12-11T10:39:35.689275857-08:00"
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -399,7 +432,7 @@ The current comparable conditions are:
|
||||
|
||||
1. `prevValue` - checks the previous value of the key.
|
||||
|
||||
2. `prevIndex` - checks the previous index of the key.
|
||||
2. `prevIndex` - checks the previous modifiedIndex of the key.
|
||||
|
||||
3. `prevExist` - checks existence of the key: if `prevExist` is true, it is an `update` request; if prevExist is `false`, it is a `create` request.
|
||||
|
||||
@ -424,7 +457,7 @@ The error code explains the problem:
|
||||
"cause": "/foo",
|
||||
"errorCode": 105,
|
||||
"index": 39776,
|
||||
"message": "Already exists"
|
||||
"message": "Key already exists"
|
||||
}
|
||||
```
|
||||
|
||||
@ -438,14 +471,15 @@ This will try to compare the previous value of the key and the previous value we
|
||||
|
||||
```json
|
||||
{
|
||||
"cause": "[two != one] [0 != 8]",
|
||||
"cause": "[two != one]",
|
||||
"errorCode": 101,
|
||||
"index": 8,
|
||||
"message": "Test Failed"
|
||||
"message": "Compare failed"
|
||||
}
|
||||
```
|
||||
|
||||
which means `CompareAndSwap` failed.
|
||||
which means `CompareAndSwap` failed. `cause` explains why the test failed.
|
||||
Note: the condition prevIndex=0 always passes.
|
||||
|
||||
Let's try a valid condition:
|
||||
|
||||
@ -475,6 +509,79 @@ The response should be:
|
||||
|
||||
We successfully changed the value from "one" to "two" since we gave the correct previous value.
|
||||
|
||||
### Atomic Compare-and-Delete
|
||||
|
||||
This command will delete a key only if the client-provided conditions are equal to the current conditions.
|
||||
|
||||
The current comparable conditions are:
|
||||
|
||||
1. `prevValue` - checks the previous value of the key.
|
||||
|
||||
2. `prevIndex` - checks the previous modifiedIndex of the key.
|
||||
|
||||
Here is a simple example. Let's first create a key: `foo=one`.
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/keys/foo -XPUT -d value=one
|
||||
```
|
||||
|
||||
Now let's try some `CompareAndDelete` commands.
|
||||
|
||||
Trying to delete the key with `prevValue=two` fails as expected:
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/keys/foo?prevValue=two -XDELETE
|
||||
```
|
||||
|
||||
The error code explains the problem:
|
||||
|
||||
```json
|
||||
{
|
||||
"errorCode": 101,
|
||||
"message": "Compare failed",
|
||||
"cause": "[two != one]",
|
||||
"index": 8
|
||||
}
|
||||
```
|
||||
|
||||
As does a `CompareAndDelete` with a mismatched `prevIndex`:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/keys/foo?prevIndex=1 -XDELETE
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"errorCode": 101,
|
||||
"message": "Compare failed",
|
||||
"cause": "[1 != 8]",
|
||||
"index": 8
|
||||
}
|
||||
```
|
||||
|
||||
And now a valid `prevValue` condition:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/keys/foo?prevValue=one -XDELETE
|
||||
```
|
||||
|
||||
The successful response will look something like:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "compareAndDelete",
|
||||
"node": {
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 9,
|
||||
"createdIndex": 8
|
||||
},
|
||||
"prevNode": {
|
||||
"key": "/foo",
|
||||
"value": "one",
|
||||
"modifiedIndex": 8,
|
||||
"createdIndex": 8
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Creating Directories
|
||||
|
||||
@ -536,16 +643,22 @@ We should see the response as an array of items:
|
||||
```json
|
||||
{
|
||||
"action": "get",
|
||||
"node": {
|
||||
"dir": true,
|
||||
"key": "/",
|
||||
"nodes": [
|
||||
{
|
||||
"createdIndex": 2,
|
||||
"dir": true,
|
||||
"key": "/foo_dir",
|
||||
"modifiedIndex": 2
|
||||
}
|
||||
"node": {
|
||||
"key": "/",
|
||||
"dir": true,
|
||||
"nodes": [
|
||||
{
|
||||
"key": "/foo_dir",
|
||||
"dir": true,
|
||||
"modifiedIndex": 2,
|
||||
"createdIndex": 2
|
||||
},
|
||||
{
|
||||
"key": "/foo",
|
||||
"value": "two",
|
||||
"modifiedIndex": 1,
|
||||
"createdIndex": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -560,27 +673,33 @@ curl -L http://127.0.0.1:4001/v2/keys/?recursive=true
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "get",
|
||||
"node": {
|
||||
"dir": true,
|
||||
"key": "/",
|
||||
"nodes": [
|
||||
{
|
||||
"createdIndex": 2,
|
||||
"dir": true,
|
||||
"key": "/foo_dir",
|
||||
"modifiedIndex": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"createdIndex": 2,
|
||||
"key": "/foo_dir/foo",
|
||||
"modifiedIndex": 2,
|
||||
"value": "bar"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
"action": "get",
|
||||
"node": {
|
||||
"key": "/",
|
||||
"dir": true,
|
||||
"nodes": [
|
||||
{
|
||||
"key": "/foo_dir",
|
||||
"dir": true,
|
||||
"nodes": [
|
||||
{
|
||||
"key": "/foo_dir/foo",
|
||||
"value": "bar",
|
||||
"modifiedIndex": 2,
|
||||
"createdIndex": 2
|
||||
}
|
||||
],
|
||||
"modifiedIndex": 2,
|
||||
"createdIndex": 2
|
||||
},
|
||||
{
|
||||
"key": "/foo",
|
||||
"value": "two",
|
||||
"modifiedIndex": 1,
|
||||
"createdIndex": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -710,13 +829,63 @@ curl -L http://127.0.0.1:4001/v2/keys/
|
||||
|
||||
Here we see the `/message` key but our hidden `/_message` key is not returned.
|
||||
|
||||
### Setting a key from a file
|
||||
|
||||
## Lock Module
|
||||
You can also use etcd to store small configuration files, json documents, XML documents, etc directly.
|
||||
For example you can use curl to upload a simple text file and encode it:
|
||||
|
||||
```
|
||||
echo "Hello\nWorld" > afile.txt
|
||||
curl -L http://127.0.0.1:4001/v2/keys/afile -XPUT --data-urlencode value@afile.txt
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "get",
|
||||
"node": {
|
||||
"createdIndex": 2,
|
||||
"key": "/afile",
|
||||
"modifiedIndex": 2,
|
||||
"value": "Hello\nWorld\n"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Read Consistency
|
||||
|
||||
#### Read from the Master
|
||||
|
||||
Followers in a cluster can be behind the leader in their copy of the keyspace.
|
||||
If your application wants or needs the most up-to-date version of a key then it should ensure it reads from the current leader.
|
||||
By using the `consistent=true` flag in your GET requests, etcd will make sure you are talking to the current master.
|
||||
|
||||
As an example of how a machine can be behind the leader let's start with a three machine cluster: L, F1, and F2.
|
||||
A client makes a write to L and F1 acknowledges the request.
|
||||
The client is told the write was successful and the keyspace is updated.
|
||||
Meanwhile F2 has partitioned from the network and will have an out-of-date version of the keyspace until the partition resolves.
|
||||
Since F2 missed the most recent write, a client reading from F2 will have an out-of-date version of the keyspace.
|
||||
|
||||
Implementation notes on `consistent=true`: If the leader you are talking to is
|
||||
partitioned it will be unable to determine if it is not currently the master.
|
||||
In a later version we will provide a mechanism to set an upperbound of time
|
||||
that the current master can be unable to contact the quorom and still serve
|
||||
reads.
|
||||
|
||||
### Read Linearization
|
||||
|
||||
If you want a read that is fully linearized you can use a `quorum=true` GET.
|
||||
The read will take a very similar path to a write and will have a similar
|
||||
speed. If you are unsure if you need this feature feel free to email etcd-dev
|
||||
for advice.
|
||||
|
||||
## Lock Module (*Deprecated and Removed*)
|
||||
|
||||
The lock module is used to serialize access to resources used by clients.
|
||||
Multiple clients can attempt to acquire a lock but only one can have it at a time.
|
||||
Once the lock is released, the next client waiting for the lock will receive it.
|
||||
|
||||
**Warning:** This module is deprecated and removed at v0.4. See [Modules][modules] for more details.
|
||||
|
||||
|
||||
### Acquiring a Lock
|
||||
|
||||
@ -797,7 +966,7 @@ If the TTL is not specified or is not a number then you'll receive the following
|
||||
When the client is finished with the lock, simply send a `DELETE` request to release the lock:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/mod/v2/lock/mylock -XDELETE -d index=5
|
||||
curl -L http://127.0.0.1:4001/mod/v2/lock/mylock?index=5 -XDELETE
|
||||
```
|
||||
|
||||
If the index or value is not specified then you'll receive the following error:
|
||||
@ -847,7 +1016,7 @@ Will return the current index:
|
||||
2
|
||||
```
|
||||
|
||||
If you specify a field other than `field` or `value` then you'll receive the following error:
|
||||
If you specify a field other than `index` or `value` then you'll receive the following error:
|
||||
|
||||
```json
|
||||
{
|
||||
@ -858,10 +1027,13 @@ If you specify a field other than `field` or `value` then you'll receive the fol
|
||||
```
|
||||
|
||||
|
||||
## Leader Module
|
||||
## Leader Module (*Deprecated*)
|
||||
|
||||
The leader module wraps the lock module to provide a simple interface for electing a single leader in a cluster.
|
||||
|
||||
**Warning:** This module is deprecated at v0.4. See [Modules][modules] for more details.
|
||||
[modules]: https://github.com/coreos/etcd/blob/master/Documentation/modules.md
|
||||
|
||||
|
||||
### Setting the Leader
|
||||
|
||||
@ -981,9 +1153,9 @@ Each node keeps a number of internal statistics:
|
||||
- `recvBandwidthRate`: number of bytes per second this node is receiving (follower only)
|
||||
- `recvPkgRate`: number of requests per second this node is receiving (follower only)
|
||||
- `sendAppendRequestCnt`: number of requests that this node has sent
|
||||
- `sendBandwidthRate`: number of bytes per second this node is receiving (leader only)
|
||||
- `sendPkgRate`: number of requests per second this node is receiving (leader only)
|
||||
- `state`: either leader or folower
|
||||
- `sendBandwidthRate`: number of bytes per second this node is receiving (leader only). This value is undefined on single machine clusters.
|
||||
- `sendPkgRate`: number of requests per second this node is receiving (leader only). This value is undefined on single machine clusters.
|
||||
- `state`: either leader or follower
|
||||
- `startTime`: the time when this node was started
|
||||
|
||||
This is an example response from a follower machine:
|
||||
@ -1014,7 +1186,7 @@ And this is an example response from a leader machine:
|
||||
curl -L http://127.0.0.1:4001/v2/stats/self
|
||||
```
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"leaderInfo": {
|
||||
"leader": "machine0",
|
||||
@ -1060,3 +1232,95 @@ curl -L http://127.0.0.1:4001/v2/stats/store
|
||||
"watchers": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Cluster Config
|
||||
|
||||
The configuration endpoint manages shared cluster wide properties.
|
||||
|
||||
### Set Cluster Config
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:7001/v2/admin/config -XPUT -d '{"activeSize":3, "removeDelay":1800,"syncInterval":5}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"activeSize": 3,
|
||||
"removeDelay": 1800,
|
||||
"syncInterval":5
|
||||
}
|
||||
```
|
||||
|
||||
`activeSize` is the maximum number of peers that can join the cluster and participate in the consensus protocol.
|
||||
|
||||
The size of cluster is controlled to be around a certain number. If it is not, standby-mode instances will join or peer-mode instances will be removed to make it happen.
|
||||
|
||||
`removeDelay` indicates the minimum time that a machine has been observed to be unresponsive before it is removed from the cluster.
|
||||
|
||||
### Get Cluster Config
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:7001/v2/admin/config
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"activeSize": 3,
|
||||
"removeDelay": 1800,
|
||||
"syncInterval":5
|
||||
}
|
||||
```
|
||||
|
||||
## Remove Machines
|
||||
|
||||
At times you may want to manually remove a machine. Using the machines endpoint
|
||||
you can find and remove machines.
|
||||
|
||||
First, list all the machines in the cluster.
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:7001/v2/admin/machines
|
||||
```
|
||||
```json
|
||||
[
|
||||
{
|
||||
"clientURL": "http://127.0.0.1:4001",
|
||||
"name": "peer1",
|
||||
"peerURL": "http://127.0.0.1:7001",
|
||||
"state": "leader"
|
||||
},
|
||||
{
|
||||
"clientURL": "http://127.0.0.1:4002",
|
||||
"name": "peer2",
|
||||
"peerURL": "http://127.0.0.1:7002",
|
||||
"state": "follower"
|
||||
},
|
||||
{
|
||||
"clientURL": "http://127.0.0.1:4003",
|
||||
"name": "peer3",
|
||||
"peerURL": "http://127.0.0.1:7003",
|
||||
"state": "follower"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Then take a closer look at the machine you want to remove.
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:7001/v2/admin/machines/peer2
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"clientURL": "http://127.0.0.1:4002",
|
||||
"name": "peer2",
|
||||
"peerURL": "http://127.0.0.1:7002",
|
||||
"state": "follower"
|
||||
}
|
||||
```
|
||||
|
||||
And finally remove it.
|
||||
|
||||
```sh
|
||||
curl -L -XDELETE http://127.0.0.1:7001/v2/admin/machines/peer2
|
||||
```
|
||||
|
@ -1,46 +1,43 @@
|
||||
# Client libraries support matrix for etcd
|
||||
|
||||
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
|
||||
|
||||
The v2 API has a lot of features, we will categorize them in a few categories:
|
||||
|
||||
- **Language**: The language in which the client library was written.
|
||||
- **HTTPS Auth**: Support for SSL-certificate based authentication
|
||||
- **Reconnect**: If the client is able to reconnect automatically to another server if one fails.
|
||||
- **Mod/Lock**: Support for the locking module
|
||||
- **Mod/Leader**: Support for the leader election module
|
||||
- **GET,PUT,POST,DEL Features**: Support for all the modifiers when calling the etcd server with said HTTP method.
|
||||
|
||||
|
||||
### Supported features matrix
|
||||
|
||||
| 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)|
|
||||
| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
|
||||
| **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 |
|
||||
| **PUT Features** | F | B | F | F | F | F | F | G | F | G | F |
|
||||
| **POST Features** | F | - | F | F | - | F | F | - | - | - | F |
|
||||
| **DEL Features** | F | B | F | F | F | F | F | B | G | B | F |
|
||||
|
||||
**Legend**
|
||||
|
||||
**F**: Full support **G**: Good support **B**: Basic support
|
||||
**Y**: Feature supported **-**: Feature not supported
|
||||
|
||||
Sorted alphabetically on language/name
|
||||
|
||||
|Client |**Language**|**HTTPS Auth**|**Re-connect**|**GET**|**PUT**|**POST**|**DEL**|**Mod Lock**|**Mod Leader**|
|
||||
| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
|
||||
|[etcd-api](https://github.com/jdarcy/etcd-api) |C |-|Y|B|G|-|B|-|-|
|
||||
|[etcdcpp](https://github.com/edwardcapriolo/etcdcpp) |C++ |-|-|F|F|G|-|-|-|
|
||||
|[cetcd](https://github.com/dwwoelfel/cetcd) |Clojure|-|-|F|F|-|G|-|-|
|
||||
|[clj-etcd](https://github.com/rthomas/clj-etcd) |Clojure|-|-|G|G|-|B|-|-|
|
||||
|[etcd-clojure](https://github.com/aterreno/etcd-clojure) |Clojure|-|-|F|F|F|F|-|-|
|
||||
|[go-etcd](https://github.com/coreos/go-etcd) |go |Y|Y|F|F|F|F|-|-|
|
||||
|[boon etcd client](https://github.com/boonproject/boon/blob/master/etcd/README.md) |java |Y|Y|F|F|F|F|-|F|
|
||||
|[etcd4j](https://github.com/jurmous/etcd4j) |java |Y|Y|F|F|F|F|-|-|
|
||||
|[jetcd](https://github.com/diwakergupta/jetcd) |java |Y|-|B|B|-|B|-|-|
|
||||
|[jetcd](https://github.com/justinsb/jetcd) |java |-|-|B|B|-|B|-|-|
|
||||
|[Etcd.jl](https://github.com/forio/Etcd.jl) |Julia |-|-|F|F|F|F|Y|Y|
|
||||
|[etcetera](https://github.com/drusellers/etcetera) |.net |-|-|F|F|F|F|-|-|
|
||||
|[node-etcd](https://github.com/stianeikeland/node-etcd) |nodejs |Y|-|F|F|-|F|-|-|
|
||||
|[nodejs-etcd](https://github.com/lavagetto/nodejs-etcd) |nodejs |Y|-|F|F|F|F|-|-|
|
||||
|[p5-etcd](https://metacpan.org/release/Etcd) |perl |-|-|F|F|F|F|-|-|
|
||||
|[python-etcd](https://github.com/jplana/python-etcd) |python |Y|Y|F|F|F|F|Y|-|
|
||||
|[python-etcd-client](https://github.com/dsoprea/PythonEtcdClient)|python |Y|Y|F|F|F|F|Y|Y|
|
||||
|[txetcd](https://github.com/russellhaering/txetcd) |python |-|-|G|G|F|G|-|-|
|
||||
|[etcd-ruby](https://github.com/ranjib/etcd-ruby) |ruby |-|-|F|F|F|F|-|-|
|
||||
|
@ -2,15 +2,22 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Starting an etcd cluster can be painful since each node needs to know of another node in the cluster to get started. If you are trying to bring up a cluster all at once, say using a cloud formation, you also need to coordinate who will be the initial cluster leader. The discovery protocol helps you by providing an automated way to discover other existing peers in a cluster.
|
||||
Starting an etcd cluster requires that each node knows another in the cluster. If you are trying to bring up a cluster all at once, say using a cloud formation, you also need to coordinate who will be the initial cluster leader. The discovery protocol helps you by providing an automated way to discover other existing peers in a cluster.
|
||||
|
||||
For more information on how etcd can locate the cluster, see the [finding the cluster][cluster-finding] documentation.
|
||||
|
||||
Please note - at least 3 nodes are required for [cluster availability][optimal-cluster-size].
|
||||
|
||||
[cluster-finding]: https://github.com/coreos/etcd/blob/master/Documentation/design/cluster-finding.md
|
||||
[optimal-cluster-size]: https://github.com/coreos/etcd/blob/master/Documentation/optimal-cluster-size.md
|
||||
|
||||
## Using discovery.etcd.io
|
||||
|
||||
### Create a 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
|
||||
|
||||
@ -19,9 +26,10 @@ Specify the `-discovery` flag when you start each etcd instance. The list of exi
|
||||
Here's a full example:
|
||||
|
||||
```
|
||||
TOKEN=$(curl https://discovery.etcd.io/new)
|
||||
./etcd -name instance1 -peer-addr 10.1.2.3:7001 -addr 10.1.2.3:4001 -discovery https://discovery.etcd.io/$TOKEN
|
||||
./etcd -name instance2 -peer-addr 10.1.2.4:7002 -addr 10.1.2.4:4002 -discovery https://discovery.etcd.io/$TOKEN
|
||||
URL=$(curl https://discovery.etcd.io/new)
|
||||
./etcd -name instance1 -peer-addr 10.1.2.3:7001 -addr 10.1.2.3:4001 -discovery $URL
|
||||
./etcd -name instance2 -peer-addr 10.1.2.4:7001 -addr 10.1.2.4:4001 -discovery $URL
|
||||
./etcd -name instance3 -peer-addr 10.1.2.5:7001 -addr 10.1.2.5:4001 -discovery $URL
|
||||
```
|
||||
|
||||
## Running Your Own Discovery Endpoint
|
||||
@ -29,9 +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`:
|
||||
|
||||
```
|
||||
TOKEN="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 instance2 -peer-addr 10.1.2.4:7002 -addr 10.1.2.4:4002 -discovery http://10.10.10.10:4001/v2/keys/$TOKEN
|
||||
URL="http://10.10.10.10:4001/v2/keys/testcluster"
|
||||
./etcd -name instance1 -peer-addr 10.1.2.3:7001 -addr 10.1.2.3:4001 -discovery $URL
|
||||
./etcd -name instance2 -peer-addr 10.1.2.4:7001 -addr 10.1.2.4:4001 -discovery $URL
|
||||
./etcd -name instance3 -peer-addr 10.1.2.5:7001 -addr 10.1.2.5:4001 -discovery $URL
|
||||
```
|
||||
|
||||
If you're interested in how to discovery API works behind the scenes, read about the [Discovery Protocol](https://github.com/coreos/etcd/blob/master/Documentation/discovery-protocol.md).
|
||||
@ -43,3 +52,9 @@ The Discovery API submits the `-peer-addr` of each etcd instance to the configur
|
||||
## Stale Peers
|
||||
|
||||
The discovery API will automatically clean up the address of a stale peer that is no longer part of the cluster. The TTL for this process is a week, which should be long enough to handle any extremely long outage you may encounter. There is no harm in having stale peers in the list until they are cleaned up, since an etcd instance only needs to connect to one valid peer in the cluster to join.
|
||||
|
||||
## Lifetime of a Discovery URL
|
||||
|
||||
A discovery URL identifies a single etcd cluster. Do not re-use discovery URLs for new clusters.
|
||||
|
||||
When a machine starts with a new discovery URL the discovery URL will be activated and record the machine's metadata. If you destroy the whole cluster and attempt to bring the cluster back up with the same discovery URL it will fail. This is intentional because all of the registered machines are gone including their logs so there is nothing to recover the killed cluster.
|
||||
|
@ -103,6 +103,13 @@ curl -L http://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar
|
||||
}
|
||||
```
|
||||
|
||||
### Rejoining to the Cluster
|
||||
|
||||
If one machine disconnects from the cluster, it could rejoin the cluster automatically when the communication is recovered.
|
||||
|
||||
If one machine is killed, it could rejoin the cluster when started with old name. If the peer address is changed, etcd will treat the new peer address as the refreshed one, which benefits instance migration, or virtual machine boot with different IP. The peer-address-changing functionality is only supported when the majority of the cluster is alive, because this behavior needs the consensus of the etcd cluster.
|
||||
|
||||
**Note:** For now, it is user responsibility to ensure that the machine doesn't join the cluster that has the member with the same name. Or unexpected error will happen. It would be improved sooner or later.
|
||||
|
||||
### Killing Nodes in the Cluster
|
||||
|
||||
@ -161,14 +168,8 @@ 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.
|
||||
|
||||
### Bootstrapping a new cluster by name
|
||||
|
||||
### What size cluster should I use?
|
||||
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.
|
||||
|
||||
Every command the client sends to the master is broadcast to all of the followers.
|
||||
The command is not committed until the majority of the cluster peers receive that command.
|
||||
|
||||
Because of this majority voting property, the ideal cluster should be kept small to keep speed up and be made up of an odd number of peers.
|
||||
|
||||
Odd numbers are good because if you have 8 peers the majority will be 5 and if you have 9 peers the majority will still be 5.
|
||||
The result is that an 8 peer cluster can tolerate 3 peer failures and a 9 peer cluster can tolerate 4 machine failures.
|
||||
And in the best case when all 9 peers are responding the cluster will perform at the speed of the fastest 5 machines.
|
||||
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.
|
||||
|
@ -1,6 +1,8 @@
|
||||
# Etcd Configuration
|
||||
|
||||
Configuration options can be set in three places:
|
||||
## Node Configuration
|
||||
|
||||
Individual node configuration options can be set in three places:
|
||||
|
||||
1. Command line flags
|
||||
2. Environment variables
|
||||
@ -10,35 +12,51 @@ Options set on the command line take precedence over all other sources.
|
||||
Options set in environment variables take precedence over options set in
|
||||
configuration files.
|
||||
|
||||
## Cluster Configuration
|
||||
|
||||
Cluster-wide settings are configured via the `/config` admin endpoint and additionally in the configuration file. Values contained in the configuration file will seed the cluster setting with the provided value. After the cluster is running, only the admin endpoint is used.
|
||||
|
||||
The full documentation is contained in the [API docs](https://github.com/coreos/etcd/blob/master/Documentation/api.md#cluster-config).
|
||||
|
||||
* `activeSize` - the maximum number of peers that can participate in the consensus protocol. Other peers will join as standbys.
|
||||
* `removeDelay` - the minimum time in seconds that a machine has been observed to be unresponsive before it is removed from the cluster.
|
||||
* `syncInterval` - the amount of time in seconds between cluster sync when it runs in standby mode.
|
||||
|
||||
## Command Line Flags
|
||||
|
||||
### Required
|
||||
|
||||
* `-name` - The node name. Defaults to the hostname.
|
||||
* `-name` - The node name. Defaults to a UUID.
|
||||
|
||||
### Optional
|
||||
|
||||
* `-addr` - The advertised public hostname:port for client communication. Defaults to `127.0.0.1:4001`.
|
||||
* `-discovery` - A URL to use for discovering the peer list. (i.e `"https://discovery.etcd.io/your-unique-key"`).
|
||||
* `-bind-addr` - The listening hostname for client communication. Defaults to advertised ip.
|
||||
* `-http-read-timeout` - The number of seconds before an HTTP read operation is timed out.
|
||||
* `-http-write-timeout` - The number of seconds before an HTTP write operation is timed out.
|
||||
* `-bind-addr` - The listening hostname for client communication. Defaults to advertised IP.
|
||||
* `-peers` - A comma separated list of peers in the cluster (i.e `"203.0.113.101:7001,203.0.113.102:7001"`).
|
||||
* `-peers-file` - The file path containing a comma separated list of peers in the cluster.
|
||||
* `-ca-file` - The path of the client CAFile. Enables client cert authentication when present.
|
||||
* `-cert-file` - The cert file of the client.
|
||||
* `-key-file` - The key file of the client.
|
||||
* `-config` - The path of the etcd config file. Defaults to `/etc/etcd/etcd.conf`.
|
||||
* `-config` - The path of the etcd configuration file. Defaults to `/etc/etcd/etcd.conf`.
|
||||
* `-cors` - A comma separated white list of origins for cross-origin resource sharing.
|
||||
* `-cpuprofile` - The path to a file to output cpu profile data. Enables cpu profiling when present.
|
||||
* `-cpuprofile` - The path to a file to output CPU profile data. Enables CPU profiling when present.
|
||||
* `-data-dir` - The directory to store log and snapshot. Defaults to the current working directory.
|
||||
* `-max-result-buffer` - The max size of result buffer. Defaults to `1024`.
|
||||
* `-max-cluster-size` - The max size of the cluster. Defaults to `9`.
|
||||
* `-max-retry-attempts` - The max retry attempts when trying to join a cluster. Defaults to `3`.
|
||||
* `-peer-addr` - The advertised public hostname:port for server communication. Defaults to `127.0.0.1:7001`.
|
||||
* `-peer-bind-addr` - The listening hostname for server communication. Defaults to advertised ip.
|
||||
* `-peer-bind-addr` - The listening hostname for server communication. Defaults to advertised IP.
|
||||
* `-peer-ca-file` - The path of the CAFile. Enables client/peer cert authentication when present.
|
||||
* `-peer-cert-file` - The cert file of the server.
|
||||
* `-peer-key-file` - The key file of the server.
|
||||
* `-peer-election-timeout` - The number of milliseconds to wait before the leader is declared unhealthy.
|
||||
* `-peer-heartbeat-interval` - The number of milliseconds in between heartbeat requests
|
||||
* `-snapshot=false` - Disable log snapshots. Defaults to `true`.
|
||||
* `-cluster-active-size` - The expected number of instances participating in the consensus protocol. Only applied if the etcd instance is the first peer in the cluster.
|
||||
* `-cluster-remove-delay` - The number of seconds before one node is removed from the cluster since it cannot be connected at all. Only applied if the etcd instance is the first peer in the cluster.
|
||||
* `-cluster-sync-interval` - The number of seconds between synchronization for standby-mode instance with the cluster. Only applied if the etcd instance is the first peer in the cluster.
|
||||
* `-v` - Enable verbose logging. Defaults to `false`.
|
||||
* `-vv` - Enable very verbose logging. Defaults to `false`.
|
||||
* `-version` - Print the version and exit.
|
||||
@ -56,6 +74,9 @@ cert_file = ""
|
||||
cors = []
|
||||
cpu_profile_file = ""
|
||||
data_dir = "."
|
||||
discovery = "http://etcd.local:4001/v2/keys/_etcd/registry/examplecluster"
|
||||
http_read_timeout = 10.0
|
||||
http_write_timeout = 10.0
|
||||
key_file = ""
|
||||
peers = []
|
||||
peers_file = ""
|
||||
@ -63,7 +84,7 @@ max_cluster_size = 9
|
||||
max_result_buffer = 1024
|
||||
max_retry_attempts = 3
|
||||
name = "default-name"
|
||||
snapshot = false
|
||||
snapshot = true
|
||||
verbose = false
|
||||
very_verbose = false
|
||||
|
||||
@ -73,6 +94,11 @@ bind_addr = "127.0.0.1:7001"
|
||||
ca_file = ""
|
||||
cert_file = ""
|
||||
key_file = ""
|
||||
|
||||
[cluster]
|
||||
active_size = 9
|
||||
remove_delay = 1800.0
|
||||
sync_interval = 5.0
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
@ -85,6 +111,9 @@ key_file = ""
|
||||
* `ETCD_CONFIG`
|
||||
* `ETCD_CPU_PROFILE_FILE`
|
||||
* `ETCD_DATA_DIR`
|
||||
* `ETCD_DISCOVERY`
|
||||
* `ETCD_HTTP_READ_TIMEOUT`
|
||||
* `ETCD_HTTP_WRITE_TIMEOUT`
|
||||
* `ETCD_KEY_FILE`
|
||||
* `ETCD_PEERS`
|
||||
* `ETCD_PEERS_FILE`
|
||||
@ -100,3 +129,7 @@ key_file = ""
|
||||
* `ETCD_PEER_CA_FILE`
|
||||
* `ETCD_PEER_CERT_FILE`
|
||||
* `ETCD_PEER_KEY_FILE`
|
||||
* `ETCD_PEER_ELECTION_TIMEOUT`
|
||||
* `ETCD_CLUSTER_ACTIVE_SIZE`
|
||||
* `ETCD_CLUSTER_REMOVE_DELAY`
|
||||
* `ETCD_CLUSTER_SYNC_INTERVAL`
|
||||
|
@ -44,10 +44,10 @@ See an [example collectd deploy script](https://github.com/coreos/etcd/contrib/c
|
||||
## Profiling
|
||||
|
||||
etcd exposes profiling information from the Go pprof package over HTTP.
|
||||
The basic browseable interface is served by etcd at the `/debug/pprof` HTTP endpoint (i.e. `http://127.0.0.1:4001/debug/pprof`).
|
||||
The basic browsable interface is served by etcd at the `/debug/pprof` HTTP endpoint (i.e. `http://127.0.0.1:4001/debug/pprof`).
|
||||
For more information on using profiling tools, see http://blog.golang.org/profiling-go-programs.
|
||||
|
||||
**NOTE**: In the following examples you need to ensure that the `./bin/etcd` is identical to the `./bin/etcd` that you are targetting (same git hash, arch, platform, etc).
|
||||
**NOTE**: In the following examples you need to ensure that the `./bin/etcd` is identical to the `./bin/etcd` that you are targeting (same git hash, arch, platform, etc).
|
||||
|
||||
#### Heap memory profile
|
||||
|
||||
|
34
Documentation/design/cluster-finding.md
Normal file
34
Documentation/design/cluster-finding.md
Normal file
@ -0,0 +1,34 @@
|
||||
## Cluster Finding Process
|
||||
|
||||
Peer discovery uses the following sources in this order: log data in `-data-dir`, `-discovery` and `-peers`.
|
||||
|
||||
If log data is provided, etcd will concatenate possible peers from three sources: the log data, the `-discovery` option, and `-peers`. Then it tries to join cluster through them one by one. If all connection attempts fail (which indicates that the majority of the cluster is currently down), it will restart itself based on the log data, which helps the cluster to recover from a full outage.
|
||||
|
||||
Without log data, the instance is assumed to be a brand new one. If possible targets are provided by `-discovery` and `-peers`, etcd will make a best effort attempt to join them, and if none is reachable it will exit. Otherwise, if no `-discovery` or `-peers` option is provided, a new cluster will always be started.
|
||||
|
||||
This ensures that users can always restart the node safely with the same command (without --force), and etcd will either reconnect to the old cluster if it is still running or recover its cluster from a outage.
|
||||
|
||||
## Logical Workflow
|
||||
|
||||
Start an etcd machine:
|
||||
|
||||
```
|
||||
If log data is given:
|
||||
Try to join via peers in previous cluster
|
||||
Try to join via peers found in discover URL
|
||||
Try to join via peers in peer list
|
||||
Restart the previous cluster which is down
|
||||
return
|
||||
|
||||
If discover URL is given:
|
||||
Fetch peers through discover URL
|
||||
If Success:
|
||||
Join via peers found
|
||||
return
|
||||
|
||||
If peer list is given:
|
||||
Join as follower via peers in peer list
|
||||
return
|
||||
|
||||
Start as the leader of a new cluster
|
||||
```
|
232
Documentation/design/standbys.md
Normal file
232
Documentation/design/standbys.md
Normal file
@ -0,0 +1,232 @@
|
||||
## Standbys
|
||||
|
||||
Adding peers in an etcd cluster adds network, CPU, and disk overhead to the leader since each one requires replication.
|
||||
Peers primarily provide resiliency in the event of a leader failure but the benefit of more failover nodes decreases as the cluster size increases.
|
||||
A lightweight alternative is the standby.
|
||||
|
||||
Standbys are a way for an etcd node to forward requests along to the cluster but the standbys are not part of the Raft cluster themselves.
|
||||
This provides an easier API for local applications while reducing the overhead required by a regular peer node.
|
||||
Standbys also act as standby nodes in the event that a peer node in the cluster has not recovered after a long duration.
|
||||
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
There are three configuration parameters used by standbys: active size, remove delay and standby sync interval.
|
||||
|
||||
The active size specifies a target size for the number of peers in the cluster.
|
||||
If there are not enough peers to meet the active size, standbys will send join requests until the peer count is equal to the active size.
|
||||
If there are more peers than the target active size then peers are removed by the leader and will become standbys.
|
||||
|
||||
The remove delay specifies how long the cluster should wait before removing a dead peer.
|
||||
By default this is 30 minutes.
|
||||
If a peer is inactive for 30 minutes then the peer is removed.
|
||||
|
||||
The standby sync interval specifies the synchronization interval of standbys with the cluster.
|
||||
By default this is 5 seconds.
|
||||
After each interval, standbys synchronize information with cluster.
|
||||
|
||||
|
||||
## Logical Workflow
|
||||
|
||||
### Start a etcd machine
|
||||
|
||||
#### Main logic
|
||||
|
||||
```
|
||||
If find existing standby cluster info:
|
||||
Goto standby loop
|
||||
|
||||
Find cluster as required
|
||||
If determine to start peer server:
|
||||
Goto peer loop
|
||||
Else:
|
||||
Goto standby loop
|
||||
|
||||
Peer loop:
|
||||
Start peer mode
|
||||
If running:
|
||||
Wait for stop
|
||||
Goto standby loop
|
||||
|
||||
Standby loop:
|
||||
Start standby mode
|
||||
If running:
|
||||
Wait for stop
|
||||
Goto peer loop
|
||||
```
|
||||
|
||||
|
||||
#### [Cluster finding logic][cluster-finding.md]
|
||||
|
||||
|
||||
#### Join request logic:
|
||||
|
||||
```
|
||||
Fetch machine info
|
||||
If cannot match version:
|
||||
return false
|
||||
If active size <= peer count:
|
||||
return false
|
||||
If it has existed in the cluster:
|
||||
return true
|
||||
If join request fails:
|
||||
return false
|
||||
return true
|
||||
```
|
||||
|
||||
**Note**
|
||||
1. [TODO] The running mode cannot be determined by log, because the log may be outdated. But the log could be used to estimate its state.
|
||||
2. Even if sync cluster fails, it will restart still for recovery from full outage.
|
||||
|
||||
|
||||
#### Peer mode start logic
|
||||
|
||||
```
|
||||
Start raft server
|
||||
Start other helper routines
|
||||
```
|
||||
|
||||
|
||||
#### Peer mode auto stop logic
|
||||
|
||||
```
|
||||
When removed from the cluster:
|
||||
Stop raft server
|
||||
Stop other helper routines
|
||||
```
|
||||
|
||||
|
||||
#### Standby mode run logic
|
||||
|
||||
```
|
||||
Loop:
|
||||
Sleep for some time
|
||||
|
||||
Sync cluster, and write cluster info into disk
|
||||
|
||||
Check active size and send join request if needed
|
||||
If succeed:
|
||||
Clear cluster info from disk
|
||||
Return
|
||||
```
|
||||
|
||||
|
||||
#### Serve Requests as Standby
|
||||
|
||||
Return '404 Page Not Found' always on peer address. This is because peer address is used for raft communication and cluster management, which should not be used in standby mode.
|
||||
|
||||
|
||||
Serve requests from client:
|
||||
|
||||
```
|
||||
Redirect all requests to client URL of leader
|
||||
```
|
||||
|
||||
**Note**
|
||||
1. The leader here implies the one in raft cluster when doing the latest successful synchronization.
|
||||
2. [IDEA] We could extend HTTP Redirect to multiple possible targets.
|
||||
|
||||
|
||||
### Join Request Handling
|
||||
|
||||
```
|
||||
If machine has existed in the cluster:
|
||||
Return
|
||||
If peer count < active size:
|
||||
Add peer
|
||||
Increase peer count
|
||||
```
|
||||
|
||||
|
||||
### Remove Request Handling
|
||||
|
||||
```
|
||||
If machine exists in the cluster:
|
||||
Remove peer
|
||||
Decrease peer count
|
||||
```
|
||||
|
||||
|
||||
## Cluster Monitor Logic
|
||||
|
||||
### Active Size Monitor:
|
||||
|
||||
This is only run by current cluster leader.
|
||||
|
||||
```
|
||||
Loop:
|
||||
Sleep for some time
|
||||
|
||||
If peer count > active size:
|
||||
Remove randomly selected peer
|
||||
```
|
||||
|
||||
|
||||
### Peer Activity Monitor
|
||||
|
||||
This is only run by current cluster leader.
|
||||
|
||||
```
|
||||
Loop:
|
||||
Sleep for some time
|
||||
|
||||
For each peer:
|
||||
If peer last activity time > remove delay:
|
||||
Remove the peer
|
||||
Goto Loop
|
||||
```
|
||||
|
||||
|
||||
## Cluster Cases
|
||||
|
||||
### Create Cluster with Thousands of Instances
|
||||
|
||||
First few machines run in peer mode.
|
||||
|
||||
All the others check the status of the cluster and run in standby mode.
|
||||
|
||||
|
||||
### Recover from full outage
|
||||
|
||||
Machines with log data restart with join failure.
|
||||
|
||||
Machines in peer mode recover heartbeat between each other.
|
||||
|
||||
Machines in standby mode always sync the cluster. If sync fails, it uses the first address from data log as redirect target.
|
||||
|
||||
|
||||
### Kill one peer machine
|
||||
|
||||
Leader of the cluster lose the connection with the peer.
|
||||
|
||||
When the time exceeds remove delay, it removes the peer from the cluster.
|
||||
|
||||
Machine in standby mode finds one available place of the cluster. It sends join request and joins the cluster.
|
||||
|
||||
**Note**
|
||||
1. [TODO] Machine which was divided from majority and was removed from the cluster will distribute running of the cluster if the new node uses the same name.
|
||||
|
||||
|
||||
### Kill one standby machine
|
||||
|
||||
No change for the cluster.
|
||||
|
||||
|
||||
## Cons
|
||||
|
||||
1. New instance cannot join immediately after one peer is kicked out of the cluster, because the leader doesn't know the info about the standby instances.
|
||||
|
||||
2. It may introduce join collision
|
||||
|
||||
3. Cluster needs a good interval setting to balance the join delay and join collision.
|
||||
|
||||
|
||||
## Future Attack Plans
|
||||
|
||||
1. Based on heartbeat miss and remove delay, standby could adjust its next check time.
|
||||
|
||||
2. Preregister the promotion target when heartbeat miss happens.
|
||||
|
||||
3. Get the estimated cluster size from the check happened in the sync interval, and adjust sync interval dynamically.
|
||||
|
||||
4. Accept join requests based on active size and alive peers.
|
@ -21,6 +21,7 @@ Error code corresponding strerror
|
||||
EcodeNotDir = 104
|
||||
EcodeNodeExist = 105
|
||||
EcodeKeyIsPreserved = 106
|
||||
EcodeRootROnly = 107
|
||||
|
||||
EcodeValueRequired = 200
|
||||
EcodePrevValueRequired = 201
|
||||
@ -42,6 +43,7 @@ Error code corresponding strerror
|
||||
errors[104] = "Not A Directory"
|
||||
errors[105] = "Already exists" // create
|
||||
errors[106] = "The prefix of given key is a keyword in etcd"
|
||||
errors[107] = "Root is read only"
|
||||
|
||||
// Post form related errors
|
||||
errors[200] = "Value is Required in POST form"
|
||||
|
@ -77,7 +77,7 @@ Unless overridden, a node naturally inherits the ACL names of its parent directo
|
||||
|
||||
For each ACL name, it has three children: *R (Reading)*, *W (Writing)*, *C (Changing)*
|
||||
|
||||
Each permission is also a node. Under the node it contains the users who have this permission for the file refering to this ACL name.
|
||||
Each permission is also a node. Under the node it contains the users who have this permission for the file referring to this ACL name.
|
||||
|
||||
### Example
|
||||
[TODO]
|
||||
|
@ -26,7 +26,7 @@ Advantages
|
||||
|
||||
Disadvantages
|
||||
|
||||
- Follower knows better what versions of the interal protocol it can talk than the leader
|
||||
- Follower knows better what versions of the internal protocol it can talk than the leader
|
||||
|
||||
|
||||
### Follower Controlled
|
||||
|
@ -3,7 +3,10 @@
|
||||
**Tools**
|
||||
|
||||
- [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-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**
|
||||
|
||||
@ -11,14 +14,18 @@
|
||||
|
||||
**Java libraries**
|
||||
|
||||
|
||||
- [boonproject/etcd](https://github.com/boonproject/boon/blob/master/etcd/README.md) - Supports v2, Async/Sync and waits
|
||||
- [justinsb/jetcd](https://github.com/justinsb/jetcd)
|
||||
- [diwakergupta/jetcd](https://github.com/diwakergupta/jetcd) - Supports v2
|
||||
- [jurmous/etcd4j](https://github.com/jurmous/etcd4j) - Supports v2
|
||||
- [AdoHe/etcd4j](http://github.com/AdoHe/etcd4j) - Supports v2 (enhance for real production cluster)
|
||||
|
||||
**Python libraries**
|
||||
|
||||
- [transitorykris/etcd-py](https://github.com/transitorykris/etcd-py)
|
||||
- [jplana/python-etcd](https://github.com/jplana/python-etcd) - Supports v2
|
||||
- [russellhaering/txetcd](https://github.com/russellhaering/txetcd) - a Twisted Python library
|
||||
- [cholcombe973/autodock](https://github.com/cholcombe973/autodock) - A docker deployment automation tool
|
||||
|
||||
**Node libraries**
|
||||
|
||||
@ -35,6 +42,9 @@
|
||||
|
||||
- [jdarcy/etcd-api](https://github.com/jdarcy/etcd-api) - Supports v2
|
||||
|
||||
**C++ libraries**
|
||||
- [edwardcapriolo/etcdcpp](https://github.com/edwardcapriolo/etcdcpp) - Supports v2
|
||||
|
||||
**Clojure libraries**
|
||||
|
||||
- [aterreno/etcd-clojure](https://github.com/aterreno/etcd-clojure)
|
||||
@ -46,8 +56,17 @@
|
||||
- [marshall-lee/etcd.erl](https://github.com/marshall-lee/etcd.erl)
|
||||
|
||||
**.Net Libraries**
|
||||
|
||||
- [drusellers/etcetera](https://github.com/drusellers/etcetera)
|
||||
|
||||
**PHP Libraries**
|
||||
|
||||
- [linkorb/etcd-php](https://github.com/linkorb/etcd-php)
|
||||
|
||||
**Haskell libraries**
|
||||
|
||||
- [wereHamster/etcd-hs](https://github.com/wereHamster/etcd-hs)
|
||||
|
||||
A detailed recap of client functionalities can be found in the [clients compatibility matrix][clients-matrix.md].
|
||||
|
||||
[clients-matrix.md]: https://github.com/coreos/etcd/blob/master/Documentation/clients-matrix.md
|
||||
@ -77,3 +96,8 @@ A detailed recap of client functionalities can be found in the [clients compatib
|
||||
- [mattn/etcdenv](https://github.com/mattn/etcdenv) - "env" shebang with etcd integration
|
||||
- [kelseyhightower/confd](https://github.com/kelseyhightower/confd) - Manage local app config files using templates and data from etcd
|
||||
- [configdb](https://git.autistici.org/ai/configdb/tree/master) - A REST relational abstraction on top of arbitrary database backends, aimed at storing configs and inventories.
|
||||
- [scrz](https://github.com/scrz/scrz) - Container manager, stores configuration in etcd.
|
||||
- [fleet](https://github.com/coreos/fleet) - Distributed init system
|
||||
- [GoogleCloudPlatform/kubernetes](https://github.com/GoogleCloudPlatform/kubernetes) - Container cluster manager.
|
||||
- [mailgun/vulcand](https://github.com/mailgun/vulcand) - HTTP proxy that uses etcd as a configuration backend.
|
||||
- [duedil-ltd/discodns](https://github.com/duedil-ltd/discodns) - Simple DNS nameserver using etcd as a database for names and records.
|
||||
|
@ -1,7 +1,13 @@
|
||||
## Modules
|
||||
|
||||
etcd has a number of modules that are built on top of the core etcd API.
|
||||
These modules provide things like dashboards, locks and leader election.
|
||||
These modules provide things like dashboards, locks and leader election (removed).
|
||||
|
||||
**Warning**: Modules are deprecated from v0.4 until we have a solid base we can apply them back onto.
|
||||
For now, we are choosing to focus on raft algorithm and core etcd to make sure that it works correctly and fast.
|
||||
And it is time consuming to maintain these modules in this period, given that etcd's API changes from time to time.
|
||||
Moreover, the lock module has some unfixed bugs, which may mislead users.
|
||||
But we also notice that these modules are popular and useful, and plan to add them back with full functionality as soon as possible.
|
||||
|
||||
### Dashboard
|
||||
|
||||
@ -15,6 +21,7 @@ Use the `-cors='*'` flag to allow your browser to request information from the c
|
||||
The Lock module implements a fair lock that can be used when lots of clients want access to a single resource.
|
||||
A lock can be associated with a value.
|
||||
The value is unique so if a lock tries to request a value that is already queued for a lock then it will find it and watch until that value obtains the lock.
|
||||
You may supply a `timeout` which will cancel the lock request if it is not obtained within `timeout` seconds. If `timeout` is not supplied, it is presumed to be infinite. If `timeout` is `0`, the lock request will fail if it is not immediately acquired.
|
||||
If you lock the same value on a key from two separate curl sessions they'll both return at the same time.
|
||||
|
||||
Here's the API:
|
||||
@ -31,13 +38,19 @@ curl -X POST http://127.0.0.1:4001/mod/v2/lock/customer1?ttl=60
|
||||
curl -X POST http://127.0.0.1:4001/mod/v2/lock/customer1?ttl=60 -d value=bar
|
||||
```
|
||||
|
||||
**Acquire a lock for "customer1" that is associated with the value "bar" only if it is done within 2 seconds**
|
||||
|
||||
```sh
|
||||
curl -X POST http://127.0.0.1:4001/mod/v2/lock/customer1?ttl=60 -d value=bar -d timeout=2
|
||||
```
|
||||
|
||||
**Renew the TTL on the "customer1" lock for index 2**
|
||||
|
||||
```sh
|
||||
curl -X PUT http://127.0.0.1:4001/mod/v2/lock/customer1?ttl=60 -d index=2
|
||||
```
|
||||
|
||||
**Renew the TTL on the "customer1" lock for value "customer1"**
|
||||
**Renew the TTL on the "customer1" lock for value "bar"**
|
||||
|
||||
```sh
|
||||
curl -X PUT http://127.0.0.1:4001/mod/v2/lock/customer1?ttl=60 -d value=bar
|
||||
@ -68,7 +81,7 @@ curl -X DELETE http://127.0.0.1:4001/mod/v2/lock/customer1?value=bar
|
||||
```
|
||||
|
||||
|
||||
### Leader Election
|
||||
### Leader Election (Deprecated and Removed in 0.4)
|
||||
|
||||
The Leader Election module wraps the Lock module to allow clients to come to consensus on a single value.
|
||||
This is useful when you want one server to process at a time but allow other servers to fail over.
|
||||
|
@ -1,30 +1,38 @@
|
||||
# Optimal etcd Cluster Size
|
||||
|
||||
etcd's Raft consensus algorithm is most efficient in small clusters between 3 and 9 peers. Let's briefly explore how etcd works internally to understand why.
|
||||
|
||||
## Writing to etcd
|
||||
|
||||
Writes to an etcd peer are always redirected to the leader of the cluster and distributed to all of the peers immediately. A write is only considered successful when a majority of the peers acknowledge the write.
|
||||
|
||||
For example, in a 5 node cluster, a write operation is only as fast as the 3rd fastest machine. This is the main reason for keeping your etcd cluster below 9 nodes. In practice, you only need to worry about write performance in high latency environments such as a cluster spanning multiple data centers.
|
||||
|
||||
## Leader Election
|
||||
|
||||
The leader election process is similar to writing a key — a majority of the cluster must acknowledge the new leader before cluster operations can continue. The longer each node takes to elect a new leader means you have to wait longer before you can write to the cluster again. In low latency environments this process takes milliseconds.
|
||||
|
||||
## Odd Cluster Size
|
||||
|
||||
The other important cluster optimization is to always have an odd cluster size. Adding an odd node to the cluster doesn't change the size of the majority and therefore doesn't increase the total latency of the majority as described above. But you do gain a higher tolerance for peer failure by adding the extra machine. You can see this in practice when comparing two even and odd sized clusters:
|
||||
|
||||
| Cluster Size | Majority | Failure Tolerance |
|
||||
|--------------|------------|-------------------|
|
||||
| 8 machines | 5 machines | 3 machines |
|
||||
| 9 machines | 5 machines | **4 machines** |
|
||||
|
||||
As you can see, adding another node to bring the cluster up to an odd size is always worth it. During a network partition, an odd cluster size also guarantees that there will almost always be a majority of the cluster that can continue to operate and be the source of truth when the partition ends.
|
||||
etcd's Raft consensus algorithm is most efficient in small clusters between 3 and 9 peers. For clusters larger than 9, etcd will select a subset of instances to participate in the algorithm in order to keep it efficient. The end of this document briefly explores how etcd works internally and why these choices have been made.
|
||||
|
||||
## Cluster Management
|
||||
|
||||
Currently, each CoreOS machine is an etcd peer — if you have 30 CoreOS machines, you have 30 etcd peers and end up with a cluster size that is way too large. If desired, you may manually stop some of these etcd instances to increase cluster performance.
|
||||
You can manage the active cluster size through the [cluster config API](https://github.com/coreos/etcd/blob/master/Documentation/api.md#cluster-config). `activeSize` represents the etcd peers allowed to actively participate in the consensus algorithm.
|
||||
|
||||
Functionality is being developed to expose two different types of followers: active and benched followers. Active followers will influence operations within the cluster. Benched followers will not participate, but will transparently proxy etcd traffic to an active follower. This allows every CoreOS machine to expose etcd on port 4001 for ease of use. Benched followers will have the ability to transition into an active follower if needed.
|
||||
If the total number of etcd instances exceeds this number, additional peers are started as [standbys](https://github.com/coreos/etcd/blob/master/Documentation/design/standbys.md), which can be promoted to active participation if one of the existing active instances has failed or been removed.
|
||||
|
||||
## Internals of etcd
|
||||
|
||||
### Writing to etcd
|
||||
|
||||
Writes to an etcd peer are always redirected to the leader of the cluster and distributed to all of the peers immediately. A write is only considered successful when a majority of the peers acknowledge the write.
|
||||
|
||||
For example, in a cluster with 5 peers, a write operation is only as fast as the 3rd fastest machine. This is the main reason for keeping the number of active peers below 9. In practice, you only need to worry about write performance in high latency environments such as a cluster spanning multiple data centers.
|
||||
|
||||
### Leader Election
|
||||
|
||||
The leader election process is similar to writing a key — a majority of the active peers must acknowledge the new leader before cluster operations can continue. The longer each peer takes to elect a new leader means you have to wait longer before you can write to the cluster again. In low latency environments this process takes milliseconds.
|
||||
|
||||
### Odd Active Cluster Size
|
||||
|
||||
The other important cluster optimization is to always have an odd active cluster size (i.e. `activeSize`). Adding an odd node to the number of peers doesn't change the size of the majority and therefore doesn't increase the total latency of the majority as described above. But, you gain a higher tolerance for peer failure by adding the extra machine. You can see this in practice when comparing two even and odd sized clusters:
|
||||
|
||||
| Active Peers | Majority | Failure Tolerance |
|
||||
|--------------|------------|-------------------|
|
||||
| 1 peers | 1 peers | None |
|
||||
| 3 peers | 2 peers | 1 peer |
|
||||
| 4 peers | 3 peers | 1 peer |
|
||||
| 5 peers | 3 peers | **2 peers** |
|
||||
| 6 peers | 4 peers | 2 peers |
|
||||
| 7 peers | 4 peers | **3 peers** |
|
||||
| 8 peers | 5 peers | 3 peers |
|
||||
| 9 peers | 5 peers | **4 peers** |
|
||||
|
||||
As you can see, adding another peer to bring the number of active peers up to an odd size is always worth it. During a network partition, an odd number of active peers also guarantees that there will almost always be a majority of the cluster that can continue to operate and be the source of truth when the partition ends.
|
||||
|
@ -26,7 +26,7 @@ coreosetcdctl0.2.0 Simple commandline client for et
|
||||
r@fbsd10:/ #
|
||||
```
|
||||
|
||||
5. You’re ready to use etcd and etcdctl! For more information about using pkgng, plese
|
||||
5. You’re ready to use etcd and etcdctl! For more information about using pkgng, please
|
||||
see: http://www.freebsd.org/doc/handbook/pkgngintro.html
|
||||
|
||||
### Using ports system
|
||||
@ -37,7 +37,7 @@ may take some time depending on your hardware and network connection)
|
||||
2. Build etcd with `cd /usr/ports/devel/etcd && make install clean`, you
|
||||
will get an option to build and install documentation and etcdctl with it.
|
||||
|
||||
3. If you havent install it with etcdctl, and you would like to install it later, you can build it
|
||||
3. If you haven't installed it with etcdctl, and you would like to install it later, you can build it
|
||||
with `cd /usr/ports/devel/etcdctl && make install clean`
|
||||
|
||||
4. Verify successful installation with `pkg info | grep etcd` and you should get:
|
||||
@ -55,8 +55,8 @@ please see: https://www.freebsd.org/doc/handbook/portsusing.html
|
||||
|
||||
## Issues
|
||||
|
||||
If you find any issues with the build/install procedure or you’ve found a problem that
|
||||
you’ve verified is local to FreeBSD version only (for example, by not being able to
|
||||
If you find any issues with the build/install procedure or you've found a problem that
|
||||
you've verified is local to FreeBSD version only (for example, by not being able to
|
||||
reproduce it on any other platform, like OSX or Linux), please sent a
|
||||
problem report using this page for more
|
||||
information: http://www.freebsd.org/sendpr.html
|
||||
|
7
Documentation/production-ready.md
Normal file
7
Documentation/production-ready.md
Normal file
@ -0,0 +1,7 @@
|
||||
etcd is being used successfully by many companies in production. It is,
|
||||
however, under active development and systems like etcd are difficult to get
|
||||
correct. If you are comfortable with bleeding-edge software please use etcd and
|
||||
provide us with the feedback and testing young software needs.
|
||||
|
||||
When the etcd team feels confident removing this warning we will release etcd
|
||||
1.0.
|
@ -1,33 +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/
|
||||
|
||||
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
|
||||
./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:
|
||||
|
||||
* `-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:
|
||||
This should start up fine and you can now test the configuration by speaking HTTPS to etcd:
|
||||
|
||||
```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.
|
||||
Instead you must import the dummy ca.crt directly into the keychain or add the `-k` flag to curl to ignore errors.
|
||||
@ -35,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!
|
||||
If you know of a workaround let us know.
|
||||
|
||||
```
|
||||
...
|
||||
SSLv3, TLS handshake, Finished (20):
|
||||
...
|
||||
```
|
||||
## Example 2: Client-to-server authentication with HTTPS client certificates
|
||||
|
||||
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
|
||||
{
|
||||
"action": "set",
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 3,
|
||||
"value": "bar"
|
||||
}
|
||||
```
|
||||
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.
|
||||
|
||||
|
||||
## 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.
|
||||
You need the same files mentioned in the first example for this, as well as a key pair for the client (`client.crt`, `client.key`) signed by the same certificate authority.
|
||||
|
||||
```sh
|
||||
./etcd -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
|
||||
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:
|
||||
|
||||
```
|
||||
...
|
||||
@ -78,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
|
||||
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:
|
||||
@ -107,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 node1 -data-dir node2 -ca-file=/path/to/ca.crt -cert-file=/path/to/node2.crt -key-file=/path/to/node2.key -peer-addr ${node2_public_ip}:7001 -discovery ${DISCOVERY_URL}
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
@ -128,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
|
||||
```
|
||||
|
||||
### 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).
|
||||
|
@ -1,47 +1,49 @@
|
||||
## Tuning
|
||||
|
||||
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 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.
|
||||
|
||||
### Timeouts
|
||||
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.
|
||||
|
||||
The underlying distributed consensus protocol relies on two separate timeouts to ensure that nodes can handoff leadership if one stalls or goes offline.
|
||||
The first timeout is called the *Heartbeat Timeout*.
|
||||
### Time Parameters
|
||||
|
||||
The underlying distributed consensus protocol relies on two separate time parameters to ensure that nodes can handoff leadership if one stalls or goes offline.
|
||||
The first parameter is called the *Heartbeat Interval*.
|
||||
This is the frequency with which the leader will notify followers that it is still the leader.
|
||||
etcd batches commands together for higher throughput so this heartbeat timeout is also a delay for how long it takes for commands to be committed.
|
||||
By default, etcd uses a `50ms` heartbeat timeout.
|
||||
etcd batches commands together for higher throughput so this heartbeat interval is also a delay for how long it takes for commands to be committed.
|
||||
By default, etcd uses a `50ms` heartbeat interval.
|
||||
|
||||
The second timeout is the *Election Timeout*.
|
||||
The second parameter is the *Election Timeout*.
|
||||
This timeout is how long a follower node will go without hearing a heartbeat before attempting to become leader itself.
|
||||
By default, etcd uses a `200ms` election timeout.
|
||||
|
||||
Adjusting these values is a trade off.
|
||||
Lowering the heartbeat timeout will cause individual commands to be committed faster but it will lower the overall throughput of etcd.
|
||||
If your etcd instances have low utilization then lowering the heartbeat timeout can improve your command response time.
|
||||
Lowering the heartbeat interval will cause individual commands to be committed faster but it will lower the overall throughput of etcd.
|
||||
If your etcd instances have low utilization then lowering the heartbeat interval can improve your command response time.
|
||||
|
||||
The election timeout should be set based on the heartbeat timeout and your network ping time between nodes.
|
||||
The election timeout should be set based on the heartbeat interval and your network ping time between nodes.
|
||||
Election timeouts should be at least 10 times your ping time so it can account for variance in your network.
|
||||
For example, if the ping time between your nodes is 10ms then you should have at least a 100ms election timeout.
|
||||
|
||||
You should also set your election timeout to at least 4 to 5 times your heartbeat timeout to account for variance in leader replication.
|
||||
For a heartbeat timeout of 50ms you should set your election timeout to at least 200ms - 250ms.
|
||||
You should also set your election timeout to at least 4 to 5 times your heartbeat interval to account for variance in leader replication.
|
||||
For a heartbeat interval of 50ms you should set your election timeout to at least 200ms - 250ms.
|
||||
|
||||
You can override the default values on the command line:
|
||||
|
||||
```sh
|
||||
# Command line arguments:
|
||||
$ etcd -peer-heartbeat-timeout=100 -peer-election-timeout=500
|
||||
$ etcd -peer-heartbeat-interval=100 -peer-election-timeout=500
|
||||
|
||||
# Environment variables:
|
||||
$ ETCD_PEER_HEARTBEAT_TIMEOUT=100 ETCD_PEER_ELECTION_TIMEOUT=500 etcd
|
||||
$ ETCD_PEER_HEARTBEAT_INTERVAL=100 ETCD_PEER_ELECTION_TIMEOUT=500 etcd
|
||||
```
|
||||
|
||||
Or you can set the values within the configuration file:
|
||||
|
||||
```toml
|
||||
[peer]
|
||||
heartbeat_timeout = 100
|
||||
election_timeout = 100
|
||||
heartbeat_interval = 100
|
||||
election_timeout = 500
|
||||
```
|
||||
|
||||
The values are specified in milliseconds.
|
||||
@ -86,7 +88,7 @@ $ etcd -snapshot false
|
||||
$ ETCD_SNAPSHOT=false etcd
|
||||
```
|
||||
|
||||
You can also enable snapshotting within the configuration file:
|
||||
You can also disable snapshotting within the configuration file:
|
||||
|
||||
```toml
|
||||
snapshot = false
|
||||
|
17
Documentation/upgrade.md
Normal file
17
Documentation/upgrade.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Upgrading an Existing Cluster
|
||||
|
||||
etcd clusters can be upgraded by doing a rolling upgrade or all at once. We make every effort to test this process, but please be sure to backup your data [by etcd-dump](https://github.com/AaronO/etcd-dump), or make a copy of data directory beforehand.
|
||||
|
||||
## Upgrade Process
|
||||
|
||||
- Stop the old etcd processes
|
||||
- Upgrade the etcd binary
|
||||
- Restart the etcd instance using the original --name, --address, --peer-address and --data-dir.
|
||||
|
||||
## Rolling Upgrade
|
||||
|
||||
During an upgrade, etcd clusters are designed to continue working in a mix of old and new versions. It's recommended to converge on the new version quickly. Using new API features before the entire cluster has been upgraded is only supported as a best effort. Each instance's version can be found with `curl http://127.0.0.1:4001/version`.
|
||||
|
||||
## All at Once
|
||||
|
||||
If downtime is not an issue, the easiest way to upgrade your cluster is to shutdown all of the etcd instances and restart them with the new binary. The current state of the cluster is saved to disk and will be loaded into the cluster when it restarts.
|
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.0.0-72-gbb91895",
|
||||
"Rev": "bb9189510af1f49580c073c9e59e8bf288f0df27"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
|
||||
. "./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
|
@ -83,9 +83,14 @@ func mergeStruct(out, in reflect.Value) {
|
||||
mergeAny(out.Field(i), in.Field(i))
|
||||
}
|
||||
|
||||
if emIn, ok := in.Addr().Interface().(extendableProto); ok {
|
||||
emOut := out.Addr().Interface().(extendableProto)
|
||||
if emIn, ok := in.Addr().Interface().(extensionsMap); ok {
|
||||
emOut := out.Addr().Interface().(extensionsMap)
|
||||
mergeExtension(emOut.ExtensionMap(), emIn.ExtensionMap())
|
||||
} else if emIn, ok := in.Addr().Interface().(extensionsBytes); ok {
|
||||
emOut := out.Addr().Interface().(extensionsBytes)
|
||||
bIn := emIn.GetExtensions()
|
||||
bOut := emOut.GetExtensions()
|
||||
*bOut = append(*bOut, *bIn...)
|
||||
}
|
||||
|
||||
uf := in.FieldByName("XXX_unrecognized")
|
@ -34,7 +34,7 @@ package proto_test
|
||||
import (
|
||||
"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"
|
||||
)
|
@ -235,12 +235,6 @@ func (o *Buffer) skipAndSave(t reflect.Type, tag, wire int, base structPointer,
|
||||
|
||||
ptr := structPointer_Bytes(base, unrecField)
|
||||
|
||||
if *ptr == nil {
|
||||
// This is the first skipped element,
|
||||
// allocate a new buffer.
|
||||
*ptr = o.bufalloc()
|
||||
}
|
||||
|
||||
// Add the skipped field to struct field
|
||||
obuf := o.buf
|
||||
|
||||
@ -381,9 +375,14 @@ func (o *Buffer) unmarshalType(st reflect.Type, prop *StructProperties, is_group
|
||||
if prop.extendable {
|
||||
if e := structPointer_Interface(base, st).(extendableProto); isExtensionField(e, int32(tag)) {
|
||||
if err = o.skip(st, tag, wire); err == nil {
|
||||
ext := e.ExtensionMap()[int32(tag)] // may be missing
|
||||
if ee, ok := e.(extensionsMap); ok {
|
||||
ext := ee.ExtensionMap()[int32(tag)] // may be missing
|
||||
ext.enc = append(ext.enc, o.buf[oi:o.index]...)
|
||||
e.ExtensionMap()[int32(tag)] = ext
|
||||
ee.ExtensionMap()[int32(tag)] = ext
|
||||
} else if ee, ok := e.(extensionsBytes); ok {
|
||||
ext := ee.GetExtensions()
|
||||
*ext = append(*ext, o.buf[oi:o.index]...)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
@ -221,6 +221,10 @@ func Marshal(pb Message) ([]byte, error) {
|
||||
if err != nil && !state.shouldContinue(err, nil) {
|
||||
return nil, err
|
||||
}
|
||||
if p.buf == nil && err == nil {
|
||||
// Return a non-nil slice on success.
|
||||
return []byte{}, nil
|
||||
}
|
||||
return p.buf, err
|
||||
}
|
||||
|
||||
@ -400,23 +404,8 @@ func (o *Buffer) enc_struct_message(p *Properties, base structPointer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// need the length before we can write out the message itself,
|
||||
// so marshal into a separate byte buffer first.
|
||||
obuf := o.buf
|
||||
o.buf = o.bufalloc()
|
||||
|
||||
err := o.enc_struct(p.stype, p.sprop, structp)
|
||||
|
||||
nbuf := o.buf
|
||||
o.buf = obuf
|
||||
if err != nil && !state.shouldContinue(err, nil) {
|
||||
o.buffree(nbuf)
|
||||
return err
|
||||
}
|
||||
o.buf = append(o.buf, p.tagcode...)
|
||||
o.EncodeRawBytes(nbuf)
|
||||
o.buffree(nbuf)
|
||||
return state.err
|
||||
return o.enc_len_struct(p.stype, p.sprop, structp, &state)
|
||||
}
|
||||
|
||||
func size_struct_message(p *Properties, base structPointer) int {
|
||||
@ -748,24 +737,14 @@ func (o *Buffer) enc_slice_struct_message(p *Properties, base structPointer) err
|
||||
continue
|
||||
}
|
||||
|
||||
obuf := o.buf
|
||||
o.buf = o.bufalloc()
|
||||
|
||||
err := o.enc_struct(p.stype, p.sprop, structp)
|
||||
|
||||
nbuf := o.buf
|
||||
o.buf = obuf
|
||||
o.buf = append(o.buf, p.tagcode...)
|
||||
err := o.enc_len_struct(p.stype, p.sprop, structp, &state)
|
||||
if err != nil && !state.shouldContinue(err, nil) {
|
||||
o.buffree(nbuf)
|
||||
if err == ErrNil {
|
||||
return ErrRepeatedHasNil
|
||||
}
|
||||
return err
|
||||
}
|
||||
o.buf = append(o.buf, p.tagcode...)
|
||||
o.EncodeRawBytes(nbuf)
|
||||
|
||||
o.buffree(nbuf)
|
||||
}
|
||||
return state.err
|
||||
}
|
||||
@ -923,6 +902,36 @@ func size_struct(t reflect.Type, prop *StructProperties, base structPointer) (n
|
||||
return
|
||||
}
|
||||
|
||||
var zeroes [20]byte // longer than any conceivable sizeVarint
|
||||
|
||||
// Encode a struct, preceded by its encoded length (as a varint).
|
||||
func (o *Buffer) enc_len_struct(t reflect.Type, prop *StructProperties, base structPointer, state *errorState) error {
|
||||
iLen := len(o.buf)
|
||||
o.buf = append(o.buf, 0, 0, 0, 0) // reserve four bytes for length
|
||||
iMsg := len(o.buf)
|
||||
err := o.enc_struct(t, prop, base)
|
||||
if err != nil && !state.shouldContinue(err, nil) {
|
||||
return err
|
||||
}
|
||||
lMsg := len(o.buf) - iMsg
|
||||
lLen := sizeVarint(uint64(lMsg))
|
||||
switch x := lLen - (iMsg - iLen); {
|
||||
case x > 0: // actual length is x bytes larger than the space we reserved
|
||||
// Move msg x bytes right.
|
||||
o.buf = append(o.buf, zeroes[:x]...)
|
||||
copy(o.buf[iMsg+x:], o.buf[iMsg:iMsg+lMsg])
|
||||
case x < 0: // actual length is x bytes smaller than the space we reserved
|
||||
// Move msg x bytes left.
|
||||
copy(o.buf[iMsg+x:], o.buf[iMsg:iMsg+lMsg])
|
||||
o.buf = o.buf[:len(o.buf)+x] // x is negative
|
||||
}
|
||||
// Encode the length in the reserved space.
|
||||
o.buf = o.buf[:iLen]
|
||||
o.EncodeVarint(uint64(lMsg))
|
||||
o.buf = o.buf[:len(o.buf)+lMsg]
|
||||
return state.err
|
||||
}
|
||||
|
||||
// errorState maintains the first error that occurs and updates that error
|
||||
// with additional context.
|
||||
type errorState struct {
|
@ -44,6 +44,24 @@ type Sizer interface {
|
||||
Size() int
|
||||
}
|
||||
|
||||
func (o *Buffer) enc_ext_slice_byte(p *Properties, base structPointer) error {
|
||||
s := *structPointer_Bytes(base, p.field)
|
||||
if s == nil {
|
||||
return ErrNil
|
||||
}
|
||||
o.buf = append(o.buf, s...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func size_ext_slice_byte(p *Properties, base structPointer) (n int) {
|
||||
s := *structPointer_Bytes(base, p.field)
|
||||
if s == nil {
|
||||
return 0
|
||||
}
|
||||
n += len(s)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode a reference to bool pointer.
|
||||
func (o *Buffer) enc_ref_bool(p *Properties, base structPointer) error {
|
||||
v := structPointer_RefBool(base, p.field)
|
||||
@ -156,23 +174,8 @@ func (o *Buffer) enc_ref_struct_message(p *Properties, base structPointer) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// need the length before we can write out the message itself,
|
||||
// so marshal into a separate byte buffer first.
|
||||
obuf := o.buf
|
||||
o.buf = o.bufalloc()
|
||||
|
||||
err := o.enc_struct(p.stype, p.sprop, structp)
|
||||
|
||||
nbuf := o.buf
|
||||
o.buf = obuf
|
||||
if err != nil && !state.shouldContinue(err, nil) {
|
||||
o.buffree(nbuf)
|
||||
return err
|
||||
}
|
||||
o.buf = append(o.buf, p.tagcode...)
|
||||
o.EncodeRawBytes(nbuf)
|
||||
o.buffree(nbuf)
|
||||
return nil
|
||||
return o.enc_len_struct(p.stype, p.sprop, structp, &state)
|
||||
}
|
||||
|
||||
//TODO this is only copied, please fix this
|
||||
@ -222,26 +225,17 @@ func (o *Buffer) enc_slice_ref_struct_message(p *Properties, base structPointer)
|
||||
continue
|
||||
}
|
||||
|
||||
obuf := o.buf
|
||||
o.buf = o.bufalloc()
|
||||
|
||||
err := o.enc_struct(p.stype, p.sprop, structp)
|
||||
|
||||
nbuf := o.buf
|
||||
o.buf = obuf
|
||||
o.buf = append(o.buf, p.tagcode...)
|
||||
err := o.enc_len_struct(p.stype, p.sprop, structp, &state)
|
||||
if err != nil && !state.shouldContinue(err, nil) {
|
||||
o.buffree(nbuf)
|
||||
if err == ErrNil {
|
||||
return ErrRepeatedHasNil
|
||||
}
|
||||
return err
|
||||
}
|
||||
o.buf = append(o.buf, p.tagcode...)
|
||||
o.EncodeRawBytes(nbuf)
|
||||
|
||||
o.buffree(nbuf)
|
||||
}
|
||||
return nil
|
||||
return state.err
|
||||
}
|
||||
|
||||
//TODO this is only copied, please fix this
|
@ -35,7 +35,7 @@ import (
|
||||
"testing"
|
||||
|
||||
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.
|
@ -55,9 +55,18 @@ type ExtensionRange struct {
|
||||
type extendableProto interface {
|
||||
Message
|
||||
ExtensionRangeArray() []ExtensionRange
|
||||
}
|
||||
|
||||
type extensionsMap interface {
|
||||
extendableProto
|
||||
ExtensionMap() map[int32]Extension
|
||||
}
|
||||
|
||||
type extensionsBytes interface {
|
||||
extendableProto
|
||||
GetExtensions() *[]byte
|
||||
}
|
||||
|
||||
var extendableProtoType = reflect.TypeOf((*extendableProto)(nil)).Elem()
|
||||
|
||||
// ExtensionDesc represents an extension specification.
|
||||
@ -92,7 +101,15 @@ type Extension struct {
|
||||
|
||||
// SetRawExtension is for testing only.
|
||||
func SetRawExtension(base extendableProto, id int32, b []byte) {
|
||||
base.ExtensionMap()[id] = Extension{enc: b}
|
||||
if ebase, ok := base.(extensionsMap); ok {
|
||||
ebase.ExtensionMap()[id] = Extension{enc: b}
|
||||
} else if ebase, ok := base.(extensionsBytes); ok {
|
||||
clearExtension(base, id)
|
||||
ext := ebase.GetExtensions()
|
||||
*ext = append(*ext, b...)
|
||||
} else {
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
// isExtensionField returns true iff the given field number is in an extension range.
|
||||
@ -210,26 +227,80 @@ func sizeExtensionMap(m map[int32]Extension) (n int) {
|
||||
// HasExtension returns whether the given extension is present in pb.
|
||||
func HasExtension(pb extendableProto, extension *ExtensionDesc) bool {
|
||||
// TODO: Check types, field numbers, etc.?
|
||||
_, ok := pb.ExtensionMap()[extension.Field]
|
||||
if epb, doki := pb.(extensionsMap); doki {
|
||||
_, ok := epb.ExtensionMap()[extension.Field]
|
||||
return ok
|
||||
} else if epb, doki := pb.(extensionsBytes); doki {
|
||||
ext := epb.GetExtensions()
|
||||
buf := *ext
|
||||
o := 0
|
||||
for o < len(buf) {
|
||||
tag, n := DecodeVarint(buf[o:])
|
||||
fieldNum := int32(tag >> 3)
|
||||
if int32(fieldNum) == extension.Field {
|
||||
return true
|
||||
}
|
||||
wireType := int(tag & 0x7)
|
||||
o += n
|
||||
l, err := size(buf[o:], wireType)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
o += l
|
||||
}
|
||||
return false
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func deleteExtension(pb extensionsBytes, theFieldNum int32, offset int) int {
|
||||
ext := pb.GetExtensions()
|
||||
for offset < len(*ext) {
|
||||
tag, n1 := DecodeVarint((*ext)[offset:])
|
||||
fieldNum := int32(tag >> 3)
|
||||
wireType := int(tag & 0x7)
|
||||
n2, err := size((*ext)[offset+n1:], wireType)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
newOffset := offset + n1 + n2
|
||||
if fieldNum == theFieldNum {
|
||||
*ext = append((*ext)[:offset], (*ext)[newOffset:]...)
|
||||
return offset
|
||||
}
|
||||
offset = newOffset
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func clearExtension(pb extendableProto, fieldNum int32) {
|
||||
if epb, doki := pb.(extensionsMap); doki {
|
||||
delete(epb.ExtensionMap(), fieldNum)
|
||||
} else if epb, doki := pb.(extensionsBytes); doki {
|
||||
offset := 0
|
||||
for offset != -1 {
|
||||
offset = deleteExtension(epb, fieldNum, offset)
|
||||
}
|
||||
} else {
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
// ClearExtension removes the given extension from pb.
|
||||
func ClearExtension(pb extendableProto, extension *ExtensionDesc) {
|
||||
// TODO: Check types, field numbers, etc.?
|
||||
delete(pb.ExtensionMap(), extension.Field)
|
||||
clearExtension(pb, extension.Field)
|
||||
}
|
||||
|
||||
// GetExtension parses and returns the given extension of pb.
|
||||
// If the extension is not present it returns ErrMissingExtension.
|
||||
// If the returned extension is modified, SetExtension must be called
|
||||
// for the modifications to be reflected in pb.
|
||||
func GetExtension(pb extendableProto, extension *ExtensionDesc) (interface{}, error) {
|
||||
if err := checkExtensionTypes(pb, extension); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e, ok := pb.ExtensionMap()[extension.Field]
|
||||
if epb, doki := pb.(extensionsMap); doki {
|
||||
e, ok := epb.ExtensionMap()[extension.Field]
|
||||
if !ok {
|
||||
return nil, ErrMissingExtension
|
||||
}
|
||||
@ -255,6 +326,28 @@ func GetExtension(pb extendableProto, extension *ExtensionDesc) (interface{}, er
|
||||
e.desc = extension
|
||||
e.enc = nil
|
||||
return e.value, nil
|
||||
} else if epb, doki := pb.(extensionsBytes); doki {
|
||||
ext := epb.GetExtensions()
|
||||
o := 0
|
||||
for o < len(*ext) {
|
||||
tag, n := DecodeVarint((*ext)[o:])
|
||||
fieldNum := int32(tag >> 3)
|
||||
wireType := int(tag & 0x7)
|
||||
l, err := size((*ext)[o+n:], wireType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if int32(fieldNum) == extension.Field {
|
||||
v, err := decodeExtension((*ext)[o:o+n+l], extension)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
o += n + l
|
||||
}
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// decodeExtension decodes an extension encoded in b.
|
||||
@ -319,7 +412,21 @@ func SetExtension(pb extendableProto, extension *ExtensionDesc, value interface{
|
||||
return errors.New("proto: bad extension value type")
|
||||
}
|
||||
|
||||
pb.ExtensionMap()[extension.Field] = Extension{desc: extension, value: value}
|
||||
if epb, doki := pb.(extensionsMap); doki {
|
||||
epb.ExtensionMap()[extension.Field] = Extension{desc: extension, value: value}
|
||||
} else if epb, doki := pb.(extensionsBytes); doki {
|
||||
ClearExtension(pb, extension)
|
||||
ext := epb.GetExtensions()
|
||||
et := reflect.TypeOf(extension.ExtensionType)
|
||||
props := extensionProperties(extension)
|
||||
p := NewBuffer(nil)
|
||||
x := reflect.New(et)
|
||||
x.Elem().Set(reflect.ValueOf(value))
|
||||
if err := props.enc(p, props, toStructPointer(x)); err != nil {
|
||||
return err
|
||||
}
|
||||
*ext = append(*ext, p.buf...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetBoolExtension(pb extendableProto, extension *ExtensionDesc, ifnotset bool) bool {
|
||||
@ -58,6 +59,48 @@ func SizeOfExtensionMap(m map[int32]Extension) (n int) {
|
||||
return sizeExtensionMap(m)
|
||||
}
|
||||
|
||||
type sortableMapElem struct {
|
||||
field int32
|
||||
ext Extension
|
||||
}
|
||||
|
||||
func newSortableExtensionsFromMap(m map[int32]Extension) sortableExtensions {
|
||||
s := make(sortableExtensions, 0, len(m))
|
||||
for k, v := range m {
|
||||
s = append(s, &sortableMapElem{field: k, ext: v})
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type sortableExtensions []*sortableMapElem
|
||||
|
||||
func (this sortableExtensions) Len() int { return len(this) }
|
||||
|
||||
func (this sortableExtensions) Swap(i, j int) { this[i], this[j] = this[j], this[i] }
|
||||
|
||||
func (this sortableExtensions) Less(i, j int) bool { return this[i].field < this[j].field }
|
||||
|
||||
func (this sortableExtensions) String() string {
|
||||
sort.Sort(this)
|
||||
ss := make([]string, len(this))
|
||||
for i := range this {
|
||||
ss[i] = fmt.Sprintf("%d: %v", this[i].field, this[i].ext)
|
||||
}
|
||||
return "map[" + strings.Join(ss, ",") + "]"
|
||||
}
|
||||
|
||||
func StringFromExtensionsMap(m map[int32]Extension) string {
|
||||
return newSortableExtensionsFromMap(m).String()
|
||||
}
|
||||
|
||||
func StringFromExtensionsBytes(ext []byte) string {
|
||||
m, err := BytesToExtensionsMap(ext)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return StringFromExtensionsMap(m)
|
||||
}
|
||||
|
||||
func EncodeExtensionMap(m map[int32]Extension, data []byte) (n int, err error) {
|
||||
if err := encodeExtensionMap(m); err != nil {
|
||||
return 0, err
|
||||
@ -83,6 +126,58 @@ func GetRawExtension(m map[int32]Extension, id int32) ([]byte, error) {
|
||||
return m[id].enc, nil
|
||||
}
|
||||
|
||||
func size(buf []byte, wire int) (int, error) {
|
||||
switch wire {
|
||||
case WireVarint:
|
||||
_, n := DecodeVarint(buf)
|
||||
return n, nil
|
||||
case WireFixed64:
|
||||
return 8, nil
|
||||
case WireBytes:
|
||||
v, n := DecodeVarint(buf)
|
||||
return int(v) + n, nil
|
||||
case WireFixed32:
|
||||
return 4, nil
|
||||
case WireStartGroup:
|
||||
offset := 0
|
||||
for {
|
||||
u, n := DecodeVarint(buf[offset:])
|
||||
fwire := int(u & 0x7)
|
||||
offset += n
|
||||
if fwire == WireEndGroup {
|
||||
return offset, nil
|
||||
}
|
||||
s, err := size(buf[offset:], wire)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
offset += s
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("proto: can't get size for unknown wire type %d", wire)
|
||||
}
|
||||
|
||||
func BytesToExtensionsMap(buf []byte) (map[int32]Extension, error) {
|
||||
m := make(map[int32]Extension)
|
||||
i := 0
|
||||
for i < len(buf) {
|
||||
tag, n := DecodeVarint(buf[i:])
|
||||
if n <= 0 {
|
||||
return nil, fmt.Errorf("unable to decode varint")
|
||||
}
|
||||
fieldNum := int32(tag >> 3)
|
||||
wireType := int(tag & 0x7)
|
||||
l, err := size(buf[i+n:], wireType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
end := i + int(l) + n
|
||||
m[int32(fieldNum)] = Extension{enc: buf[i:end]}
|
||||
i = end
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func NewExtension(e []byte) Extension {
|
||||
ee := Extension{enc: make([]byte, len(e))}
|
||||
copy(ee.enc, e)
|
@ -242,8 +242,6 @@ func GetStats() Stats { return stats }
|
||||
type Buffer struct {
|
||||
buf []byte // encode/decode byte stream
|
||||
index int // write point
|
||||
freelist [10][]byte // list of available buffers
|
||||
nfreelist int // number of free buffers
|
||||
|
||||
// pools of basic types to amortize allocation.
|
||||
bools []bool
|
||||
@ -260,20 +258,11 @@ type Buffer struct {
|
||||
// NewBuffer allocates a new Buffer and initializes its internal data to
|
||||
// the contents of the argument slice.
|
||||
func NewBuffer(e []byte) *Buffer {
|
||||
p := new(Buffer)
|
||||
if e == nil {
|
||||
e = p.bufalloc()
|
||||
}
|
||||
p.buf = e
|
||||
p.index = 0
|
||||
return p
|
||||
return &Buffer{buf: e}
|
||||
}
|
||||
|
||||
// Reset resets the Buffer, ready for marshaling a new protocol buffer.
|
||||
func (p *Buffer) Reset() {
|
||||
if p.buf == nil {
|
||||
p.buf = p.bufalloc()
|
||||
}
|
||||
p.buf = p.buf[0:0] // for reading/writing
|
||||
p.index = 0 // for reading
|
||||
}
|
||||
@ -288,44 +277,6 @@ func (p *Buffer) SetBuf(s []byte) {
|
||||
// Bytes returns the contents of the Buffer.
|
||||
func (p *Buffer) Bytes() []byte { return p.buf }
|
||||
|
||||
// Allocate a buffer for the Buffer.
|
||||
func (p *Buffer) bufalloc() []byte {
|
||||
if p.nfreelist > 0 {
|
||||
// reuse an old one
|
||||
p.nfreelist--
|
||||
s := p.freelist[p.nfreelist]
|
||||
return s[0:0]
|
||||
}
|
||||
// make a new one
|
||||
s := make([]byte, 0, 16)
|
||||
return s
|
||||
}
|
||||
|
||||
// Free (and remember in freelist) a byte buffer for the Buffer.
|
||||
func (p *Buffer) buffree(s []byte) {
|
||||
if p.nfreelist < len(p.freelist) {
|
||||
// Take next slot.
|
||||
p.freelist[p.nfreelist] = s
|
||||
p.nfreelist++
|
||||
return
|
||||
}
|
||||
|
||||
// Find the smallest.
|
||||
besti := -1
|
||||
bestl := len(s)
|
||||
for i, b := range p.freelist {
|
||||
if len(b) < bestl {
|
||||
besti = i
|
||||
bestl = len(b)
|
||||
}
|
||||
}
|
||||
|
||||
// Overwrite the smallest.
|
||||
if besti >= 0 {
|
||||
p.freelist[besti] = s
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper routines for simplifying the creation of optional fields of basic type.
|
||||
*/
|
@ -51,10 +51,17 @@ func structPointer_InterfaceRef(p structPointer, f field, t reflect.Type) interf
|
||||
}
|
||||
|
||||
func copyUintPtr(oldptr, newptr uintptr, size int) {
|
||||
for j := 0; j < size; j++ {
|
||||
oldb := (*byte)(unsafe.Pointer(oldptr + uintptr(j)))
|
||||
*(*byte)(unsafe.Pointer(newptr + uintptr(j))) = *oldb
|
||||
}
|
||||
oldbytes := make([]byte, 0)
|
||||
oldslice := (*reflect.SliceHeader)(unsafe.Pointer(&oldbytes))
|
||||
oldslice.Data = oldptr
|
||||
oldslice.Len = size
|
||||
oldslice.Cap = size
|
||||
newbytes := make([]byte, 0)
|
||||
newslice := (*reflect.SliceHeader)(unsafe.Pointer(&newbytes))
|
||||
newslice.Data = newptr
|
||||
newslice.Len = size
|
||||
newslice.Cap = size
|
||||
copy(newbytes, oldbytes)
|
||||
}
|
||||
|
||||
func structPointer_Copy(oldptr structPointer, newptr structPointer, size int) {
|
@ -575,10 +575,16 @@ func getPropertiesLocked(t reflect.Type) *StructProperties {
|
||||
p.init(f.Type, name, f.Tag.Get("protobuf"), &f, false)
|
||||
|
||||
if f.Name == "XXX_extensions" { // special case
|
||||
if len(f.Tag.Get("protobuf")) > 0 {
|
||||
p.enc = (*Buffer).enc_ext_slice_byte
|
||||
p.dec = nil // not needed
|
||||
p.size = size_ext_slice_byte
|
||||
} else {
|
||||
p.enc = (*Buffer).enc_map
|
||||
p.dec = nil // not needed
|
||||
p.size = size_map
|
||||
}
|
||||
}
|
||||
if f.Name == "XXX_unrecognized" { // special case
|
||||
prop.unrecField = toField(&f)
|
||||
}
|
@ -36,7 +36,7 @@ import (
|
||||
"testing"
|
||||
|
||||
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)}
|
2326
Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/test.pb.go
generated
vendored
Normal file
2326
Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto/testdata/test.pb.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -79,6 +79,13 @@ type textWriter struct {
|
||||
w writer
|
||||
}
|
||||
|
||||
// textMarshaler is implemented by Messages that can marshal themsleves.
|
||||
// It is identical to encoding.TextMarshaler, introduced in go 1.2,
|
||||
// which will eventually replace it.
|
||||
type textMarshaler interface {
|
||||
MarshalText() (text []byte, err error)
|
||||
}
|
||||
|
||||
func (w *textWriter) WriteString(s string) (n int, err error) {
|
||||
if !strings.Contains(s, "\n") {
|
||||
if !w.compact && w.complete {
|
||||
@ -366,7 +373,15 @@ func writeAny(w *textWriter, v reflect.Value, props *Properties) error {
|
||||
}
|
||||
}
|
||||
w.indent()
|
||||
if err := writeStruct(w, v); err != nil {
|
||||
if tm, ok := v.Interface().(textMarshaler); ok {
|
||||
text, err := tm.MarshalText()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = w.Write(text); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := writeStruct(w, v); err != nil {
|
||||
return err
|
||||
}
|
||||
w.unindent()
|
||||
@ -555,7 +570,18 @@ func writeExtensions(w *textWriter, pv reflect.Value) error {
|
||||
// Order the extensions by ID.
|
||||
// This isn't strictly necessary, but it will give us
|
||||
// canonical output, which will also make testing easier.
|
||||
m := ep.ExtensionMap()
|
||||
var m map[int32]Extension
|
||||
if em, ok := ep.(extensionsMap); ok {
|
||||
m = em.ExtensionMap()
|
||||
} else if em, ok := ep.(extensionsBytes); ok {
|
||||
eb := em.GetExtensions()
|
||||
var err error
|
||||
m, err = BytesToExtensionsMap(*eb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ids := make([]int32, 0, len(m))
|
||||
for id := range m {
|
||||
ids = append(ids, id)
|
||||
@ -653,6 +679,19 @@ func marshalText(w io.Writer, pb Message, compact bool) error {
|
||||
compact: compact,
|
||||
}
|
||||
|
||||
if tm, ok := pb.(textMarshaler); ok {
|
||||
text, err := tm.MarshalText()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = aw.Write(text); err != nil {
|
||||
return err
|
||||
}
|
||||
if bw != nil {
|
||||
return bw.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Dereference the received pointer so we don't have outer < and >.
|
||||
v := reflect.Indirect(val)
|
||||
if err := writeStruct(aw, v); err != nil {
|
||||
@ -666,7 +705,9 @@ func marshalText(w io.Writer, pb Message, compact bool) error {
|
||||
|
||||
// MarshalText writes a given protocol buffer in text format.
|
||||
// The only errors returned are from w.
|
||||
func MarshalText(w io.Writer, pb Message) error { return marshalText(w, pb, false) }
|
||||
func MarshalText(w io.Writer, pb Message) error {
|
||||
return marshalText(w, pb, false)
|
||||
}
|
||||
|
||||
// MarshalTextString is the same as MarshalText, but returns the string directly.
|
||||
func MarshalTextString(pb Message) string {
|
@ -48,6 +48,13 @@ import (
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// textUnmarshaler is implemented by Messages that can unmarshal themsleves.
|
||||
// It is identical to encoding.TextUnmarshaler, introduced in go 1.2,
|
||||
// which will eventually replace it.
|
||||
type textUnmarshaler interface {
|
||||
UnmarshalText(text []byte) error
|
||||
}
|
||||
|
||||
type ParseError struct {
|
||||
Message string
|
||||
Line int // 1-based line number
|
||||
@ -686,6 +693,7 @@ func (p *textParser) readAny(v reflect.Value, props *Properties) *ParseError {
|
||||
default:
|
||||
return p.errorf("expected '{' or '<', found %q", tok.value)
|
||||
}
|
||||
// TODO: Handle nested messages which implement textUnmarshaler.
|
||||
return p.readStruct(fv, terminator)
|
||||
case reflect.Uint32:
|
||||
if x, err := strconv.ParseUint(tok.value, 0, 32); err == nil {
|
||||
@ -704,6 +712,10 @@ func (p *textParser) readAny(v reflect.Value, props *Properties) *ParseError {
|
||||
// UnmarshalText reads a protocol buffer in Text format. UnmarshalText resets pb
|
||||
// before starting to unmarshal, so any existing data in pb is always removed.
|
||||
func UnmarshalText(s string, pb Message) error {
|
||||
if um, ok := pb.(textUnmarshaler); ok {
|
||||
err := um.UnmarshalText([]byte(s))
|
||||
return err
|
||||
}
|
||||
pb.Reset()
|
||||
v := reflect.ValueOf(pb)
|
||||
if pe := newTextParser(s).readStruct(v.Elem(), ""); pe != nil {
|
@ -37,7 +37,7 @@ import (
|
||||
"testing"
|
||||
|
||||
. "./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 {
|
||||
@ -413,6 +413,16 @@ func TestUnmarshalText(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalTextCustomMessage(t *testing.T) {
|
||||
msg := &textMessage{}
|
||||
if err := UnmarshalText("custom", msg); err != nil {
|
||||
t.Errorf("Unexpected error from custom unmarshal: %v", err)
|
||||
}
|
||||
if UnmarshalText("not custom", msg) == nil {
|
||||
t.Errorf("Didn't get expected error from custom unmarshal")
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test; this caused a panic.
|
||||
func TestRepeatedEnum(t *testing.T) {
|
||||
pb := new(RepeatedEnum)
|
@ -39,11 +39,31 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/third_party/code.google.com/p/goprotobuf/proto"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/code.google.com/p/gogoprotobuf/proto"
|
||||
|
||||
pb "./testdata"
|
||||
)
|
||||
|
||||
// textMessage implements the methods that allow it to marshal and unmarshal
|
||||
// itself as text.
|
||||
type textMessage struct {
|
||||
}
|
||||
|
||||
func (*textMessage) MarshalText() ([]byte, error) {
|
||||
return []byte("custom"), nil
|
||||
}
|
||||
|
||||
func (*textMessage) UnmarshalText(bytes []byte) error {
|
||||
if string(bytes) != "custom" {
|
||||
return errors.New("expected 'custom'")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*textMessage) Reset() {}
|
||||
func (*textMessage) String() string { return "" }
|
||||
func (*textMessage) ProtoMessage() {}
|
||||
|
||||
func newTestMessage() *pb.MyMessage {
|
||||
msg := &pb.MyMessage{
|
||||
Count: proto.Int32(42),
|
||||
@ -153,6 +173,16 @@ func TestMarshalText(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalTextCustomMessage(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := proto.MarshalText(buf, &textMessage{}); err != nil {
|
||||
t.Fatalf("proto.MarshalText: %v", err)
|
||||
}
|
||||
s := buf.String()
|
||||
if s != "custom" {
|
||||
t.Errorf("Got %q, expected %q", s, "custom")
|
||||
}
|
||||
}
|
||||
func TestMarshalTextNil(t *testing.T) {
|
||||
want := "<nil>"
|
||||
tests := []proto.Message{nil, (*pb.MyMessage)(nil)}
|
2
Godeps/_workspace/src/github.com/codegangsta/cli/.travis.yml
generated
vendored
Normal file
2
Godeps/_workspace/src/github.com/codegangsta/cli/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
language: go
|
||||
go: 1.1
|
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.
|
257
Godeps/_workspace/src/github.com/codegangsta/cli/README.md
generated
vendored
Normal file
257
Godeps/_workspace/src/github.com/codegangsta/cli/README.md
generated
vendored
Normal file
@ -0,0 +1,257 @@
|
||||
[](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{"lang", "english", "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{"lang, l", "english", "language for the greeting"},
|
||||
}
|
||||
```
|
||||
|
||||
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 {
|
||||
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`
|
||||
|
||||
|
||||
## About
|
||||
cli.go is written by none other than the [Code Gangsta](http://codegangsta.io)
|
238
Godeps/_workspace/src/github.com/codegangsta/cli/app.go
generated
vendored
Normal file
238
Godeps/_workspace/src/github.com/codegangsta/cli/app.go
generated
vendored
Normal file
@ -0,0 +1,238 @@
|
||||
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
|
||||
// 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(),
|
||||
Author: "Author",
|
||||
Email: "unknown@email",
|
||||
}
|
||||
}
|
||||
|
||||
// Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination
|
||||
func (a *App) Run(arguments []string) error {
|
||||
// append help to commands
|
||||
if a.Command(helpCommand.Name) == nil {
|
||||
a.Commands = append(a.Commands, helpCommand)
|
||||
}
|
||||
|
||||
//append version/help flags
|
||||
if a.EnableBashCompletion {
|
||||
a.appendFlag(BashCompletionFlag)
|
||||
}
|
||||
a.appendFlag(VersionFlag)
|
||||
a.appendFlag(HelpFlag)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.Commands = append(a.Commands, helpCommand)
|
||||
}
|
||||
}
|
||||
|
||||
// append flags
|
||||
if a.EnableBashCompletion {
|
||||
a.appendFlag(BashCompletionFlag)
|
||||
}
|
||||
a.appendFlag(HelpFlag)
|
||||
|
||||
// parse flags
|
||||
set := flagSet(a.Name, a.Flags)
|
||||
set.SetOutput(ioutil.Discard)
|
||||
err := set.Parse(ctx.Args().Tail())
|
||||
nerr := normalizeFlags(a.Flags, set)
|
||||
context := NewContext(a, set, set)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
371
Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go
generated
vendored
Normal file
371
Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go
generated
vendored
Normal file
@ -0,0 +1,371 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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", "Bob", "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 [command options] [arguments...]
|
||||
//
|
||||
// DESCRIPTION:
|
||||
// This is how we describe describeit the function
|
||||
//
|
||||
// OPTIONS:
|
||||
}
|
||||
|
||||
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("%s does not match %s", parsedIntSlice, expectedIntSlice)
|
||||
}
|
||||
|
||||
if !StrsEquals(parsedStringSlice, expectedStringSlice) {
|
||||
t.Errorf("%s does not match %s", 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 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)
|
||||
}
|
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
|
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
|
87
Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go
generated
vendored
Normal file
87
Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go
generated
vendored
Normal file
@ -0,0 +1,87 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||
"os"
|
||||
)
|
||||
|
||||
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", "Bob", "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{"surname", "Jones", "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{"nickname", "Stevie", "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)
|
||||
}
|
136
Godeps/_workspace/src/github.com/codegangsta/cli/command.go
generated
vendored
Normal file
136
Godeps/_workspace/src/github.com/codegangsta/cli/command.go
generated
vendored
Normal file
@ -0,0 +1,136 @@
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 the flags and commands
|
||||
app.Commands = c.Subcommands
|
||||
app.Flags = c.Flags
|
||||
|
||||
// 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)
|
||||
}
|
48
Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go
generated
vendored
Normal file
48
Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go
generated
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
271
Godeps/_workspace/src/github.com/codegangsta/cli/context.go
generated
vendored
Normal file
271
Godeps/_workspace/src/github.com/codegangsta/cli/context.go
generated
vendored
Normal file
@ -0,0 +1,271 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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 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 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
68
Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go
generated
vendored
Normal file
68
Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go
generated
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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_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)
|
||||
}
|
280
Godeps/_workspace/src/github.com/codegangsta/cli/flag.go
generated
vendored
Normal file
280
Godeps/_workspace/src/github.com/codegangsta/cli/flag.go
generated
vendored
Normal file
@ -0,0 +1,280 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// This flag enables bash-completion for all commands and subcommands
|
||||
var BashCompletionFlag = BoolFlag{"generate-bash-completion", ""}
|
||||
|
||||
// This flag prints the version for the application
|
||||
var VersionFlag = BoolFlag{"version, v", "print the version"}
|
||||
|
||||
// This flag prints the help for all commands and subcommands
|
||||
var HelpFlag = BoolFlag{"help, h", "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
|
||||
}
|
||||
|
||||
func (f GenericFlag) String() string {
|
||||
return fmt.Sprintf("%s%s %v\t`%v` %s", prefixFor(f.Name), f.Name, f.Value, "-"+f.Name+" option -"+f.Name+" option", f.Usage)
|
||||
}
|
||||
|
||||
func (f GenericFlag) Apply(set *flag.FlagSet) {
|
||||
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
|
||||
}
|
||||
|
||||
func (f StringSliceFlag) String() string {
|
||||
firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ")
|
||||
pref := prefixFor(firstName)
|
||||
return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)
|
||||
}
|
||||
|
||||
func (f StringSliceFlag) Apply(set *flag.FlagSet) {
|
||||
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
|
||||
}
|
||||
|
||||
func (f IntSliceFlag) String() string {
|
||||
firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ")
|
||||
pref := prefixFor(firstName)
|
||||
return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)
|
||||
}
|
||||
|
||||
func (f IntSliceFlag) Apply(set *flag.FlagSet) {
|
||||
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
|
||||
}
|
||||
|
||||
func (f BoolFlag) String() string {
|
||||
return fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage)
|
||||
}
|
||||
|
||||
func (f BoolFlag) Apply(set *flag.FlagSet) {
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Bool(name, false, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f BoolFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
type BoolTFlag struct {
|
||||
Name string
|
||||
Usage string
|
||||
}
|
||||
|
||||
func (f BoolTFlag) String() string {
|
||||
return fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage)
|
||||
}
|
||||
|
||||
func (f BoolTFlag) Apply(set *flag.FlagSet) {
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Bool(name, true, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f BoolTFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
type StringFlag struct {
|
||||
Name string
|
||||
Value string
|
||||
Usage 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 fmt.Sprintf(fmtString, prefixedNames(f.Name), f.Value, f.Usage)
|
||||
}
|
||||
|
||||
func (f StringFlag) Apply(set *flag.FlagSet) {
|
||||
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
|
||||
}
|
||||
|
||||
func (f IntFlag) String() string {
|
||||
return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage)
|
||||
}
|
||||
|
||||
func (f IntFlag) Apply(set *flag.FlagSet) {
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Int(name, f.Value, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f IntFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
type Float64Flag struct {
|
||||
Name string
|
||||
Value float64
|
||||
Usage string
|
||||
}
|
||||
|
||||
func (f Float64Flag) String() string {
|
||||
return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage)
|
||||
}
|
||||
|
||||
func (f Float64Flag) Apply(set *flag.FlagSet) {
|
||||
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
|
||||
}
|
194
Godeps/_workspace/src/github.com/codegangsta/cli/flag_test.go
generated
vendored
Normal file
194
Godeps/_workspace/src/github.com/codegangsta/cli/flag_test.go
generated
vendored
Normal file
@ -0,0 +1,194 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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 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 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 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"})
|
||||
}
|
||||
|
||||
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"})
|
||||
}
|
213
Godeps/_workspace/src/github.com/codegangsta/cli/help.go
generated
vendored
Normal file
213
Godeps/_workspace/src/github.com/codegangsta/cli/help.go
generated
vendored
Normal file
@ -0,0 +1,213 @@
|
||||
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}} [global options] command [command options] [arguments...]
|
||||
|
||||
VERSION:
|
||||
{{.Version}}
|
||||
|
||||
COMMANDS:
|
||||
{{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}
|
||||
{{end}}
|
||||
GLOBAL OPTIONS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{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}} [command options] [arguments...]
|
||||
|
||||
DESCRIPTION:
|
||||
{{.Description}}
|
||||
|
||||
OPTIONS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{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}} [global options] command [command options] [arguments...]
|
||||
|
||||
COMMANDS:
|
||||
{{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}
|
||||
{{end}}
|
||||
OPTIONS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{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
|
||||
|
||||
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) {
|
||||
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 raw.toResponse()
|
||||
return raw.Unmarshal()
|
||||
}
|
||||
|
||||
// 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 raw.toResponse()
|
||||
return raw.Unmarshal()
|
||||
}
|
@ -32,7 +32,7 @@ type Config struct {
|
||||
CertFile string `json:"certFile"`
|
||||
KeyFile string `json:"keyFile"`
|
||||
CaCertFile []string `json:"caCertFiles"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
DialTimeout time.Duration `json:"timeout"`
|
||||
Consistency string `json:"consistency"`
|
||||
}
|
||||
|
||||
@ -42,7 +42,20 @@ type Client struct {
|
||||
httpClient *http.Client
|
||||
persistence io.Writer
|
||||
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
|
||||
@ -50,7 +63,7 @@ type Client struct {
|
||||
func NewClient(machines []string) *Client {
|
||||
config := Config{
|
||||
// default timeout is one second
|
||||
Timeout: time.Second,
|
||||
DialTimeout: time.Second,
|
||||
// default consistency level is STRONG
|
||||
Consistency: STRONG_CONSISTENCY,
|
||||
}
|
||||
@ -58,7 +71,6 @@ func NewClient(machines []string) *Client {
|
||||
client := &Client{
|
||||
cluster: NewCluster(machines),
|
||||
config: config,
|
||||
keyPrefix: path.Join(version, "keys"),
|
||||
}
|
||||
|
||||
client.initHTTPClient()
|
||||
@ -76,7 +88,7 @@ func NewTLSClient(machines []string, cert, key, caCert string) (*Client, error)
|
||||
|
||||
config := Config{
|
||||
// default timeout is one second
|
||||
Timeout: time.Second,
|
||||
DialTimeout: time.Second,
|
||||
// default consistency level is STRONG
|
||||
Consistency: STRONG_CONSISTENCY,
|
||||
CertFile: cert,
|
||||
@ -87,7 +99,6 @@ func NewTLSClient(machines []string, cert, key, caCert string) (*Client, error)
|
||||
client := &Client{
|
||||
cluster: NewCluster(machines),
|
||||
config: config,
|
||||
keyPrefix: path.Join(version, "keys"),
|
||||
}
|
||||
|
||||
err := client.initHTTPSClient(cert, key)
|
||||
@ -157,16 +168,10 @@ func (c *Client) SetTransport(tr *http.Transport) {
|
||||
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
|
||||
func (c *Client) initHTTPClient() {
|
||||
tr := &http.Transport{
|
||||
Dial: dialTimeout,
|
||||
Dial: c.dial,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
@ -192,7 +197,7 @@ func (c *Client) initHTTPSClient(cert, key string) error {
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
Dial: dialTimeout,
|
||||
Dial: c.dial,
|
||||
}
|
||||
|
||||
c.httpClient = &http.Client{Transport: tr}
|
||||
@ -226,6 +231,11 @@ func (c *Client) SetConsistency(consistency string) error {
|
||||
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
|
||||
func (c *Client) AddRootCA(caCert string) error {
|
||||
if c.httpClient == nil {
|
||||
@ -326,9 +336,29 @@ func (c *Client) createHttpPath(serverName string, _path string) string {
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// Dial with timeout.
|
||||
func dialTimeout(network, addr string) (net.Conn, error) {
|
||||
return net.DialTimeout(network, addr, time.Second)
|
||||
// dial attempts to open a TCP connection to the provided address, explicitly
|
||||
// enabling keep-alives with a one-second interval.
|
||||
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() {
|
@ -8,7 +8,7 @@ func (c *Client) CompareAndDelete(key string, prevValue string, prevIndex uint64
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return raw.toResponse()
|
||||
return raw.Unmarshal()
|
||||
}
|
||||
|
||||
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.")
|
||||
}
|
||||
|
||||
options := options{}
|
||||
options := Options{}
|
||||
if prevValue != "" {
|
||||
options["prevValue"] = prevValue
|
||||
}
|
@ -9,7 +9,7 @@ func (c *Client) CompareAndSwap(key string, value string, ttl uint64,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return raw.toResponse()
|
||||
return raw.Unmarshal()
|
||||
}
|
||||
|
||||
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.")
|
||||
}
|
||||
|
||||
options := options{}
|
||||
options := Options{}
|
||||
if 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 raw.toResponse()
|
||||
return raw.Unmarshal()
|
||||
}
|
||||
|
||||
// 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 raw.toResponse()
|
||||
return raw.Unmarshal()
|
||||
}
|
||||
|
||||
func (c *Client) RawDelete(key string, recursive bool, dir bool) (*RawResponse, error) {
|
||||
ops := options{
|
||||
ops := Options{
|
||||
"recursive": recursive,
|
||||
"dir": dir,
|
||||
}
|
@ -14,11 +14,11 @@ func (c *Client) Get(key string, sort, recursive bool) (*Response, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return raw.toResponse()
|
||||
return raw.Unmarshal()
|
||||
}
|
||||
|
||||
func (c *Client) RawGet(key string, sort, recursive bool) (*RawResponse, error) {
|
||||
ops := options{
|
||||
ops := Options{
|
||||
"recursive": recursive,
|
||||
"sorted": sort,
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user