Compare commits
1664 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3d784a14f9 | ||
|
47a9b086ea | ||
|
e70c8a7b46 | ||
|
673351d821 | ||
|
4b966f1f82 | ||
|
93626de01c | ||
|
7847b7685d | ||
|
255e88fbf6 | ||
|
685c6dc00c | ||
|
8e18d37b3d | ||
|
b4c7b90c9e | ||
|
b55be9fdea | ||
|
401b3afa3b | ||
|
7fa3537015 | ||
|
149ed91afb | ||
|
887826ee68 | ||
|
7357d5eae2 | ||
|
e40e3af760 | ||
|
24a2788081 | ||
|
1388266102 | ||
|
43af0b051f | ||
|
6e8138e19b | ||
|
fb8edd86d5 | ||
|
34be181706 | ||
|
fcc1109e76 | ||
|
2b828765e3 | ||
|
25f4c23ab2 | ||
|
be90b20a5d | ||
|
232c113dae | ||
|
605a9b2817 | ||
|
d044c0f4cc | ||
|
1959e1fd44 | ||
|
6712423dd1 | ||
|
3689990bd5 | ||
|
81a1f618f9 | ||
|
b77bb690de | ||
|
f843f260ee | ||
|
770b3739e0 | ||
|
261e7c1744 | ||
|
10acbb8d92 | ||
|
a917115a85 | ||
|
b8ed6f1588 | ||
|
3ed57e01a6 | ||
|
cb7c5a8ca1 | ||
|
07eb9c5970 | ||
|
306e5081d9 | ||
|
259c7adc81 | ||
|
af9762cf32 | ||
|
17554202f6 | ||
|
0d9cf697fa | ||
|
df0dd2f5e6 | ||
|
38508f9a9c | ||
|
b113972bcf | ||
|
72e67bf4e9 | ||
|
da8aa2d8e4 | ||
|
602a2ea541 | ||
|
fd24b1898e | ||
|
89150e1164 | ||
|
e1831c4c60 | ||
|
4ec90c5c0d | ||
|
a8c73f7baf | ||
|
6fed76a687 | ||
|
84de444325 | ||
|
0fbd87ca87 | ||
|
99797502eb | ||
|
16bd0b9ca8 | ||
|
5fdfa963f4 | ||
|
1d86e71331 | ||
|
9e3f549341 | ||
|
2895ad21f3 | ||
|
5731ae7f47 | ||
|
51f7d9a07f | ||
|
6be390c795 | ||
|
0f32de4aa2 | ||
|
5d01452648 | ||
|
51b0508512 | ||
|
4c5e7a238d | ||
|
f327b7b499 | ||
|
306e86c9c6 | ||
|
9024f1b444 | ||
|
fc26e8c194 | ||
|
ffd8e5667c | ||
|
9299c3abc7 | ||
|
63a07fe6cf | ||
|
c2d440a914 | ||
|
2b5c7f9e91 | ||
|
91e63dea47 | ||
|
cd164de776 | ||
|
c0ef5ce512 | ||
|
7c852fbf33 | ||
|
28500989bc | ||
|
75c99a0491 | ||
|
8b4ba3cb67 | ||
|
3ef2971c3f | ||
|
a5aa8c6006 | ||
|
022d14abe1 | ||
|
1800b0b69c | ||
|
c39a550b00 | ||
|
092aa8fa6d | ||
|
f75f73f3d2 | ||
|
e3627e9cba | ||
|
d5f4934acf | ||
|
693bd7e110 | ||
|
4d8dcdc623 | ||
|
8e97af8dc3 | ||
|
4dc448056c | ||
|
68c349bbfa | ||
|
75aedc8e94 | ||
|
8b08f89d2c | ||
|
889b38f75a | ||
|
a17ac23457 | ||
|
6fdd48509e | ||
|
62800116d3 | ||
|
1bccbf061b | ||
|
093658836e | ||
|
f49800e56a | ||
|
e478dbeb85 | ||
|
51486b18fa | ||
|
48d98dcf45 | ||
|
2c7cfd1c68 | ||
|
7a4b4c941c | ||
|
608ccb0ca1 | ||
|
3f6ea04048 | ||
|
74c5ec70a9 | ||
|
c8bf8e896a | ||
|
09cc1161c9 | ||
|
8ab33db51a | ||
|
cc4258bf9d | ||
|
0ee5d3d83f | ||
|
c39aa5e857 | ||
|
39aae4167e | ||
|
9db9143366 | ||
|
49814b92fe | ||
|
260b5d6b0d | ||
|
4360ca14c1 | ||
|
c7d336f958 | ||
|
f6436663eb | ||
|
84d7c65039 | ||
|
4245096be4 | ||
|
c9b2a07bc7 | ||
|
e69d4cba88 | ||
|
96962dd21f | ||
|
36d48224b5 | ||
|
53779d6ceb | ||
|
e7e268b3bd | ||
|
ca2f76fe1f | ||
|
4d44ab9628 | ||
|
dd62051e6c | ||
|
fdb1701d1b | ||
|
80b35575df | ||
|
69cf05df9a | ||
|
69a1817c3f | ||
|
a918dcd5a4 | ||
|
adc9a65ae3 | ||
|
1e779f7135 | ||
|
fe68e9e243 | ||
|
e9792b446f | ||
|
4012599264 | ||
|
429b1d8574 | ||
|
a34876d700 | ||
|
68ecf78f0e | ||
|
38344b342d | ||
|
346ff96de2 | ||
|
31614bebc4 | ||
|
be888b59a6 | ||
|
6069df6cbd | ||
|
5e7b6e4860 | ||
|
ea6fa6e889 | ||
|
3e914256ce | ||
|
85ce16b34f | ||
|
d306c8fd50 | ||
|
8d7eccad5d | ||
|
d18edd6f77 | ||
|
cad3704efd | ||
|
9a4b455c3f | ||
|
01c8798e4e | ||
|
61744fba11 | ||
|
0034bef6b9 | ||
|
63c3ed3931 | ||
|
8a5db8a3ee | ||
|
adc2b62c22 | ||
|
1f2fe08c33 | ||
|
77b1933833 | ||
|
c4df78b4b9 | ||
|
c1dc783512 | ||
|
518a37e776 | ||
|
b143101f82 | ||
|
2be6f4d153 | ||
|
ac612734c8 | ||
|
ffe69c67fc | ||
|
b3057a0ec3 | ||
|
563f059e73 | ||
|
6bbe7262ef | ||
|
55a1a81010 | ||
|
97ec764db7 | ||
|
f6df556eb0 | ||
|
5cd9396dae | ||
|
886a6bdbe0 | ||
|
ab60e702d2 | ||
|
17141b3589 | ||
|
8f23243cb8 | ||
|
c2345c6e9a | ||
|
2617de2cdd | ||
|
9cf6827ccc | ||
|
681892148e | ||
|
558452a143 | ||
|
5a173fa968 | ||
|
72397ef90c | ||
|
79ad4b4544 | ||
|
49f3713c4f | ||
|
4b5c3ccf58 | ||
|
21dec70971 | ||
|
0f2b774ea1 | ||
|
e929caf15a | ||
|
8d848c3d60 | ||
|
b8b0c8f3e5 | ||
|
15e78da7eb | ||
|
d80700810f | ||
|
c1de6abf23 | ||
|
01b916eaa0 | ||
|
62c03b3318 | ||
|
65679af61d | ||
|
821ad31cf6 | ||
|
ea750ad813 | ||
|
3d7633f4a6 | ||
|
d356ef1c5b | ||
|
fce762febf | ||
|
535280c162 | ||
|
bb8a193244 | ||
|
e6bdfa1d29 | ||
|
d1d2611665 | ||
|
8389b46b5c | ||
|
b9f826554c | ||
|
0750235712 | ||
|
ee0e014617 | ||
|
2e20394af4 | ||
|
6ab991ebf4 | ||
|
ef8894ef26 | ||
|
8b4efa1760 | ||
|
b0b8b75258 | ||
|
2e19e45aa4 | ||
|
e1d097ea20 | ||
|
ed12366d52 | ||
|
4919b638f9 | ||
|
49563e638b | ||
|
07d0eb9ae6 | ||
|
336135c392 | ||
|
d2b38e6ac4 | ||
|
883f90dded | ||
|
58e82743f8 | ||
|
51a0994d2d | ||
|
da20db862d | ||
|
d6c9f51082 | ||
|
08d7bb0d08 | ||
|
1bcb3d8cc2 | ||
|
c17de070fb | ||
|
b893374dc1 | ||
|
fe532ed4f2 | ||
|
6377a19b12 | ||
|
ca7ea68a6a | ||
|
a45f285a5c | ||
|
fa2c57f7cb | ||
|
0779c6a139 | ||
|
2916f540c1 | ||
|
7932e317c8 | ||
|
fd26cf265d | ||
|
3e76c25887 | ||
|
a0e2f47679 | ||
|
d70add10ab | ||
|
119d0134e0 | ||
|
2e085fa253 | ||
|
f8f7edd124 | ||
|
79ecff7b42 | ||
|
0f2c4fb5f4 | ||
|
ec1952157b | ||
|
cd38359458 | ||
|
8a86777db8 | ||
|
e7033071b9 | ||
|
f99a473436 | ||
|
c4b7e8f288 | ||
|
f346251719 | ||
|
4c3cf87f62 | ||
|
cb417b8077 | ||
|
076d6abfe4 | ||
|
82308c9a53 | ||
|
5d35079809 | ||
|
50e24f461c | ||
|
37886892c8 | ||
|
72ffa91fe0 | ||
|
9908137638 | ||
|
f3ecc040c8 | ||
|
e271378a97 | ||
|
5d050ae3ac | ||
|
615ceab597 | ||
|
f1b085fa36 | ||
|
bd4c822670 | ||
|
03d5a95bde | ||
|
e2ec64947a | ||
|
dabd9e2208 | ||
|
4c060a78cc | ||
|
cfaf47c8a2 | ||
|
87da7520de | ||
|
4a68d29ce2 | ||
|
0ca2149408 | ||
|
0cfaab02c0 | ||
|
2d54065082 | ||
|
3cfbe7cf6d | ||
|
e2d8a95c91 | ||
|
3419f9aeb9 | ||
|
ebded2cbc0 | ||
|
fb617044e0 | ||
|
5a0b5470e7 | ||
|
6b4144ad10 | ||
|
8f16ff9c49 | ||
|
ac6b11037d | ||
|
848e45c22c | ||
|
2c0bf335ba | ||
|
aef24dd74b | ||
|
c2c6aee18a | ||
|
6451b47621 | ||
|
2b2cfdfb32 | ||
|
5f4d440493 | ||
|
5f0451affe | ||
|
156f6b8d3c | ||
|
f0ee2890b2 | ||
|
16c283c91a | ||
|
db13dbdf46 | ||
|
06905cb14a | ||
|
6ea9c4dd3f | ||
|
c5c8382742 | ||
|
115ddc6a4a | ||
|
54ca0ce34f | ||
|
f19c497621 | ||
|
0561a20c06 | ||
|
162490dadf | ||
|
30087794ba | ||
|
9ebe3c38b2 | ||
|
7155f0d50d | ||
|
75e05ca142 | ||
|
5d4423910d | ||
|
0de1ff8634 | ||
|
e5fb1ffeb7 | ||
|
8c53318dac | ||
|
0d6f259adc | ||
|
85ab0e6e70 | ||
|
a18294d417 | ||
|
fecd0ca391 | ||
|
97bd92c76f | ||
|
49b89c30d8 | ||
|
8228a8e3f7 | ||
|
78be3df99a | ||
|
2f0db9a974 | ||
|
227fab3867 | ||
|
9537449b07 | ||
|
246b245959 | ||
|
a433e469cc | ||
|
04958c6951 | ||
|
b54c956c5e | ||
|
8735263930 | ||
|
a79d6aa669 | ||
|
7efafa5a2c | ||
|
0b436563bd | ||
|
5d379dc3e3 | ||
|
8c60774c6a | ||
|
9b2423aaba | ||
|
fc8c24e987 | ||
|
d7bd69714d | ||
|
099bbb8be7 | ||
|
c29a69a60d | ||
|
69e4f35d9a | ||
|
ff40467207 | ||
|
190c6c661f | ||
|
e633799c14 | ||
|
f7c6c562a5 | ||
|
bc6e9d5042 | ||
|
a0b1d54012 | ||
|
60b5286f8c | ||
|
aa3ea17a8f | ||
|
698621f127 | ||
|
906f4fe8f7 | ||
|
ddf199566c | ||
|
a47d770e71 | ||
|
057498ed01 | ||
|
fa562dc916 | ||
|
0be895febb | ||
|
11a0078966 | ||
|
92f8e5cd3f | ||
|
5b3762be08 | ||
|
3b01488c8d | ||
|
2f65572247 | ||
|
e42ddfc3d6 | ||
|
d63636243c | ||
|
a0b9c0d007 | ||
|
1f7a4174ba | ||
|
761c58e040 | ||
|
01c3d3905c | ||
|
c815a732ef | ||
|
5d91c7e15c | ||
|
c39d21c178 | ||
|
b6498cdcbc | ||
|
a09dfa3ce1 | ||
|
d3ae88f108 | ||
|
1fad7e5a1c | ||
|
19546ab518 | ||
|
e6e9a86919 | ||
|
c6dd1dccc3 | ||
|
993caf5058 | ||
|
450471d30a | ||
|
7eeecd23ac | ||
|
21c94141ba | ||
|
bc2cba5aa4 | ||
|
5e49354bf2 | ||
|
55334b2062 | ||
|
74dc5b1c58 | ||
|
ac11323fdd | ||
|
8c2e99432d | ||
|
aa26927d61 | ||
|
22ee8700ca | ||
|
df55c24cb5 | ||
|
99ddd7f9cb | ||
|
82b2a102ed | ||
|
c7df82e695 | ||
|
638960284e | ||
|
8e9b8a0953 | ||
|
3f044c48fa | ||
|
37d8e32e0b | ||
|
46ce807624 | ||
|
e6a88f3531 | ||
|
95d86d84b4 | ||
|
70fa42aee0 | ||
|
ba99fbe390 | ||
|
6a55772cda | ||
|
6dcb51a4bd | ||
|
c875819a2e | ||
|
6d4cf0d892 | ||
|
78a9d20691 | ||
|
7c2409b5a7 | ||
|
0335f6fba9 | ||
|
2c7b7cd6ca | ||
|
5632952665 | ||
|
7eeac63139 | ||
|
1b54f4d32a | ||
|
e8e9dd9400 | ||
|
b722748ec3 | ||
|
609b2630d7 | ||
|
5bdf8a5ea3 | ||
|
7a2592b2fa | ||
|
546bebc860 | ||
|
ad51f4f2a5 | ||
|
94a6f8426b | ||
|
32f7fb8bff | ||
|
a777c3553c | ||
|
51650c1412 | ||
|
157580c232 | ||
|
05f052b092 | ||
|
1431ac5751 | ||
|
a9deeb321b | ||
|
ec86149b1e | ||
|
31f92001e2 | ||
|
d69977c229 | ||
|
44e06a1a1e | ||
|
f9689d1562 | ||
|
4cb1ae4626 | ||
|
f04813fa02 | ||
|
742029d8a4 | ||
|
f74526a36e | ||
|
61e1836472 | ||
|
8d8e509fe6 | ||
|
147e79ea07 | ||
|
5eae95ee46 | ||
|
9e26f0b058 | ||
|
8cc3c4a6b7 | ||
|
1d8bdd4384 | ||
|
5acd43efaf | ||
|
7033b996c6 | ||
|
0c76a8ac89 | ||
|
f10516deb7 | ||
|
d4311f9cf5 | ||
|
6a50a6fd5a | ||
|
29473ef356 | ||
|
1f1ecb15f6 | ||
|
38d655636d | ||
|
9ab5cbf235 | ||
|
fdf14cd101 | ||
|
f63873cc73 | ||
|
c2938ff138 | ||
|
ab2c98d931 | ||
|
0ae8cd9a9d | ||
|
f3aefe282c | ||
|
a80cca95a2 | ||
|
c52f4b043d | ||
|
253060b4f3 | ||
|
36966da701 | ||
|
bb7c4aaf7e | ||
|
bd4846aa9c | ||
|
c68ebaa2ca | ||
|
538424b01c | ||
|
48e7a87741 | ||
|
74ace58ae1 | ||
|
913d8737cc | ||
|
b98f5ed8b1 | ||
|
e4bb506ace | ||
|
0f0ba099c9 | ||
|
f400292be7 | ||
|
efc6560d83 | ||
|
4055654e9b | ||
|
56488d435f | ||
|
f586950528 | ||
|
a302731cd1 | ||
|
00728e711c | ||
|
ef753838e7 | ||
|
acb79d6f73 | ||
|
157c796294 | ||
|
0861c59bec | ||
|
e4a7375d34 | ||
|
6bbac65f7e | ||
|
845f1a7377 | ||
|
9c8e518423 | ||
|
bd3b787fd5 | ||
|
27e4a8a227 | ||
|
cf2d7497e4 | ||
|
df41cd925e | ||
|
e46de74328 | ||
|
feeb7f81a6 | ||
|
2beb5236d0 | ||
|
f062ee80c8 | ||
|
a7bb768e98 | ||
|
07be89d6e9 | ||
|
d81c4e6d1a | ||
|
870755e90d | ||
|
bd3c8c3cde | ||
|
278b3180c3 | ||
|
bb2686a08f | ||
|
202783ca7d | ||
|
308904110a | ||
|
60b4095c75 | ||
|
d04b4fa2cc | ||
|
2d449f63e0 | ||
|
1ec4e03738 | ||
|
9cd47dd2aa | ||
|
015cd7a3d0 | ||
|
7ff6e6b66f | ||
|
e92b01c528 | ||
|
bb33128552 | ||
|
86add29838 | ||
|
70712a0f62 | ||
|
4db937b571 | ||
|
ad6f41c77a | ||
|
e6040e55f5 | ||
|
dad0e75121 | ||
|
c159e316be | ||
|
b4ac3d4470 | ||
|
43d22d7a2f | ||
|
d62f7e2082 | ||
|
cfe2f1a1e6 | ||
|
6f6ebb8025 | ||
|
7732e2307e | ||
|
8c733abef3 | ||
|
4809476c19 | ||
|
d727761e5d | ||
|
4d79c2a6d2 | ||
|
8627256e74 | ||
|
ed0c7d9c49 | ||
|
fb4717d5f3 | ||
|
09b489a614 | ||
|
402f7011d4 | ||
|
838dd8c19f | ||
|
91cafd1752 | ||
|
eea60b6baa | ||
|
baf8d63cb4 | ||
|
967e4208da | ||
|
ba3a579d07 | ||
|
1d53077fc7 | ||
|
4b480ece13 | ||
|
7d2b7cd7f1 | ||
|
73b4df4e18 | ||
|
a23a9228da | ||
|
37aa902cef | ||
|
bafb583666 | ||
|
aabebb2185 | ||
|
f611ef0edd | ||
|
d8f69700e6 | ||
|
c8ae97fd38 | ||
|
d50b6a34bc | ||
|
853be929bc | ||
|
3bb04142f3 | ||
|
d53fbb9d7f | ||
|
a1911a9608 | ||
|
ff2e2d5026 | ||
|
a953d3ad89 | ||
|
9ce444b91a | ||
|
ae8be89767 | ||
|
5774d100c1 | ||
|
dbe720f0f1 | ||
|
5afc8f2b12 | ||
|
c7e008f57a | ||
|
14b7152bf0 | ||
|
3ef6bf2118 | ||
|
f0ab2721a5 | ||
|
2721c2017c | ||
|
a7c158f0e1 | ||
|
7ff9193cf5 | ||
|
5ce4a2d05c | ||
|
031451abab | ||
|
8d75aba7eb | ||
|
027093a5a5 | ||
|
bdc0e3bfcf | ||
|
b2a57ca1f3 | ||
|
6ef0e6791b | ||
|
9374d6b3b9 | ||
|
f173ff02e3 | ||
|
ba2046491a | ||
|
083b471bcf | ||
|
bf73127e0b | ||
|
333b785061 | ||
|
79bf19c897 | ||
|
0c0ecc1cdc | ||
|
bacd58ed7b | ||
|
689f120410 | ||
|
2303301d38 | ||
|
f323df466d | ||
|
b1f1a5b757 | ||
|
0d262561d1 | ||
|
12c713b187 | ||
|
b1836587f2 | ||
|
04d8b5d483 | ||
|
461ebf6d88 | ||
|
41eb4f1c70 | ||
|
31a8e3e39a | ||
|
139f280f35 | ||
|
17ad5153b8 | ||
|
bb14ec70bd | ||
|
e8e36bd9d5 | ||
|
f9b1106df2 | ||
|
df600d6f3c | ||
|
157e76e829 | ||
|
dbc3b85cd0 | ||
|
11691019a0 | ||
|
3192307d59 | ||
|
ba8c9295ac | ||
|
d5436fb28b | ||
|
886cc83ad9 | ||
|
9e012a6b54 | ||
|
5eda08e9b8 | ||
|
ec6e46e2cb | ||
|
56fe023a12 | ||
|
aa705dd691 | ||
|
aa6fea7f21 | ||
|
e31c85aace | ||
|
1c3e4124f8 | ||
|
586ba31120 | ||
|
c1757372d3 | ||
|
7451449dd6 | ||
|
5b2b29043c | ||
|
2758664226 | ||
|
bb3f28ffa7 | ||
|
6ceb2af4a7 | ||
|
d5b649bf1c | ||
|
81f23cc732 | ||
|
b59276ff1c | ||
|
2e95832812 | ||
|
01f2b3cd20 | ||
|
2240bf9430 | ||
|
db036edccd | ||
|
1fbf5b84a2 | ||
|
08e1f626c1 | ||
|
c0d08f5e3e | ||
|
eac20d61df | ||
|
dec3f0798a | ||
|
bddb4cc33c | ||
|
62ded580ce | ||
|
51227241b7 | ||
|
9cf4e730e7 | ||
|
e9c63f3988 | ||
|
2c47691cf1 | ||
|
599b699ac9 | ||
|
a5beeb4f04 | ||
|
446d73fcf5 | ||
|
e299775d67 | ||
|
2c18750537 | ||
|
f317e50136 | ||
|
1d84bda7ca | ||
|
ae7c947ba5 | ||
|
6d07729c55 | ||
|
1d7bf200a8 | ||
|
6bc59f8b33 | ||
|
b2cf03fa5c | ||
|
36e273714d | ||
|
6be77b7fb9 | ||
|
6bcf45f136 | ||
|
8bca8236db | ||
|
67a0b4b4b1 | ||
|
a7200a292b | ||
|
fb5aa4c9c1 | ||
|
3f5772c62a | ||
|
e76836b948 | ||
|
2d946d7ee7 | ||
|
10ca35dccd | ||
|
bfdd1997f6 | ||
|
9420308667 | ||
|
83e09acc9f | ||
|
d6d795e286 | ||
|
c09febfffc | ||
|
5b3bba8f6e | ||
|
085593b9e5 | ||
|
e2a5d4f83e | ||
|
e3671cbb04 | ||
|
a525d02cc5 | ||
|
3c8c5ebb96 | ||
|
1cc1a4e6e2 | ||
|
3f0af3fe09 | ||
|
e2bac47a0a | ||
|
bc26d9f0de | ||
|
5c4692a0df | ||
|
0ba28bbc8b | ||
|
550184275a | ||
|
c376083ecb | ||
|
1db5fcf200 | ||
|
16b2555ab3 | ||
|
9227d32d57 | ||
|
c37b040217 | ||
|
5a1d2aa4b6 | ||
|
4a3b1f3847 | ||
|
d9a5258f40 | ||
|
190ebbed27 | ||
|
a0872c9e31 | ||
|
68cc826519 | ||
|
f5b306e7ff | ||
|
7a1feb3c51 | ||
|
e691168cdc | ||
|
4eda1e1bd4 | ||
|
1e8df9f245 | ||
|
b72937e8fb | ||
|
df11e67bb4 | ||
|
b7d20496f3 | ||
|
67847c3117 | ||
|
a2a0c80acb | ||
|
b3fd06fb45 | ||
|
c5db8d903c | ||
|
8fcd242494 | ||
|
ebd9af900e | ||
|
b02381c2d5 | ||
|
dce65ab9c2 | ||
|
97295f270b | ||
|
8e64bc8785 | ||
|
9b199ea756 | ||
|
ec3b913ee4 | ||
|
c210ab31d9 | ||
|
6c1fa91c70 | ||
|
04bab185f6 | ||
|
2213b4cf37 | ||
|
1d770e5636 | ||
|
b7e15e0a2c | ||
|
9c651ae913 | ||
|
a1bbaec71f | ||
|
3b3ca89483 | ||
|
b4e3bca6fa | ||
|
e09d5cb4ec | ||
|
cae353b9f6 | ||
|
edb5b3d711 | ||
|
667a0c41ed | ||
|
9daae9c705 | ||
|
2975acdc82 | ||
|
76dcbe3429 | ||
|
d8e2d464ad | ||
|
5f8bcb0c26 | ||
|
7ef8d6fa10 | ||
|
5924a40222 | ||
|
05968eb232 | ||
|
36dcfbfe2d | ||
|
95ce4f5c1e | ||
|
f258f20b04 | ||
|
7e2ad827aa | ||
|
e6ce61fdf0 | ||
|
3df588047d | ||
|
ac0e5cbb29 | ||
|
5ab584bc6a | ||
|
a2e03e3bd0 | ||
|
f0589b310f | ||
|
8519b0d353 | ||
|
21b8b2deb5 | ||
|
6b82a77e36 | ||
|
1954a49f37 | ||
|
0e3d1e1503 | ||
|
ebd77f314d | ||
|
749d833f65 | ||
|
0373cd6f97 | ||
|
1f3fc8a366 | ||
|
89c3930b28 | ||
|
29e1e9eef2 | ||
|
de3aeb9732 | ||
|
85aa1a444a | ||
|
702876ae7f | ||
|
7109910f46 | ||
|
8168d2fdc1 | ||
|
edbcd01fbc | ||
|
c99266e961 | ||
|
f804053736 | ||
|
2641832304 | ||
|
21f6f81914 | ||
|
ccd919aba3 | ||
|
2387010556 | ||
|
f35d574759 | ||
|
3be74bb275 | ||
|
b1be062437 | ||
|
2d0d320d05 | ||
|
1de5111ab5 | ||
|
3d530e4747 | ||
|
0ef1b7b683 | ||
|
66485e81b4 | ||
|
e74e7cf734 | ||
|
03ce6a1cc4 | ||
|
a19b93c966 | ||
|
f7fd1f2a63 | ||
|
88b71d23db | ||
|
762ef12eb6 | ||
|
6845068b82 | ||
|
5c0b18efe4 | ||
|
4b93d040b3 | ||
|
ff61cc971e | ||
|
46db91ce73 | ||
|
5921909ef5 | ||
|
1537861c61 | ||
|
1b93551572 | ||
|
197a5fbcf4 | ||
|
ff32529345 | ||
|
a179c3b399 | ||
|
a820585f56 | ||
|
bfb12f415c | ||
|
a731b43b52 | ||
|
118b4eb07a | ||
|
f1a05ab73c | ||
|
4c85a41bfb | ||
|
30e048d4ab | ||
|
aa0ab6d387 | ||
|
30b87985b7 | ||
|
df73211d56 | ||
|
e3a4ddcd08 | ||
|
0ea007b26f | ||
|
16bb9b6836 | ||
|
d2766b1b4f | ||
|
4802484729 | ||
|
c762b9bb2e | ||
|
5792a19b97 | ||
|
9699dc2a85 | ||
|
0fa0c2256a | ||
|
1b410980ca | ||
|
be0dbd62c1 | ||
|
1a411b658b | ||
|
d2e84a700f | ||
|
b9af55fc49 | ||
|
e0d92aed6d | ||
|
f94fa78565 | ||
|
007a1fc7f2 | ||
|
a3372acb6d | ||
|
af7c9b520f | ||
|
43a510c046 | ||
|
329c576f44 | ||
|
7afa33dfa1 | ||
|
73c6007730 | ||
|
526c19181e | ||
|
79cd306ac2 | ||
|
35b83678bd | ||
|
eacb6ea15a | ||
|
d88263dbf9 | ||
|
b1e3444798 | ||
|
f6c6d2bcd0 | ||
|
8d468925d3 | ||
|
f99363674b | ||
|
526a04d4c8 | ||
|
593c0e7ce2 | ||
|
e2b42ca57b | ||
|
7860534f0c | ||
|
fc81d92c88 | ||
|
8fbac2e39e | ||
|
b91ae71241 | ||
|
0a41cd43a5 | ||
|
59f7b2ea98 | ||
|
862957c30c | ||
|
4831890232 | ||
|
546f0173ab | ||
|
b001b0da86 | ||
|
04e3f2f401 | ||
|
3a2b421566 | ||
|
acc432b5a8 | ||
|
c4529820f2 | ||
|
d3edccb839 | ||
|
8380de1bd9 | ||
|
bf43149d7e | ||
|
13e2358815 | ||
|
1f6f8d5e0f | ||
|
716eca5976 | ||
|
9ae808aac4 | ||
|
c77fe6b434 | ||
|
f149b56063 | ||
|
831a3e384b | ||
|
49a9e2a9e0 | ||
|
a2db3e0499 | ||
|
422109b82f | ||
|
c864a7297b | ||
|
8da038041d | ||
|
dd954f3c0a | ||
|
6f81e3479a | ||
|
db483e9d34 | ||
|
700b7a1b51 | ||
|
ed65d00574 | ||
|
0306b5e8f7 | ||
|
cb54e414ed | ||
|
bad71d1a36 | ||
|
088b8fb348 | ||
|
e28ebf1c62 | ||
|
39eeb67d91 | ||
|
f460c1990e | ||
|
0c0949679f | ||
|
58d4481118 | ||
|
83381e99cf | ||
|
21e28ae848 | ||
|
31550fd2c9 | ||
|
7c7ee2ca61 | ||
|
ba046b4d3a | ||
|
d675d46930 | ||
|
7ea76929d4 | ||
|
5ef55dd8b4 | ||
|
d47c1a7975 | ||
|
8068057040 | ||
|
fcdeec0bfa | ||
|
b9d8eff994 | ||
|
529e34d2ae | ||
|
26b3fe201b | ||
|
f98c537ec2 | ||
|
083bde64ee | ||
|
462dcbcf03 | ||
|
45fe218ee2 | ||
|
d54777236c | ||
|
dafdaa4208 | ||
|
5212b7d3bd | ||
|
83a92596c3 | ||
|
4f3b06472b | ||
|
029fa83690 | ||
|
abdcb9e332 | ||
|
17e85e31cd | ||
|
7d3dd5a0e4 | ||
|
dd873fbeee | ||
|
38a4c80995 | ||
|
91fa727c74 | ||
|
794c0206f3 | ||
|
52bad03c8d | ||
|
2fde3e8679 | ||
|
1e71f52b72 | ||
|
2b1d2853cd | ||
|
6a92ac0b7b | ||
|
f07e8f58e6 | ||
|
7b19cb5631 | ||
|
f5adea1061 | ||
|
dbd173b4e4 | ||
|
85cfd87c44 | ||
|
c867f48f11 | ||
|
514f9a7215 | ||
|
0b0380b690 | ||
|
4d0c8c189a | ||
|
afe4c307f9 | ||
|
c0563f1a39 | ||
|
ce3a0fdd46 | ||
|
ce3c72e9d9 | ||
|
dcba74deb9 | ||
|
203a5c5c48 | ||
|
be4aeaacde | ||
|
04ebd9d46a | ||
|
52b4e93c38 | ||
|
58d6681824 | ||
|
c944d203fb | ||
|
62df067fac | ||
|
7c80b9a692 | ||
|
a4a8345a33 | ||
|
742dde72bb | ||
|
4497ddbb0e | ||
|
53388a3570 | ||
|
1c495d7ea4 | ||
|
4c0d6e211b | ||
|
5bfd6acd52 | ||
|
0b49de94c6 | ||
|
7c0e557f84 | ||
|
a81171d5f1 | ||
|
26dc2f4d61 | ||
|
d426126a92 | ||
|
6aac78fc36 | ||
|
f6c53f0450 | ||
|
54e09b98c7 | ||
|
395b1702de | ||
|
4eebaa1a80 | ||
|
cb9bf3ce68 | ||
|
ef4aa202d0 | ||
|
cc5ee00b89 | ||
|
49a8cb76f5 | ||
|
bf12306f17 | ||
|
fa1f4f761d | ||
|
b50aebd2ed | ||
|
323b8237a0 | ||
|
9f741abd84 | ||
|
32ccc26712 | ||
|
563a0bd274 | ||
|
a91080b060 | ||
|
039ccaf4f1 | ||
|
c878d262bf | ||
|
c8446c2dc8 | ||
|
4afb39778a | ||
|
751781a3b7 | ||
|
f5d150c3b4 | ||
|
ae9342208e | ||
|
3040d9df0d | ||
|
00e0571811 | ||
|
bfb07746fe | ||
|
171cda6186 | ||
|
4cc17e112f | ||
|
b6af61fa6e | ||
|
4e07d92190 | ||
|
fb4ba7af2b | ||
|
c134dcd6fe | ||
|
fc00e1c228 | ||
|
ae34486b57 | ||
|
d7b513e9aa | ||
|
d8297a055a | ||
|
5140bbe99a | ||
|
0c33d110f4 | ||
|
5b37fb83fd | ||
|
bc6879ecc1 | ||
|
17137ba3e7 | ||
|
e9d2124885 | ||
|
f1f2e1bf64 | ||
|
ced5aa5dc6 | ||
|
72bc74001f | ||
|
adfa3f795c | ||
|
89d90de7d8 | ||
|
fe426f6fb2 | ||
|
3e439cc39b | ||
|
56c0634918 | ||
|
bcadd68904 | ||
|
9790aa91fe | ||
|
5316b412d2 | ||
|
b5ee5c34f2 | ||
|
2618aef008 | ||
|
8239e04a19 | ||
|
709d50836b | ||
|
e2c5f3712f | ||
|
ee71b4bfef | ||
|
0d57e2aed9 | ||
|
30ffba78e6 | ||
|
8394549857 | ||
|
d0f3ad6024 | ||
|
870c0b5cf4 | ||
|
044d87d96d | ||
|
b60edd9ee9 | ||
|
b1ea36793b | ||
|
750878d668 | ||
|
617b8b20f0 | ||
|
d88554fa92 | ||
|
e74a20de24 | ||
|
7c227392fa | ||
|
8a697f7a39 | ||
|
8327dd0c0b | ||
|
60fd26e0b7 | ||
|
acd0c1bcd5 | ||
|
9b3750320b | ||
|
b9f1f7752d | ||
|
944008661f | ||
|
79ae52aca7 | ||
|
51390aa874 | ||
|
cfa1f47226 | ||
|
40b59da224 | ||
|
f7ed4a5805 | ||
|
3d47030349 | ||
|
34eb2e371e | ||
|
6573634012 | ||
|
61ecb4cd18 | ||
|
22bdbd2498 | ||
|
287fb78654 | ||
|
06d528a2bd | ||
|
1fe6a8b04d | ||
|
bd5cab6e87 | ||
|
238acd9330 | ||
|
8e7ac513b6 | ||
|
e56551d047 | ||
|
170fc13e02 | ||
|
97ce77169a | ||
|
c9b871a03a | ||
|
2fdefa258e | ||
|
f0a733d6d6 | ||
|
586b5714a7 | ||
|
6e23454202 | ||
|
5b24403c8e | ||
|
e83599dd08 | ||
|
de7dd068d9 | ||
|
a33476dea8 | ||
|
dceccbdb92 | ||
|
393651f5e2 | ||
|
5acee9e11d | ||
|
81626eef38 | ||
|
e60fbbbebe | ||
|
e45e63dc37 | ||
|
c3d5ad2eeb | ||
|
7c64f5d31e | ||
|
66f46c5b96 | ||
|
07a6d48a27 | ||
|
722ea28e3a | ||
|
f195ef27f3 | ||
|
7e5c258266 | ||
|
38b5aef208 | ||
|
a7e4ded722 | ||
|
22405a1259 | ||
|
d0a6689413 | ||
|
a1f47cb4db | ||
|
c884c7bb8a | ||
|
c042098889 | ||
|
571f41dcf0 | ||
|
f30ad20c9b | ||
|
cbd54470ba | ||
|
01e17b6c3e | ||
|
3e13ebec93 | ||
|
c84fb9895e | ||
|
23c1a9ca8e | ||
|
741c739ef1 | ||
|
52f16e11a8 | ||
|
5623a53464 | ||
|
c95393b238 | ||
|
be0dd71bb4 | ||
|
0ee6973e2f | ||
|
4819974a1c | ||
|
e8e8b41eed | ||
|
7d23d3c0a4 | ||
|
718fc7a79d | ||
|
bfd142b13b | ||
|
75533b2beb | ||
|
e3d1201b46 | ||
|
8f982ff1f2 | ||
|
0391e21c84 | ||
|
b8a1cb5c68 | ||
|
7a71cd3012 | ||
|
26bedced35 | ||
|
c1aefb8ad8 | ||
|
576e87f398 | ||
|
b4f6bf0f6a | ||
|
edc55aad3c | ||
|
38a3fe4316 | ||
|
81e3b2dd4c | ||
|
4524cdc151 | ||
|
9a7821b8fa | ||
|
e8333883df | ||
|
aeffe1036d | ||
|
987e8a93bd | ||
|
2cb4acd6cc | ||
|
1e44e339ad | ||
|
59549d5f39 | ||
|
4a7297d05c | ||
|
a5335667bb | ||
|
498b806ca9 | ||
|
dd7a8a9a87 | ||
|
133aa77c21 | ||
|
942614dd23 | ||
|
c30ebe5f90 | ||
|
50757b5e99 | ||
|
42b900b9b2 | ||
|
c26b9b1a5d | ||
|
9ee642a7db | ||
|
423385bca0 | ||
|
6e5f7650a5 | ||
|
89a79d0f1b | ||
|
9e41485ff1 | ||
|
3c7c6c4d9f | ||
|
cd1b3904da | ||
|
b23b2611b3 | ||
|
877770f7cf | ||
|
3142a4f4b3 | ||
|
b4dc96527d | ||
|
35b5ca4c63 | ||
|
daf3023b02 | ||
|
705f3f1372 | ||
|
f6520727a3 | ||
|
b17d5b80b8 | ||
|
48b4eb5c0d | ||
|
7ecd6d20ba | ||
|
bddad57a7b | ||
|
799136a714 | ||
|
350d61b4a6 | ||
|
b6f5a66fab | ||
|
b0c12e2422 | ||
|
623a7dc7e6 | ||
|
709c7e5707 | ||
|
5f6c5025d5 | ||
|
328be161d6 | ||
|
ee04f52a16 | ||
|
c446c291d9 | ||
|
c66d9de759 | ||
|
260ee980e0 | ||
|
7d98c1c4e0 | ||
|
4387cf38d7 | ||
|
a9d38570ab | ||
|
0e619369fd | ||
|
6890dc1844 | ||
|
cda09c843a | ||
|
e2190bd9d5 | ||
|
0472d19bd4 | ||
|
07524f5c99 | ||
|
1710800cc0 | ||
|
c705d6f9b3 | ||
|
be718aea11 | ||
|
ca680710a2 | ||
|
5f71a43758 | ||
|
04dd63da1c | ||
|
cee022b935 | ||
|
ae2ae85070 | ||
|
ce6bbbaa33 | ||
|
6333bfe6e8 | ||
|
41d8863d2f | ||
|
523b7f96f8 | ||
|
ab1a930705 | ||
|
dc74f76a03 | ||
|
3a99c86cb3 | ||
|
d6ad7e2e64 | ||
|
aaf120f263 | ||
|
c228e73b26 | ||
|
e27e65eb76 | ||
|
1c8acf3929 | ||
|
40b3c17703 | ||
|
e042ef3f27 | ||
|
313357a6b3 | ||
|
37a1aaad64 | ||
|
f084d2a28b | ||
|
077b39d7c6 | ||
|
7081f3df58 | ||
|
9fe6a0a894 | ||
|
3d452fd5b9 | ||
|
47a5cfbd3e | ||
|
4cb6241e93 | ||
|
b572879691 | ||
|
ad07a6ab2b | ||
|
4bdeb33ac1 | ||
|
101a4d0d8d | ||
|
89e07d0c55 | ||
|
39c1cc1b3c | ||
|
9f6f637527 | ||
|
0f09551a76 | ||
|
8cd72cfc1b | ||
|
7a141c8616 | ||
|
0ca65f955d | ||
|
011b748a55 | ||
|
f6181ef3e2 | ||
|
24368747ab | ||
|
66591cf216 | ||
|
1feeeb2eec | ||
|
419d46c958 | ||
|
7063da1c7d | ||
|
bee8ebb00b | ||
|
da5e4a13bf | ||
|
5dc1ec68a3 | ||
|
3d2e5ebe39 | ||
|
f5130db6b0 | ||
|
676b79db42 | ||
|
6d2f4a0813 | ||
|
4b91204686 | ||
|
7ddefcef72 | ||
|
0f3e42d463 | ||
|
c9129b8ecf | ||
|
a6955ecf59 | ||
|
6619a787a3 | ||
|
aae17c817b | ||
|
ab87bad952 | ||
|
be306d651e | ||
|
8fe5c22075 | ||
|
05a9350e57 | ||
|
7ed4ae2f8c | ||
|
5d6384e101 | ||
|
1a4564d998 | ||
|
66e489addb | ||
|
cdab6b1796 | ||
|
722f299306 | ||
|
66be04f39e | ||
|
8719f2836e | ||
|
0c702b0b6b | ||
|
6fcab72ec7 | ||
|
77b111702b | ||
|
96a7cc483f | ||
|
1e3506848a | ||
|
5ee2cae85c | ||
|
5c119fe2d6 | ||
|
d55115844a | ||
|
4f4491c247 | ||
|
1691f586d7 | ||
|
04dfe0de84 | ||
|
27d1b46835 | ||
|
2f62ec3632 | ||
|
384488ac02 | ||
|
c469e669fd | ||
|
56affb90ae | ||
|
f6aa147c78 | ||
|
9bd0fff319 | ||
|
00d7c5972f | ||
|
58a438167b | ||
|
e3131481e9 | ||
|
bc8d68bd31 | ||
|
07c6e33598 | ||
|
70812c70fc | ||
|
d89b234cad | ||
|
2070aa9443 | ||
|
91ff94ea56 | ||
|
0347537f43 | ||
|
db9b18f121 | ||
|
ee70001be3 | ||
|
972eea97fe | ||
|
2b4d33e919 | ||
|
fc4d670c88 | ||
|
02035d4942 | ||
|
93a46089ce | ||
|
e8d63b2a3b | ||
|
d3c7681bc5 | ||
|
dc66db4abe | ||
|
a0e1cf8376 | ||
|
5292b84f4f | ||
|
b27455a36f | ||
|
5042c5bf40 | ||
|
da7b6f0baf | ||
|
9b5845f1cb | ||
|
e8633d17e8 | ||
|
d1d8b01dfb | ||
|
7c4353a0ac | ||
|
1b2cb53d4f | ||
|
3158e51c62 | ||
|
a0c72cdf00 | ||
|
f0371da838 | ||
|
44b82e6231 | ||
|
04f0bf3070 | ||
|
7400c39511 | ||
|
008a5af6d6 | ||
|
35ca40c3de | ||
|
de821fc305 | ||
|
e3cac7d0e5 | ||
|
81f7aa9df2 | ||
|
6bce298d90 | ||
|
afbad56012 | ||
|
d973096464 | ||
|
7192aa86b5 | ||
|
9c8df8b9ce | ||
|
ff4c7b82bc | ||
|
47ff51e640 | ||
|
08503655d9 | ||
|
3afd6024b5 | ||
|
aa308b7a3a | ||
|
9598f646f5 | ||
|
8af39bdaf7 | ||
|
914f3d1fa3 | ||
|
8cb3f0835a | ||
|
cba0898e4f | ||
|
8d158402f3 | ||
|
7f2582e3b6 | ||
|
dbc796359f | ||
|
4d1285d8e5 | ||
|
871d097b30 | ||
|
1532033a7f | ||
|
9faae7387e | ||
|
a5c644e719 | ||
|
7a2ce59563 | ||
|
14cec7e610 | ||
|
6287a3dd53 | ||
|
93a1db77c5 | ||
|
a9d4b09bdb | ||
|
ed2eb7b5a6 | ||
|
18d8537d29 | ||
|
72f3b1ed39 | ||
|
fd70e6edb1 | ||
|
5a578c5375 | ||
|
9db8773055 | ||
|
8a67434380 | ||
|
c94e5f3589 | ||
|
adef7200f6 | ||
|
cf508b6d48 | ||
|
f8d36fda28 | ||
|
4fe9cc7730 | ||
|
758b7f875b | ||
|
0b97a67cfa | ||
|
ec5976bbc9 | ||
|
5cc49e2931 | ||
|
b6752a2c02 | ||
|
d41e28fc36 | ||
|
64c52a6921 | ||
|
691a678b19 | ||
|
1ba7fd91ff | ||
|
1c98a9ad3e | ||
|
dd23ceeead | ||
|
058fa1367b | ||
|
9db12374ea | ||
|
fc550ac1fc | ||
|
d6ef8ec3d1 | ||
|
837db9a2d9 | ||
|
a941739f8a | ||
|
795a346006 | ||
|
9d00da7285 | ||
|
52c1909f24 | ||
|
2cbf9cae71 | ||
|
f9225c54ff | ||
|
cb05f36976 | ||
|
49e0e20ce2 | ||
|
7c35337999 | ||
|
2296aab5a8 | ||
|
ce3b255f1a | ||
|
3942f3366d | ||
|
df76cc33a5 | ||
|
cf387d5a6d | ||
|
0a0cf87625 | ||
|
1a2544610d | ||
|
5229b7cfba | ||
|
243b45881d | ||
|
883028d981 | ||
|
bdeb7bfb9f | ||
|
808ffb0491 | ||
|
5305a16350 | ||
|
63b581935d | ||
|
c7c9349b00 | ||
|
d54417acfe | ||
|
9fba37b409 | ||
|
6d28c52f59 | ||
|
f80a6ef2a6 | ||
|
ecf31097ea | ||
|
16fc3675db | ||
|
651d993d9c | ||
|
03eb5139a2 | ||
|
286d882f1e | ||
|
3b6afdf80c | ||
|
c19cce69fa | ||
|
5c4931e235 | ||
|
b705e64a8a | ||
|
7fd1eb3780 | ||
|
8c5514612f | ||
|
924e82ab0c | ||
|
adcb99d330 | ||
|
8339139400 | ||
|
a43cf8d2b8 | ||
|
2b863d9bc2 | ||
|
9ce4f94818 | ||
|
5157a6ad47 | ||
|
cd6c58a372 | ||
|
03ba8396f3 | ||
|
b0a0e16136 | ||
|
732d73dd43 | ||
|
e075dfe911 | ||
|
425b53585a | ||
|
d5bbb103d4 | ||
|
5c2849ea07 | ||
|
723418e2cc | ||
|
45e2e8baec | ||
|
b0ae6bc049 | ||
|
ffb53c07b8 | ||
|
f329b3b51d | ||
|
5b27aba3e1 | ||
|
7c2ba62b56 | ||
|
24862402e5 | ||
|
d568d2f55a | ||
|
dae7e7a80a | ||
|
23cdb37165 | ||
|
2c82dfd444 | ||
|
c8c31aea62 | ||
|
89b0037ec1 | ||
|
b75fb23887 | ||
|
52b69fbcb8 | ||
|
f16219f90a | ||
|
7b0cef0fac | ||
|
e0af17a17a | ||
|
92fb86b66f | ||
|
919295cffc | ||
|
086a85d2f0 | ||
|
8235cd3645 | ||
|
f1a257abf8 | ||
|
98dfd2ba0e | ||
|
87e6285cf6 | ||
|
0d56a98836 | ||
|
8105f1c379 | ||
|
e6c2040ea8 | ||
|
c1b5b740ff | ||
|
1d2d0cefaa | ||
|
04e65958ee | ||
|
8765494cbd | ||
|
05665f4eec | ||
|
396449c07f | ||
|
eda679776e | ||
|
69d57d602f | ||
|
32b2736efd | ||
|
3f650bbd11 | ||
|
5313922bb7 | ||
|
ec3e2c08b8 | ||
|
0367034f93 | ||
|
b80ecd51a7 | ||
|
14a0d66410 | ||
|
d84ccbc52a | ||
|
1190768f4b | ||
|
ea3510d1f3 | ||
|
3f76f73e8c | ||
|
759c269dee | ||
|
c360395afc | ||
|
60a35c8aba | ||
|
50dd2b8cff | ||
|
4e5fcac9cb | ||
|
64b8fc52c3 | ||
|
19a5ba3264 | ||
|
7ff6c32452 | ||
|
ff11467022 | ||
|
7d3878214a | ||
|
984817d3a0 | ||
|
6b133e24b9 | ||
|
990ee89650 | ||
|
8071f31721 | ||
|
d456c2ce6a | ||
|
94f922cd28 | ||
|
29390a3c4a | ||
|
765c44d77f | ||
|
64ee68763b | ||
|
4122aef12e | ||
|
8cb44598c0 | ||
|
69c628b626 | ||
|
cd28e7b24f | ||
|
40d9058bb6 | ||
|
c36e0b3b06 | ||
|
3174fb8861 | ||
|
074b31b5e9 | ||
|
16609cd485 | ||
|
70ab34cfb8 | ||
|
c53be185f4 | ||
|
779eeba650 | ||
|
58ffea6627 | ||
|
a2d68ed881 | ||
|
d653a348b1 | ||
|
2e84b1e556 | ||
|
bbb133d94c | ||
|
d90fa5ab3e | ||
|
759a19bc4f | ||
|
a7ec785994 | ||
|
46faa7a745 | ||
|
54e3f08833 | ||
|
b365836c57 | ||
|
242f1b9c3c | ||
|
4dfbb6d489 | ||
|
c31b4c55c2 | ||
|
ca5bbab20a | ||
|
41dd124a4b | ||
|
dbf6161fa1 | ||
|
7aabd6e385 | ||
|
cb203f8e7e | ||
|
8f845bac74 | ||
|
a89eb122a0 | ||
|
b7daa2f3a4 | ||
|
7d178f49b4 | ||
|
85f4f26942 | ||
|
eee8ba8a53 | ||
|
22aceec426 | ||
|
121c057b90 | ||
|
2c976227dd | ||
|
81d011e57d | ||
|
3776e58041 | ||
|
36c88111de | ||
|
2201dcd505 | ||
|
7a7cafcbaa | ||
|
efb671401d | ||
|
4128c1ac8d | ||
|
73e10c96cc | ||
|
0055965295 | ||
|
34e60a8404 | ||
|
ceec81011b | ||
|
927003329e | ||
|
01bb0a80ab | ||
|
db1baf80a9 | ||
|
9cb07d026f | ||
|
984ea1040f | ||
|
447109e868 | ||
|
f79317a435 | ||
|
131d8dd765 | ||
|
b452695c20 | ||
|
f17785c3ab | ||
|
2a578748fd | ||
|
2ddae2e856 | ||
|
885b9f371c | ||
|
f275e4ad3c | ||
|
aea7bc0c07 | ||
|
a457392ec3 | ||
|
37ec7d0505 | ||
|
8f6404ab3a | ||
|
1538b16b21 | ||
|
a6477fbd95 | ||
|
e802dcd189 | ||
|
931dc02c09 | ||
|
7017cdcf49 | ||
|
5aa017d9b5 | ||
|
a7297b49a4 | ||
|
3eaeb81831 | ||
|
7d6c778211 | ||
|
9c27a98821 | ||
|
ad54c5a278 | ||
|
96939e2990 | ||
|
5268db47a1 | ||
|
3048509807 | ||
|
7399a83c74 | ||
|
18c3d8dc62 | ||
|
2d1ddcf28b | ||
|
a1a0420314 | ||
|
2223587fc0 | ||
|
63f9bccf9f | ||
|
18d11e02d0 | ||
|
a71d69cc3c | ||
|
e007bb7546 | ||
|
7874ffd506 | ||
|
a9216e24f5 | ||
|
39388a2199 | ||
|
71111708d4 | ||
|
d5efc99876 | ||
|
1e84e77a67 | ||
|
d6b448f430 | ||
|
e426b27581 | ||
|
b6c5c14447 | ||
|
cbccdd51c5 | ||
|
994e135368 | ||
|
87e5cda506 | ||
|
2833d68f15 | ||
|
64e8b31d49 | ||
|
2643271053 | ||
|
5b36b274a3 | ||
|
8ad31d6eb4 | ||
|
13e8a875cf | ||
|
c7281df230 | ||
|
5f0b215e90 | ||
|
55f610422a | ||
|
a04ef15bcd | ||
|
81754840ff | ||
|
2610023131 | ||
|
ff3481f06b | ||
|
f8ea19d29c | ||
|
3b8ebf7d33 | ||
|
5e14f20786 | ||
|
96b19deac5 | ||
|
a6aff7c85c | ||
|
1310347395 | ||
|
40c94d80d7 | ||
|
921a704c24 | ||
|
3f490f95c6 | ||
|
24d80b1909 | ||
|
78b2fba033 | ||
|
218b76275c | ||
|
cf5b6d837f | ||
|
0babc7bb64 | ||
|
8a551d91fd | ||
|
eeed035ef0 | ||
|
33404a7772 | ||
|
bd90745528 | ||
|
ede1212cb0 | ||
|
2dcbc01e51 | ||
|
61ba50fac9 | ||
|
b24b5e20b4 | ||
|
3112432480 | ||
|
94f5b0d9ff | ||
|
d2c8824902 | ||
|
db09007dbc | ||
|
5b2e8990f1 | ||
|
2f6068decc | ||
|
1e591dd188 | ||
|
6838a81e50 | ||
|
ceef5e39b7 | ||
|
ef339af623 | ||
|
acc7865542 | ||
|
c00c240c14 |
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# vendor/github.com/go-acme/lego/providers/dns/cloudxns/cloudxns.go eol=crlf
|
24
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
provider/kubernetes/** @containous/kubernetes
|
||||||
|
provider/rancher/** @containous/rancher
|
||||||
|
provider/marathon/** @containous/marathon
|
||||||
|
provider/docker/** @containous/docker
|
||||||
|
|
||||||
|
docs/user-guide/kubernetes.md @containous/kubernetes
|
||||||
|
docs/user-guide/marathon.md @containous/marathon
|
||||||
|
docs/user-guide/swarm.md @containous/docker
|
||||||
|
docs/user-guide/swarm-mode.md @containous/docker
|
||||||
|
|
||||||
|
docs/configuration/backends/docker.md @containous/docker
|
||||||
|
docs/configuration/backends/kubernetes.md @containous/kubernetes
|
||||||
|
docs/configuration/backends/marathon.md @containous/marathon
|
||||||
|
docs/configuration/backends/rancher.md @containous/rancher
|
||||||
|
|
||||||
|
examples/k8s/ @containous/kubernetes
|
||||||
|
examples/compose-k8s.yaml @containous/kubernetes
|
||||||
|
examples/k8s.namespace.yaml @containous/kubernetes
|
||||||
|
examples/compose-rancher.yml @containous/rancher
|
||||||
|
examples/compose-marathon.yml @containous/marathon
|
||||||
|
|
||||||
|
vendor/github.com/gambol99/go-marathon @containous/marathon
|
||||||
|
vendor/github.com/rancher @containous/rancher
|
||||||
|
vendor/k8s.io/ @containous/kubernetes
|
150
.github/CONTRIBUTING.md
vendored
@@ -1,150 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
You need either [Docker](https://github.com/docker/docker) and `make` (Method 1), or `go` (Method 2) in order to build traefik. For changes to its dependencies, the `glide` dependency management tool and `glide-vc` plugin are required.
|
|
||||||
|
|
||||||
#### Method 1: Using `Docker` and `Makefile`
|
|
||||||
|
|
||||||
You need to run the `binary` target. This will create binaries for Linux platform in the `dist` folder.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ make binary
|
|
||||||
docker build -t "traefik-dev:no-more-godep-ever" -f build.Dockerfile .
|
|
||||||
Sending build context to Docker daemon 295.3 MB
|
|
||||||
Step 0 : FROM golang:1.7
|
|
||||||
---> 8c6473912976
|
|
||||||
Step 1 : RUN go get github.com/Masterminds/glide
|
|
||||||
[...]
|
|
||||||
docker run --rm -v "/var/run/docker.sock:/var/run/docker.sock" -it -e OS_ARCH_ARG -e OS_PLATFORM_ARG -e TESTFLAGS -v "/home/emile/dev/go/src/github.com/containous/traefik/"dist":/go/src/github.com/containous/traefik/"dist"" "traefik-dev:no-more-godep-ever" ./script/make.sh generate binary
|
|
||||||
---> Making bundle: generate (in .)
|
|
||||||
removed 'gen.go'
|
|
||||||
|
|
||||||
---> Making bundle: binary (in .)
|
|
||||||
|
|
||||||
$ ls dist/
|
|
||||||
traefik*
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Method 2: Using `go`
|
|
||||||
|
|
||||||
###### Setting up your `go` environment
|
|
||||||
|
|
||||||
- You need `go` v1.7+
|
|
||||||
- It is recommended you clone Træfik into a directory like `~/go/src/github.com/containous/traefik` (This is the official golang workspace hierarchy, and will allow dependencies to resolve properly)
|
|
||||||
- This will allow your `GOPATH` and `PATH` variable to be set to `~/go` via:
|
|
||||||
```bash
|
|
||||||
$ export GOPATH=~/go
|
|
||||||
$ export PATH=$PATH:$GOPATH/bin
|
|
||||||
```
|
|
||||||
|
|
||||||
This can be verified via `$ go env`
|
|
||||||
- You will want to add those 2 export lines to your `.bashrc` or `.bash_profile`
|
|
||||||
- You need `go-bindata` to be able to use `go generate` command (needed to build) : `$ go get github.com/jteeuwen/go-bindata/...` (Please note, the ellipses are required)
|
|
||||||
|
|
||||||
#### Setting up `glide` and `glide-vc` for dependency management
|
|
||||||
|
|
||||||
- Glide is not required for building; however, it is necessary to modify dependencies (i.e., add, update, or remove third-party packages)
|
|
||||||
- Glide can be installed either via homebrew: `$ brew install glide` or via the official glide script: `$ curl https://glide.sh/get | sh`
|
|
||||||
- The glide plugin `glide-vc` must be installed from source: `go get github.com/sgotti/glide-vc`
|
|
||||||
|
|
||||||
If you want to add a dependency, use `$ glide get` to have glide put it into the vendor folder and update the glide manifest/lock files (`glide.yaml` and `glide.lock`, respectively). A following `glide-vc` run should be triggered to trim down the size of the vendor folder. The final result must be committed into VCS.
|
|
||||||
|
|
||||||
Dependencies for the integration tests in the `integration` folder are managed in a separate `integration/glide.yaml` file using the same toolset.
|
|
||||||
|
|
||||||
Care must be taken to choose the right arguments to `glide` when dealing with either main or integration test dependencies, or otherwise risk ending up with a broken build. For that reason, the helper script `script/glide.sh` encapsulates the gory details and conveniently calls `glide-vc` as well. Call it without parameters for basic usage instructions.
|
|
||||||
|
|
||||||
Here's a full example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# install the new main dependency github.com/foo/bar and minimize vendor size
|
|
||||||
$ ./script/glide.sh get github.com/foo/bar
|
|
||||||
# install another dependency, this time for the integration tests
|
|
||||||
$ ( cd integration && ../script/glide.sh get github.com/baz/quuz )
|
|
||||||
# generate (Only required to integrate other components such as web dashboard)
|
|
||||||
$ go generate
|
|
||||||
# Standard go build
|
|
||||||
$ go build
|
|
||||||
# Using gox to build multiple platform
|
|
||||||
$ gox "linux darwin" "386 amd64 arm" \
|
|
||||||
-output="dist/traefik_{{.OS}}-{{.Arch}}" \
|
|
||||||
./cmd/traefik
|
|
||||||
# run other commands like tests
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
##### Method 1: `Docker` and `make`
|
|
||||||
|
|
||||||
You can run unit tests using the `test-unit` target and the
|
|
||||||
integration test using the `test-integration` target.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ make test-unit
|
|
||||||
docker build -t "traefik-dev:your-feature-branch" -f build.Dockerfile .
|
|
||||||
# […]
|
|
||||||
docker run --rm -it -e OS_ARCH_ARG -e OS_PLATFORM_ARG -e TESTFLAGS -v "/home/vincent/src/github/vdemeester/traefik/dist:/go/src/github.com/containous/traefik/dist" "traefik-dev:your-feature-branch" ./script/make.sh generate test-unit
|
|
||||||
---> Making bundle: generate (in .)
|
|
||||||
removed 'gen.go'
|
|
||||||
|
|
||||||
---> Making bundle: test-unit (in .)
|
|
||||||
+ go test -cover -coverprofile=cover.out .
|
|
||||||
ok github.com/containous/traefik 0.005s coverage: 4.1% of statements
|
|
||||||
|
|
||||||
Test success
|
|
||||||
```
|
|
||||||
|
|
||||||
For development purposes, you can specify which tests to run by using:
|
|
||||||
```bash
|
|
||||||
# Run every tests in the MyTest suite
|
|
||||||
TESTFLAGS="-check.f MyTestSuite" make test-integration
|
|
||||||
|
|
||||||
# Run the test "MyTest" in the MyTest suite
|
|
||||||
TESTFLAGS="-check.f MyTestSuite.MyTest" make test-integration
|
|
||||||
|
|
||||||
# Run every tests starting with "My", in the MyTest suite
|
|
||||||
TESTFLAGS="-check.f MyTestSuite.My" make test-integration
|
|
||||||
|
|
||||||
# Run every tests ending with "Test", in the MyTest suite
|
|
||||||
TESTFLAGS="-check.f MyTestSuite.*Test" make test-integration
|
|
||||||
```
|
|
||||||
|
|
||||||
More: https://labix.org/gocheck
|
|
||||||
|
|
||||||
##### Method 2: `go`
|
|
||||||
|
|
||||||
- Tests can be run from the cloned directory, by `$ go test ./...` which should return `ok` similar to:
|
|
||||||
```
|
|
||||||
ok _/home/vincent/src/github/vdemeester/traefik 0.004s
|
|
||||||
```
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
The [documentation site](http://docs.traefik.io/) is built with [mkdocs](http://mkdocs.org/)
|
|
||||||
|
|
||||||
First make sure you have python and pip installed
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ python --version
|
|
||||||
Python 2.7.2
|
|
||||||
$ pip --version
|
|
||||||
pip 1.5.2
|
|
||||||
```
|
|
||||||
|
|
||||||
Then install mkdocs with pip
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ pip install mkdocs
|
|
||||||
```
|
|
||||||
|
|
||||||
To test documentation locally run `mkdocs serve` in the root directory, this should start a server locally to preview your changes.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ mkdocs serve
|
|
||||||
INFO - Building documentation...
|
|
||||||
WARNING - Config value: 'theme'. Warning: The theme 'united' will be removed in an upcoming MkDocs release. See http://www.mkdocs.org/about/release-notes/ for more details
|
|
||||||
INFO - Cleaning site directory
|
|
||||||
[I 160505 22:31:24 server:281] Serving on http://127.0.0.1:8000
|
|
||||||
[I 160505 22:31:24 handlers:59] Start watching changes
|
|
||||||
[I 160505 22:31:24 handlers:61] Start detecting changes
|
|
||||||
```
|
|
58
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,32 +1,46 @@
|
|||||||
<!--
|
<!--
|
||||||
PLEASE READ THIS MESSAGE.
|
DO NOT FILE ISSUES FOR GENERAL SUPPORT QUESTIONS.
|
||||||
|
|
||||||
Please keep in mind that the GitHub issue tracker is not intended as a general support forum, but for reporting bugs and feature requests.
|
The issue tracker is for reporting bugs and feature requests only.
|
||||||
|
For end-user related support questions, please refer to one of the following:
|
||||||
|
|
||||||
For other type of questions, consider using one of:
|
- the Traefik community forum: https://community.containo.us/
|
||||||
|
|
||||||
- the Traefik community Slack channel: https://traefik.herokuapp.com
|
-->
|
||||||
- StackOverflow: https://stackoverflow.com/questions/tagged/traefik
|
|
||||||
|
|
||||||
|
### Do you want to request a *feature* or report a *bug*?
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If you intend to ask a support question: DO NOT FILE AN ISSUE.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Did you try using a 1.7.x configuration for the version 2.0?
|
||||||
|
|
||||||
|
- [ ] Yes
|
||||||
|
- [ ] No
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
If you just checked the "Yes" box, be aware that this is probably not a bug. The configurations between 1.X and 2.X are NOT compatible. Please have a look here https://docs.traefik.io/v2.0/getting-started/configuration-overview/.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
### What did you do?
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
HOW TO WRITE A GOOD ISSUE?
|
HOW TO WRITE A GOOD ISSUE?
|
||||||
|
|
||||||
- if it's possible use the command `traefik bug`. See https://www.youtube.com/watch?v=Lyz62L8m93I.
|
- Respect the issue template as much as possible.
|
||||||
- The title must be short and descriptive.
|
- The title should be short and descriptive.
|
||||||
- Explain the conditions which led you to write this issue: the context.
|
- Explain the conditions which led you to report this issue: the context.
|
||||||
- The context should lead to something, an idea or a problem that you’re facing.
|
- The context should lead to something, an idea or a problem that you’re facing.
|
||||||
- Remain clear and concise.
|
- Remain clear and concise.
|
||||||
- Format your messages to help the reader focus on what matters and understand the structure of your message, use Markdown syntax https://help.github.com/articles/github-flavored-markdown
|
- Format your messages to help the reader focus on what matters and understand the structure of your message, use Markdown syntax https://help.github.com/articles/github-flavored-markdown
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Do you want to request a *feature* or report a *bug*?
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### What did you do?
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### What did you expect to see?
|
### What did you expect to see?
|
||||||
|
|
||||||
|
|
||||||
@@ -37,6 +51,16 @@ HOW TO WRITE A GOOD ISSUE?
|
|||||||
|
|
||||||
### Output of `traefik version`: (_What version of Traefik are you using?_)
|
### Output of `traefik version`: (_What version of Traefik are you using?_)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
For the Traefik Docker image:
|
||||||
|
docker run [IMAGE] version
|
||||||
|
ex: docker run traefik version
|
||||||
|
|
||||||
|
For the alpine Traefik Docker image:
|
||||||
|
docker run [IMAGE] traefik version
|
||||||
|
ex: docker run traefik traefik version
|
||||||
|
-->
|
||||||
|
|
||||||
```
|
```
|
||||||
(paste your output here)
|
(paste your output here)
|
||||||
```
|
```
|
||||||
@@ -51,7 +75,7 @@ Add more configuration information here.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
### If applicable, please paste the log output in debug mode (`--debug` switch)
|
### If applicable, please paste the log output at DEBUG level (`--log.level=DEBUG` switch)
|
||||||
|
|
||||||
```
|
```
|
||||||
(paste your output here)
|
(paste your output here)
|
||||||
|
87
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
DO NOT FILE ISSUES FOR GENERAL SUPPORT QUESTIONS.
|
||||||
|
|
||||||
|
The issue tracker is for reporting bugs and feature requests only.
|
||||||
|
For end-user related support questions, please refer to one of the following:
|
||||||
|
|
||||||
|
- the Traefik community forum: https://community.containo.us/
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
### Do you want to request a *feature* or report a *bug*?
|
||||||
|
|
||||||
|
Bug
|
||||||
|
|
||||||
|
### Did you try using a 1.7.x configuration for the version 2.0?
|
||||||
|
|
||||||
|
- [ ] Yes
|
||||||
|
- [ ] No
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
If you just checked the "Yes" box, be aware that this is probably not a bug. The configurations between 1.X and 2.X are NOT compatible. Please have a look here https://docs.traefik.io/v2.0/getting-started/configuration-overview/.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
### What did you do?
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
HOW TO WRITE A GOOD BUG REPORT?
|
||||||
|
|
||||||
|
- Respect the issue template as much as possible.
|
||||||
|
- The title should be short and descriptive.
|
||||||
|
- Explain the conditions which led you to report this issue: the context.
|
||||||
|
- The context should lead to something, an idea or a problem that you’re facing.
|
||||||
|
- Remain clear and concise.
|
||||||
|
- Format your messages to help the reader focus on what matters and understand the structure of your message, use Markdown syntax https://help.github.com/articles/github-flavored-markdown
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
### What did you expect to see?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### What did you see instead?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Output of `traefik version`: (_What version of Traefik are you using?_)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
For the Traefik Docker image:
|
||||||
|
docker run [IMAGE] version
|
||||||
|
ex: docker run traefik version
|
||||||
|
|
||||||
|
For the alpine Traefik Docker image:
|
||||||
|
docker run [IMAGE] traefik version
|
||||||
|
ex: docker run traefik traefik version
|
||||||
|
-->
|
||||||
|
|
||||||
|
```
|
||||||
|
(paste your output here)
|
||||||
|
```
|
||||||
|
|
||||||
|
### What is your environment & configuration (arguments, toml, provider, platform, ...)?
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# (paste your configuration here)
|
||||||
|
```
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Add more configuration information here.
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
### If applicable, please paste the log output in DEBUG level (`--log.level=DEBUG` switch)
|
||||||
|
|
||||||
|
```
|
||||||
|
(paste your output here)
|
||||||
|
```
|
35
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
DO NOT FILE ISSUES FOR GENERAL SUPPORT QUESTIONS.
|
||||||
|
|
||||||
|
The issue tracker is for reporting bugs and feature requests only.
|
||||||
|
For end-user related support questions, please refer to one of the following:
|
||||||
|
|
||||||
|
- the Traefik community forum: https://community.containo.us/
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
### Do you want to request a *feature* or report a *bug*?
|
||||||
|
|
||||||
|
Feature
|
||||||
|
|
||||||
|
### What did you expect to see?
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
HOW TO WRITE A GOOD ISSUE?
|
||||||
|
|
||||||
|
- Respect the issue template as much as possible.
|
||||||
|
- The title should be short and descriptive.
|
||||||
|
- Explain the conditions which led you to report this issue: the context.
|
||||||
|
- The context should lead to something, an idea or a problem that you’re facing.
|
||||||
|
- Remain clear and concise.
|
||||||
|
- Format your messages to help the reader focus on what matters and understand the structure of your message, use Markdown syntax https://help.github.com/articles/github-flavored-markdown
|
||||||
|
|
||||||
|
-->
|
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -12,12 +12,25 @@ HOW TO WRITE A GOOD PULL REQUEST?
|
|||||||
- Write useful descriptions and titles.
|
- Write useful descriptions and titles.
|
||||||
- Address review comments in terms of additional commits.
|
- Address review comments in terms of additional commits.
|
||||||
- Do not amend/squash existing ones unless the PR is trivial.
|
- Do not amend/squash existing ones unless the PR is trivial.
|
||||||
- Read the contributing guide: https://github.com/containous/traefik/blob/master/.github/CONTRIBUTING.md.
|
- Read the contributing guide: https://github.com/containous/traefik/blob/master/CONTRIBUTING.md.
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Description
|
### What does this PR do?
|
||||||
|
|
||||||
<!--
|
<!-- A brief description of the change being made with this pull request. -->
|
||||||
Briefly describe the pull request in a few paragraphs.
|
|
||||||
-->
|
|
||||||
|
### Motivation
|
||||||
|
|
||||||
|
<!-- What inspired you to submit this pull request? -->
|
||||||
|
|
||||||
|
|
||||||
|
### More
|
||||||
|
|
||||||
|
- [ ] Added/updated tests
|
||||||
|
- [ ] Added/updated documentation
|
||||||
|
|
||||||
|
### Additional Notes
|
||||||
|
|
||||||
|
<!-- Anything else we should know when reviewing? -->
|
||||||
|
7
.github/PULL_REQUEST_TEMPLATE/mergeback.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
### What does this PR do?
|
||||||
|
|
||||||
|
Merge v{{.Version}} into master
|
||||||
|
|
||||||
|
### Motivation
|
||||||
|
|
||||||
|
Be sync.
|
7
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
### What does this PR do?
|
||||||
|
|
||||||
|
Prepare release v{{.Version}}.
|
||||||
|
|
||||||
|
### Motivation
|
||||||
|
|
||||||
|
Create a new release.
|
26
.github/cpr.sh
vendored
@@ -1,26 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# git config --global alias.cpr '!sh .github/cpr.sh'
|
|
||||||
|
|
||||||
set -e # stop on error
|
|
||||||
|
|
||||||
usage="$(basename "$0") pr -- Checkout a Pull Request locally"
|
|
||||||
|
|
||||||
if [ "$#" -ne 1 ]; then
|
|
||||||
echo "Illegal number of parameters"
|
|
||||||
echo "$usage" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
command -v jq >/dev/null 2>&1 || { echo "I require jq but it's not installed. Aborting." >&2; exit 1; }
|
|
||||||
|
|
||||||
set -x # echo on
|
|
||||||
|
|
||||||
initial=$(git rev-parse --abbrev-ref HEAD)
|
|
||||||
pr=$1
|
|
||||||
remote=$(curl -s https://api.github.com/repos/containous/traefik/pulls/$pr | jq -r .head.repo.owner.login)
|
|
||||||
branch=$(curl -s https://api.github.com/repos/containous/traefik/pulls/$pr | jq -r .head.ref)
|
|
||||||
|
|
||||||
git remote add $remote git@github.com:$remote/traefik.git
|
|
||||||
git fetch $remote $branch
|
|
||||||
git checkout -t -b "$pr--$branch" $remote/$branch
|
|
27
.github/rmpr.sh
vendored
@@ -1,27 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# git config --global alias.rmpr '!sh .github/rmpr.sh'
|
|
||||||
|
|
||||||
set -e # stop on error
|
|
||||||
|
|
||||||
usage="$(basename "$0") pr -- remove a Pull Request local branch & remote"
|
|
||||||
|
|
||||||
if [ "$#" -ne 1 ]; then
|
|
||||||
echo "Illegal number of parameters"
|
|
||||||
echo "$usage" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
command -v jq >/dev/null 2>&1 || { echo "I require jq but it's not installed. Aborting." >&2; exit 1; }
|
|
||||||
|
|
||||||
set -x # echo on
|
|
||||||
|
|
||||||
initial=$(git rev-parse --abbrev-ref HEAD)
|
|
||||||
pr=$1
|
|
||||||
remote=$(curl -s https://api.github.com/repos/containous/traefik/pulls/$pr | jq -r .head.repo.owner.login)
|
|
||||||
branch=$(curl -s https://api.github.com/repos/containous/traefik/pulls/$pr | jq -r .head.ref)
|
|
||||||
|
|
||||||
# clean
|
|
||||||
git checkout $initial
|
|
||||||
git branch -D "$pr--$branch"
|
|
||||||
git remote remove $remote
|
|
36
.github/rpr.sh
vendored
@@ -1,36 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# git config --global alias.rpr '!sh .github/rpr.sh'
|
|
||||||
|
|
||||||
set -e # stop on error
|
|
||||||
|
|
||||||
usage="$(basename "$0") pr remote/branch -- rebase a Pull Request against a remote branch"
|
|
||||||
|
|
||||||
if [ "$#" -ne 2 ]; then
|
|
||||||
echo "Illegal number of parameters"
|
|
||||||
echo "$usage" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
command -v jq >/dev/null 2>&1 || { echo "I require jq but it's not installed. Aborting." >&2; exit 1; }
|
|
||||||
|
|
||||||
set -x # echo on
|
|
||||||
|
|
||||||
initial=$(git rev-parse --abbrev-ref HEAD)
|
|
||||||
pr=$1
|
|
||||||
base=$2
|
|
||||||
remote=$(curl -s https://api.github.com/repos/containous/traefik/pulls/$pr | jq -r .head.repo.owner.login)
|
|
||||||
branch=$(curl -s https://api.github.com/repos/containous/traefik/pulls/$pr | jq -r .head.ref)
|
|
||||||
|
|
||||||
clean ()
|
|
||||||
{
|
|
||||||
git checkout $initial
|
|
||||||
.github/rmpr.sh $pr
|
|
||||||
}
|
|
||||||
|
|
||||||
trap clean EXIT
|
|
||||||
|
|
||||||
.github/cpr.sh $pr
|
|
||||||
|
|
||||||
git rebase $base
|
|
||||||
git push --force-with-lease $remote "$pr--$branch"
|
|
21
.gitignore
vendored
@@ -1,13 +1,18 @@
|
|||||||
/dist
|
.idea/
|
||||||
/autogen/gen.go
|
.intellij/
|
||||||
.idea
|
|
||||||
.intellij
|
|
||||||
*.iml
|
*.iml
|
||||||
|
.vscode/
|
||||||
|
.DS_Store
|
||||||
|
/dist
|
||||||
|
/webui/.tmp/
|
||||||
|
/site/
|
||||||
|
/docs/site/
|
||||||
|
/static/
|
||||||
|
/autogen/
|
||||||
/traefik
|
/traefik
|
||||||
/traefik.toml
|
/traefik.toml
|
||||||
/static/
|
/traefik.yml
|
||||||
.vscode/
|
|
||||||
/site/
|
|
||||||
*.log
|
*.log
|
||||||
*.exe
|
*.exe
|
||||||
.DS_Store
|
cover.out
|
||||||
|
vendor/
|
||||||
|
90
.golangci.toml
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
[run]
|
||||||
|
deadline = "10m"
|
||||||
|
skip-files = []
|
||||||
|
|
||||||
|
[linters-settings]
|
||||||
|
|
||||||
|
[linters-settings.govet]
|
||||||
|
check-shadowing = false
|
||||||
|
|
||||||
|
[linters-settings.golint]
|
||||||
|
min-confidence = 0.0
|
||||||
|
|
||||||
|
[linters-settings.gocyclo]
|
||||||
|
min-complexity = 14.0
|
||||||
|
|
||||||
|
[linters-settings.maligned]
|
||||||
|
suggest-new = true
|
||||||
|
|
||||||
|
[linters-settings.goconst]
|
||||||
|
min-len = 3.0
|
||||||
|
min-occurrences = 4.0
|
||||||
|
|
||||||
|
[linters-settings.misspell]
|
||||||
|
locale = "US"
|
||||||
|
|
||||||
|
[linters-settings.funlen]
|
||||||
|
lines = 230 # default 60
|
||||||
|
statements = 120 # default 40
|
||||||
|
|
||||||
|
[linters]
|
||||||
|
enable-all = true
|
||||||
|
disable = [
|
||||||
|
"gocyclo", # FIXME must be fixed
|
||||||
|
"gosec",
|
||||||
|
"dupl",
|
||||||
|
"maligned",
|
||||||
|
"lll",
|
||||||
|
"unparam",
|
||||||
|
"prealloc",
|
||||||
|
"scopelint",
|
||||||
|
"gochecknoinits",
|
||||||
|
"gochecknoglobals",
|
||||||
|
"bodyclose", # Too many false-positive and panics.
|
||||||
|
]
|
||||||
|
|
||||||
|
[issues]
|
||||||
|
exclude-use-default = false
|
||||||
|
max-per-linter = 0
|
||||||
|
max-same-issues = 0
|
||||||
|
exclude = [
|
||||||
|
"SA1019: http.CloseNotifier is deprecated: the CloseNotifier interface predates Go's context package. New code should use Request.Context instead.", # FIXME must be fixed
|
||||||
|
"Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*printf?|os\\.(Un)?Setenv). is not checked",
|
||||||
|
"should have a package comment, unless it's in another file for this package",
|
||||||
|
]
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
path = "(.+)_test.go"
|
||||||
|
linters = ["goconst", "funlen"]
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
path = "integration/.+_test.go"
|
||||||
|
text = "Error return value of `cmd\\.Process\\.Kill` is not checked"
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
path = "integration/(consul_catalog_test|constraint_test).go"
|
||||||
|
text = "Error return value of `(s.deregisterService|s.deregisterAgentService)` is not checked"
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
path = "integration/grpc_test.go"
|
||||||
|
text = "Error return value of `closer` is not checked"
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
path = "pkg/h2c/h2c.go"
|
||||||
|
text = "Error return value of `rw.Write` is not checked"
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
path = "pkg/middlewares/recovery/recovery.go"
|
||||||
|
text = "`logger` can be `github.com/stretchr/testify/assert.TestingT`"
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
path = "pkg/provider/docker/builder_test.go"
|
||||||
|
text = "(U1000: func )?`(.+)` is unused"
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
path = "pkg/provider/kubernetes/builder_(endpoint|service)_test.go"
|
||||||
|
text = "(U1000: func )?`(.+)` is unused"
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
path = "pkg/config/parser/.+_test.go"
|
||||||
|
text = "U1000: field `(foo|fuu)` is unused"
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
path = "pkg/server/service/bufferpool.go"
|
||||||
|
text = "SA6002: argument should be pointer-like to avoid allocations"
|
||||||
|
[[issues.exclude-rules]]
|
||||||
|
path = "cmd/configuration.go"
|
||||||
|
text = "string `traefik` has (\\d) occurrences, make it a constant"
|
||||||
|
[[issues.exclude-rules]] # FIXME must be fixed
|
||||||
|
path = "cmd/context.go"
|
||||||
|
text = "S1000: should use a simple channel send/receive instead of `select` with a single case"
|
58
.goreleaser.yml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
project_name: traefik
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go generate
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- binary: traefik
|
||||||
|
|
||||||
|
main: ./cmd/traefik/traefik.go
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/containous/traefik/v2/pkg/version.Version={{.Version}} -X github.com/containous/traefik/v2/pkg/version.Codename={{.Env.CODENAME}} -X github.com/containous/traefik/v2/pkg/version.BuildDate={{.Date}}
|
||||||
|
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- darwin
|
||||||
|
- windows
|
||||||
|
- freebsd
|
||||||
|
- openbsd
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- 386
|
||||||
|
- arm
|
||||||
|
- arm64
|
||||||
|
- ppc64le
|
||||||
|
goarm:
|
||||||
|
- 7
|
||||||
|
- 6
|
||||||
|
- 5
|
||||||
|
ignore:
|
||||||
|
- goos: darwin
|
||||||
|
goarch: 386
|
||||||
|
- goos: openbsd
|
||||||
|
goarch: arm
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: arm
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
skip: true
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- id: traefik
|
||||||
|
name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
|
||||||
|
format: tar.gz
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
format: zip
|
||||||
|
files:
|
||||||
|
- LICENSE.md
|
||||||
|
- CHANGELOG.md
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
name_template: "{{ .ProjectName }}_v{{ .Version }}_checksums.txt"
|
||||||
|
|
||||||
|
release:
|
||||||
|
disable: true
|
@@ -1,10 +0,0 @@
|
|||||||
- repo: git://github.com/pre-commit/pre-commit-hooks
|
|
||||||
sha: 44e1753f98b0da305332abe26856c3e621c5c439
|
|
||||||
hooks:
|
|
||||||
- id: detect-private-key
|
|
||||||
- repo: git://github.com/containous/pre-commit-hooks
|
|
||||||
sha: 35e641b5107671e94102b0ce909648559e568d61
|
|
||||||
hooks:
|
|
||||||
- id: goFmt
|
|
||||||
- id: goLint
|
|
||||||
- id: goErrcheck
|
|
4
.semaphoreci/cleanup.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
sudo rm -rf static
|
20
.semaphoreci/golang.sh
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
curl -O https://dl.google.com/go/go"${GO_VERSION}".linux-amd64.tar.gz
|
||||||
|
|
||||||
|
tar -xvf go"${GO_VERSION}".linux-amd64.tar.gz
|
||||||
|
rm -rf go"${GO_VERSION}".linux-amd64.tar.gz
|
||||||
|
|
||||||
|
sudo mkdir -p /usr/local/golang/"${GO_VERSION}"/go
|
||||||
|
sudo mv go /usr/local/golang/"${GO_VERSION}"/
|
||||||
|
|
||||||
|
sudo rm /usr/local/bin/go
|
||||||
|
sudo chmod +x /usr/local/golang/"${GO_VERSION}"/go/bin/go
|
||||||
|
sudo ln -s /usr/local/golang/"${GO_VERSION}"/go/bin/go /usr/local/bin/go
|
||||||
|
|
||||||
|
export GOROOT="/usr/local/golang/${GO_VERSION}/go"
|
||||||
|
export GOTOOLDIR="/usr/local/golang/${GO_VERSION}/go/pkg/tool/linux_amd64"
|
||||||
|
|
||||||
|
go version
|
6
.semaphoreci/job1.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -n "$SHOULD_TEST" ]; then ci_retry make pull-images; fi
|
||||||
|
|
||||||
|
if [ -n "$SHOULD_TEST" ]; then ci_retry make test-integration; fi
|
8
.semaphoreci/job2.sh
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
ci_retry make validate
|
||||||
|
|
||||||
|
if [ -n "$SHOULD_TEST" ]; then ci_retry make test-unit; fi
|
||||||
|
|
||||||
|
if [ -n "$SHOULD_TEST" ]; then make -j"${N_MAKE_JOBS}" crossbinary-default-parallel; fi
|
@@ -1,11 +1,35 @@
|
|||||||
#!/usr/bin/env bash
|
# For personnal CI
|
||||||
set -e
|
# mv /home/runner/workspace/src/github.com/<username>/ /home/runner/workspace/src/github.com/containous/
|
||||||
|
# cd /home/runner/workspace/src/github.com/containous/traefik/
|
||||||
|
for s in apache2 cassandra elasticsearch memcached mysql mongod postgresql sphinxsearch rethinkdb rabbitmq-server redis-server; do sudo service $s stop; done
|
||||||
|
sudo swapoff -a
|
||||||
|
sudo dd if=/dev/zero of=/swapfile bs=1M count=3072
|
||||||
|
sudo mkswap /swapfile
|
||||||
|
sudo swapon /swapfile
|
||||||
|
sudo rm -rf /home/runner/.rbenv
|
||||||
|
sudo rm -rf /usr/local/golang/{1.4.3,1.5.4,1.6.4,1.7.6,1.8.6,1.9.7,1.10.3,1.11}
|
||||||
|
#export DOCKER_VERSION=18.06.3
|
||||||
|
source .semaphoreci/vars
|
||||||
|
if [ -z "${PULL_REQUEST_NUMBER}" ]; then SHOULD_TEST="-*-"; else TEMP_STORAGE=$(curl --silent https://patch-diff.githubusercontent.com/raw/containous/traefik/pull/${PULL_REQUEST_NUMBER}.diff | patch --dry-run -p1 -R || true); fi
|
||||||
|
echo ${SHOULD_TEST}
|
||||||
|
if [ -n "$TEMP_STORAGE" ]; then SHOULD_TEST=$(echo "$TEMP_STORAGE" | grep -Ev '(.md|.yaml|.yml)' || :); fi
|
||||||
|
echo ${TEMP_STORAGE}
|
||||||
|
echo ${SHOULD_TEST}
|
||||||
|
#if [ -n "$SHOULD_TEST" ]; then sudo -E apt-get -yq update; fi
|
||||||
|
#if [ -n "$SHOULD_TEST" ]; then sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install docker-ce=${DOCKER_VERSION}*; fi
|
||||||
|
if [ -n "$SHOULD_TEST" ]; then docker version; fi
|
||||||
|
export GO_VERSION=1.12
|
||||||
|
if [ -f "./go.mod" ]; then GO_VERSION="$(grep '^go .*' go.mod | awk '{print $2}')"; export GO_VERSION; fi
|
||||||
|
#if [ "${GO_VERSION}" == '1.13' ]; then export GO_VERSION=1.13rc2; fi
|
||||||
|
echo "Selected Go version: ${GO_VERSION}"
|
||||||
|
|
||||||
sudo -E apt-get -yq update
|
if [ -f "./.semaphoreci/golang.sh" ]; then ./.semaphoreci/golang.sh; fi
|
||||||
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install docker-ce=${DOCKER_VERSION}*
|
if [ -f "./.semaphoreci/golang.sh" ]; then export GOROOT="/usr/local/golang/${GO_VERSION}/go"; fi
|
||||||
docker version
|
if [ -f "./.semaphoreci/golang.sh" ]; then export GOTOOLDIR="/usr/local/golang/${GO_VERSION}/go/pkg/tool/linux_amd64"; fi
|
||||||
|
go version
|
||||||
|
|
||||||
pip install --user -r requirements.txt
|
if [ -f "./go.mod" ]; then export GO111MODULE=on; fi
|
||||||
|
if [ -f "./go.mod" ]; then export GOPROXY=https://proxy.golang.org; fi
|
||||||
|
if [ -f "./go.mod" ]; then go mod download; fi
|
||||||
|
|
||||||
make pull-images
|
df
|
||||||
ci_retry make validate
|
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
make test-unit
|
|
||||||
ci_retry make test-integration
|
|
||||||
make -j${N_MAKE_JOBS} crossbinary-default-parallel
|
|
@@ -1,8 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
export secure='btt4r13t09gQlHb6gYrvGC2yGCMMHfnp1Mz1RQedc4Mpf/FfT8aE6xmK2a2i9CCvskjrP0t/BFaS4yxIURjnFRn+ugQIEa0pLspB9UJArW/vgOSpIWM9/OQ/fg8z5XuMxN6Md4DL1/iLypMNSageA1x0TRdt89+D1N1dALpg5XRCXLFbC84TLi0gjlFuib9ibPKzEhLT+anCRJ6iZMzeupDSoaCVbAtJMoDvXw4+4AcRZ1+k4MybBLyCib5boaEOt4pTT88mz4Kk0YaMwPVJyg9Qv36VqyUcPS09Yd95LuyVQ4+tZt8Y1ccbIzULsK+sLM3hLCzxlmlpN3dQBlZJiiRtQde0mgGAKyC0P0A1XjuDTywcsa5edB+fTk1Dsewz9xZ9V0NmMz8t+UNZnaSsAPga9i86jULbXUUwMVSzVRc+Xgx02liB/8qI1xYC9FM6ilStt7rn7mF0k3KbiWhcptgeXjO6Lah9FjEKd5w4MXsdUSTi/86rQaLo+kj+XdaTrXCTulKHyRyQEUj+8V1w0oVz7pcGjePHd7y5oU9ByifVQy6sytuFBfRZvugM5bKHo+i0pcWvixrZS42DrzwxZJsspANOvqSe5ifVbvOkfUppQdCBIwptxV5N1b49XPKU3W/w34QJ8xGmKp3TFA7WwVCztriFHjPgiRpB3EG99Bg='
|
|
||||||
|
|
||||||
export REPO='containous/traefik'
|
export REPO='containous/traefik'
|
||||||
|
|
||||||
if VERSION=$(git describe --exact-match --abbrev=0 --tags);
|
if VERSION=$(git describe --exact-match --abbrev=0 --tags);
|
||||||
@@ -12,7 +10,7 @@ else
|
|||||||
export VERSION=''
|
export VERSION=''
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export CODENAME=raclette
|
export CODENAME=montdor
|
||||||
|
|
||||||
export N_MAKE_JOBS=2
|
export N_MAKE_JOBS=2
|
||||||
|
|
||||||
@@ -26,8 +24,8 @@ function ci_retry {
|
|||||||
until [ $n -ge $NRETRY ]
|
until [ $n -ge $NRETRY ]
|
||||||
do
|
do
|
||||||
"$@" && break
|
"$@" && break
|
||||||
n=$[$n+1]
|
n=$((n+1))
|
||||||
echo "$@ failed, attempt ${n}/${NRETRY}"
|
echo "${*} failed, attempt ${n}/${NRETRY}"
|
||||||
sleep $NSLEEP
|
sleep $NSLEEP
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -36,4 +34,3 @@ function ci_retry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export -f ci_retry
|
export -f ci_retry
|
||||||
|
|
||||||
|
40
.travis.yml
@@ -1,19 +1,22 @@
|
|||||||
sudo: required
|
sudo: required
|
||||||
dist: trusty
|
dist: trusty
|
||||||
|
|
||||||
|
git:
|
||||||
|
depth: false
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- docker
|
- docker
|
||||||
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- secure: btt4r13t09gQlHb6gYrvGC2yGCMMHfnp1Mz1RQedc4Mpf/FfT8aE6xmK2a2i9CCvskjrP0t/BFaS4yxIURjnFRn+ugQIEa0pLspB9UJArW/vgOSpIWM9/OQ/fg8z5XuMxN6Md4DL1/iLypMNSageA1x0TRdt89+D1N1dALpg5XRCXLFbC84TLi0gjlFuib9ibPKzEhLT+anCRJ6iZMzeupDSoaCVbAtJMoDvXw4+4AcRZ1+k4MybBLyCib5boaEOt4pTT88mz4Kk0YaMwPVJyg9Qv36VqyUcPS09Yd95LuyVQ4+tZt8Y1ccbIzULsK+sLM3hLCzxlmlpN3dQBlZJiiRtQde0mgGAKyC0P0A1XjuDTywcsa5edB+fTk1Dsewz9xZ9V0NmMz8t+UNZnaSsAPga9i86jULbXUUwMVSzVRc+Xgx02liB/8qI1xYC9FM6ilStt7rn7mF0k3KbiWhcptgeXjO6Lah9FjEKd5w4MXsdUSTi/86rQaLo+kj+XdaTrXCTulKHyRyQEUj+8V1w0oVz7pcGjePHd7y5oU9ByifVQy6sytuFBfRZvugM5bKHo+i0pcWvixrZS42DrzwxZJsspANOvqSe5ifVbvOkfUppQdCBIwptxV5N1b49XPKU3W/w34QJ8xGmKp3TFA7WwVCztriFHjPgiRpB3EG99Bg=
|
- REPO=$TRAVIS_REPO_SLUG
|
||||||
- REPO: $TRAVIS_REPO_SLUG
|
- VERSION=$TRAVIS_TAG
|
||||||
- VERSION: $TRAVIS_TAG
|
- CODENAME=montdor
|
||||||
- CODENAME: raclette
|
- GO111MODULE=on
|
||||||
- N_MAKE_JOBS: 2
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- echo "Skipping tests... (Tests are executed on SemaphoreCI)"
|
- echo "Skipping tests... (Tests are executed on SemaphoreCI)"
|
||||||
|
- if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then make docs; fi
|
||||||
|
|
||||||
before_deploy:
|
before_deploy:
|
||||||
- >
|
- >
|
||||||
@@ -22,21 +25,15 @@ before_deploy:
|
|||||||
sudo -E apt-get -yq update;
|
sudo -E apt-get -yq update;
|
||||||
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install docker-ce=${DOCKER_VERSION}*;
|
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install docker-ce=${DOCKER_VERSION}*;
|
||||||
docker version;
|
docker version;
|
||||||
pip install --user -r requirements.txt;
|
make build-image;
|
||||||
make -j${N_MAKE_JOBS} crossbinary-parallel;
|
if [ "$TRAVIS_TAG" ]; then
|
||||||
make image;
|
make release-packages;
|
||||||
mkdocs build --clean;
|
fi;
|
||||||
tar cfz dist/traefik-${VERSION}.src.tar.gz --exclude-vcs --exclude dist .;
|
curl -sfL https://raw.githubusercontent.com/containous/structor/master/godownloader.sh | bash -s -- -b "${GOPATH}/bin" ${STRUCTOR_VERSION}
|
||||||
|
structor -o containous -r traefik --dockerfile-url="https://raw.githubusercontent.com/containous/traefik/v1.7/docs.Dockerfile" --menu.js-url="https://raw.githubusercontent.com/containous/structor/master/traefik-menu.js.gotmpl" --rqts-url="https://raw.githubusercontent.com/containous/structor/master/requirements-override.txt" --force-edit-url --exp-branch=master --debug;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
- provider: pages
|
|
||||||
edge: true
|
|
||||||
github_token: ${GITHUB_TOKEN}
|
|
||||||
local_dir: site
|
|
||||||
skip_cleanup: true
|
|
||||||
on:
|
|
||||||
repo: containous/traefik
|
|
||||||
tags: true
|
|
||||||
- provider: releases
|
- provider: releases
|
||||||
api_key: ${GITHUB_TOKEN}
|
api_key: ${GITHUB_TOKEN}
|
||||||
file: dist/traefik*
|
file: dist/traefik*
|
||||||
@@ -51,8 +48,11 @@ deploy:
|
|||||||
on:
|
on:
|
||||||
repo: containous/traefik
|
repo: containous/traefik
|
||||||
tags: true
|
tags: true
|
||||||
- provider: script
|
- provider: pages
|
||||||
script: sh script/deploy-docker.sh
|
edge: false
|
||||||
|
github_token: ${GITHUB_TOKEN}
|
||||||
|
local_dir: site
|
||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
on:
|
on:
|
||||||
repo: containous/traefik
|
repo: containous/traefik
|
||||||
|
all_branches: true
|
||||||
|
BIN
.travis/traefiker_rsa.enc
Normal file
2851
CHANGELOG.md
@@ -2,17 +2,11 @@
|
|||||||
|
|
||||||
## Our Pledge
|
## Our Pledge
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as
|
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience,nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||||
contributors and maintainers pledge to making participation in our project and
|
|
||||||
our community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
|
||||||
nationality, personal appearance, race, religion, or sexual identity and
|
|
||||||
orientation.
|
|
||||||
|
|
||||||
## Our Standards
|
## Our Standards
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment
|
Examples of behavior that contributes to creating a positive environment include:
|
||||||
include:
|
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
* Using welcoming and inclusive language
|
||||||
* Being respectful of differing viewpoints and experiences
|
* Being respectful of differing viewpoints and experiences
|
||||||
@@ -22,53 +16,36 @@ include:
|
|||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||||
advances
|
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
* Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or electronic
|
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||||
address, without explicit permission
|
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
## Our Responsibilities
|
## Our Responsibilities
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable
|
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||||
behavior and are expected to take appropriate and fair corrective action in
|
|
||||||
response to any instances of unacceptable behavior.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or
|
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
||||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
||||||
permanently any contributor for other behaviors that they deem inappropriate,
|
|
||||||
threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces
|
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
|
||||||
when an individual is representing the project or its community. Examples of
|
Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||||
representing a project or community include using an official project e-mail
|
Representation of a project may be further defined and clarified by project maintainers.
|
||||||
address, posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event. Representation of a project may be
|
|
||||||
further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@containo.us
|
||||||
reported by contacting the project team at contact@containo.us
|
All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances.
|
||||||
All complaints will be reviewed and investigated and will result in a response that
|
The project team is obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
is deemed necessary and appropriate to the circumstances. The project team is
|
|
||||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
||||||
Further details of specific enforcement policies may be posted separately.
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||||
faith may face temporary or permanent repercussions as determined by other
|
|
||||||
members of the project's leadership.
|
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||||
available at [http://contributor-covenant.org/version/1/4][version]
|
|
||||||
|
|
||||||
[homepage]: http://contributor-covenant.org
|
[homepage]: http://contributor-covenant.org
|
||||||
[version]: http://contributor-covenant.org/version/1/4/
|
[version]: http://contributor-covenant.org/version/1/4/
|
3
CONTRIBUTING.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
See <https://docs.traefik.io/v2.0/contributing/thank-you/>.
|
@@ -2,4 +2,5 @@ FROM scratch
|
|||||||
COPY script/ca-certificates.crt /etc/ssl/certs/
|
COPY script/ca-certificates.crt /etc/ssl/certs/
|
||||||
COPY dist/traefik /
|
COPY dist/traefik /
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
VOLUME ["/tmp"]
|
||||||
ENTRYPOINT ["/traefik"]
|
ENTRYPOINT ["/traefik"]
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2016-2017 Containous SAS
|
Copyright (c) 2016-2018 Containous SAS
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
197
Makefile
@@ -1,4 +1,22 @@
|
|||||||
.PHONY: all
|
.PHONY: all docs docs-serve
|
||||||
|
|
||||||
|
SRCS = $(shell git ls-files '*.go' | grep -v '^vendor/')
|
||||||
|
|
||||||
|
TAG_NAME := $(shell git tag -l --contains HEAD)
|
||||||
|
SHA := $(shell git rev-parse HEAD)
|
||||||
|
VERSION_GIT := $(if $(TAG_NAME),$(TAG_NAME),$(SHA))
|
||||||
|
VERSION := $(if $(VERSION),$(VERSION),$(VERSION_GIT))
|
||||||
|
|
||||||
|
BIND_DIR := "dist"
|
||||||
|
|
||||||
|
GIT_BRANCH := $(subst heads/,,$(shell git rev-parse --abbrev-ref HEAD 2>/dev/null))
|
||||||
|
TRAEFIK_DEV_IMAGE := traefik-dev$(if $(GIT_BRANCH),:$(subst /,-,$(GIT_BRANCH)))
|
||||||
|
|
||||||
|
REPONAME := $(shell echo $(REPO) | tr '[:upper:]' '[:lower:]')
|
||||||
|
TRAEFIK_IMAGE := $(if $(REPONAME),$(REPONAME),"containous/traefik")
|
||||||
|
|
||||||
|
INTEGRATION_OPTS := $(if $(MAKE_DOCKER_HOST),-e "DOCKER_HOST=$(MAKE_DOCKER_HOST)", -e "TEST_CONTAINER=1" -v "/var/run/docker.sock:/var/run/docker.sock")
|
||||||
|
DOCKER_BUILD_ARGS := $(if $(DOCKER_VERSION), "--build-arg=DOCKER_VERSION=$(DOCKER_VERSION)",)
|
||||||
|
|
||||||
TRAEFIK_ENVS := \
|
TRAEFIK_ENVS := \
|
||||||
-e OS_ARCH_ARG \
|
-e OS_ARCH_ARG \
|
||||||
@@ -7,108 +25,127 @@ TRAEFIK_ENVS := \
|
|||||||
-e VERBOSE \
|
-e VERBOSE \
|
||||||
-e VERSION \
|
-e VERSION \
|
||||||
-e CODENAME \
|
-e CODENAME \
|
||||||
-e TESTDIRS
|
-e TESTDIRS \
|
||||||
|
-e CI \
|
||||||
|
-e CONTAINER=DOCKER # Indicator for integration tests that we are running inside a container.
|
||||||
|
|
||||||
SRCS = $(shell git ls-files '*.go' | grep -v '^vendor/' | grep -v '^integration/vendor/')
|
|
||||||
|
|
||||||
BIND_DIR := "dist"
|
|
||||||
TRAEFIK_MOUNT := -v "$(CURDIR)/$(BIND_DIR):/go/src/github.com/containous/traefik/$(BIND_DIR)"
|
TRAEFIK_MOUNT := -v "$(CURDIR)/$(BIND_DIR):/go/src/github.com/containous/traefik/$(BIND_DIR)"
|
||||||
|
|
||||||
GIT_BRANCH := $(subst heads/,,$(shell git rev-parse --abbrev-ref HEAD 2>/dev/null))
|
|
||||||
TRAEFIK_DEV_IMAGE := traefik-dev$(if $(GIT_BRANCH),:$(GIT_BRANCH))
|
|
||||||
REPONAME := $(shell echo $(REPO) | tr '[:upper:]' '[:lower:]')
|
|
||||||
TRAEFIK_IMAGE := $(if $(REPONAME),$(REPONAME),"containous/traefik")
|
|
||||||
INTEGRATION_OPTS := $(if $(MAKE_DOCKER_HOST),-e "DOCKER_HOST=$(MAKE_DOCKER_HOST)", -v "/var/run/docker.sock:/var/run/docker.sock")
|
|
||||||
|
|
||||||
DOCKER_BUILD_ARGS := $(if $(DOCKER_VERSION), "--build-arg=DOCKER_VERSION=$(DOCKER_VERSION)",)
|
|
||||||
DOCKER_RUN_OPTS := $(TRAEFIK_ENVS) $(TRAEFIK_MOUNT) "$(TRAEFIK_DEV_IMAGE)"
|
DOCKER_RUN_OPTS := $(TRAEFIK_ENVS) $(TRAEFIK_MOUNT) "$(TRAEFIK_DEV_IMAGE)"
|
||||||
DOCKER_RUN_TRAEFIK := docker run $(INTEGRATION_OPTS) -it $(DOCKER_RUN_OPTS)
|
DOCKER_RUN_TRAEFIK := docker run $(INTEGRATION_OPTS) -it $(DOCKER_RUN_OPTS)
|
||||||
DOCKER_RUN_TRAEFIK_NOTTY := docker run $(INTEGRATION_OPTS) -i $(DOCKER_RUN_OPTS)
|
DOCKER_RUN_TRAEFIK_NOTTY := docker run $(INTEGRATION_OPTS) -i $(DOCKER_RUN_OPTS)
|
||||||
|
|
||||||
|
PRE_TARGET ?= build-dev-image
|
||||||
print-%: ; @echo $*=$($*)
|
|
||||||
|
|
||||||
default: binary
|
default: binary
|
||||||
|
|
||||||
all: generate-webui build ## validate all checks, build linux binary, run all tests\ncross non-linux binaries
|
## Build Dev Docker image
|
||||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh
|
build-dev-image: dist
|
||||||
|
|
||||||
binary: generate-webui build ## build the linux binary
|
|
||||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate binary
|
|
||||||
|
|
||||||
crossbinary: generate-webui build ## cross build the non-linux binaries
|
|
||||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate crossbinary
|
|
||||||
|
|
||||||
crossbinary-parallel:
|
|
||||||
$(MAKE) generate-webui
|
|
||||||
$(MAKE) build crossbinary-default crossbinary-others
|
|
||||||
|
|
||||||
crossbinary-default: generate-webui build
|
|
||||||
$(DOCKER_RUN_TRAEFIK_NOTTY) ./script/make.sh generate crossbinary-default
|
|
||||||
|
|
||||||
crossbinary-default-parallel:
|
|
||||||
$(MAKE) generate-webui
|
|
||||||
$(MAKE) build crossbinary-default
|
|
||||||
|
|
||||||
crossbinary-others: generate-webui build
|
|
||||||
$(DOCKER_RUN_TRAEFIK_NOTTY) ./script/make.sh generate crossbinary-others
|
|
||||||
|
|
||||||
crossbinary-others-parallel:
|
|
||||||
$(MAKE) generate-webui
|
|
||||||
$(MAKE) build crossbinary-others
|
|
||||||
|
|
||||||
test: build ## run the unit and integration tests
|
|
||||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate test-unit binary test-integration
|
|
||||||
|
|
||||||
test-unit: build ## run the unit tests
|
|
||||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate test-unit
|
|
||||||
|
|
||||||
test-integration: build ## run the integration tests
|
|
||||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate binary test-integration
|
|
||||||
|
|
||||||
validate: build ## validate gofmt, golint and go vet
|
|
||||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-glide validate-gofmt validate-govet validate-golint validate-misspell validate-vendor
|
|
||||||
|
|
||||||
build: dist
|
|
||||||
docker build $(DOCKER_BUILD_ARGS) -t "$(TRAEFIK_DEV_IMAGE)" -f build.Dockerfile .
|
docker build $(DOCKER_BUILD_ARGS) -t "$(TRAEFIK_DEV_IMAGE)" -f build.Dockerfile .
|
||||||
|
|
||||||
build-webui:
|
## Build Dev Docker image without cache
|
||||||
docker build -t traefik-webui -f webui/Dockerfile webui
|
build-dev-image-no-cache: dist
|
||||||
|
|
||||||
build-no-cache: dist
|
|
||||||
docker build --no-cache -t "$(TRAEFIK_DEV_IMAGE)" -f build.Dockerfile .
|
docker build --no-cache -t "$(TRAEFIK_DEV_IMAGE)" -f build.Dockerfile .
|
||||||
|
|
||||||
shell: build ## start a shell inside the build env
|
## Create the "dist" directory
|
||||||
$(DOCKER_RUN_TRAEFIK) /bin/bash
|
|
||||||
|
|
||||||
image: binary ## build a docker traefik image
|
|
||||||
docker build -t $(TRAEFIK_IMAGE) .
|
|
||||||
|
|
||||||
dist:
|
dist:
|
||||||
mkdir dist
|
mkdir dist
|
||||||
|
|
||||||
run-dev:
|
## Build WebUI Docker image
|
||||||
go generate
|
build-webui-image:
|
||||||
go build
|
docker build -t traefik-webui -f webui/Dockerfile webui
|
||||||
./traefik
|
|
||||||
|
|
||||||
generate-webui: build-webui
|
## Generate WebUI
|
||||||
|
generate-webui: build-webui-image
|
||||||
if [ ! -d "static" ]; then \
|
if [ ! -d "static" ]; then \
|
||||||
mkdir -p static; \
|
mkdir -p static; \
|
||||||
docker run --rm -v "$$PWD/static":'/src/static' traefik-webui npm run build; \
|
docker run --rm -v "$$PWD/static":'/src/static' traefik-webui npm run build:nc; \
|
||||||
|
docker run --rm -v "$$PWD/static":'/src/static' traefik-webui chown -R $(shell id -u):$(shell id -g) ../static; \
|
||||||
echo 'For more informations show `webui/readme.md`' > $$PWD/static/DONT-EDIT-FILES-IN-THIS-DIRECTORY.md; \
|
echo 'For more informations show `webui/readme.md`' > $$PWD/static/DONT-EDIT-FILES-IN-THIS-DIRECTORY.md; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
lint:
|
## Build the linux binary
|
||||||
script/validate-golint
|
binary: generate-webui $(PRE_TARGET)
|
||||||
|
$(if $(PRE_TARGET),$(DOCKER_RUN_TRAEFIK)) ./script/make.sh generate binary
|
||||||
|
|
||||||
|
## Build the binary for the standard plaforms (linux, darwin, windows)
|
||||||
|
crossbinary-default: generate-webui build-dev-image
|
||||||
|
$(DOCKER_RUN_TRAEFIK_NOTTY) ./script/make.sh generate crossbinary-default
|
||||||
|
|
||||||
|
## Build the binary for the standard plaforms (linux, darwin, windows) in parallel
|
||||||
|
crossbinary-default-parallel:
|
||||||
|
$(MAKE) generate-webui
|
||||||
|
$(MAKE) build-dev-image crossbinary-default
|
||||||
|
|
||||||
|
## Run the unit and integration tests
|
||||||
|
test: build-dev-image
|
||||||
|
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate test-unit binary test-integration
|
||||||
|
|
||||||
|
## Run the unit tests
|
||||||
|
test-unit: $(PRE_TARGET)
|
||||||
|
$(if $(PRE_TARGET),$(DOCKER_RUN_TRAEFIK)) ./script/make.sh generate test-unit
|
||||||
|
|
||||||
|
## Pull all images for integration tests
|
||||||
|
pull-images:
|
||||||
|
grep --no-filename -E '^\s+image:' ./integration/resources/compose/*.yml | awk '{print $$2}' | sort | uniq | xargs -P 6 -n 1 docker pull
|
||||||
|
|
||||||
|
## Run the integration tests
|
||||||
|
test-integration: $(PRE_TARGET)
|
||||||
|
$(if $(PRE_TARGET),$(DOCKER_RUN_TRAEFIK),TEST_CONTAINER=1) ./script/make.sh generate binary test-integration
|
||||||
|
TEST_HOST=1 ./script/make.sh test-integration
|
||||||
|
|
||||||
|
## Validate code and docs
|
||||||
|
validate-files: $(PRE_TARGET)
|
||||||
|
$(if $(PRE_TARGET),$(DOCKER_RUN_TRAEFIK)) ./script/make.sh generate validate-lint validate-misspell
|
||||||
|
bash $(CURDIR)/script/validate-shell-script.sh
|
||||||
|
|
||||||
|
## Validate code, docs, and vendor
|
||||||
|
validate: $(PRE_TARGET)
|
||||||
|
$(if $(PRE_TARGET),$(DOCKER_RUN_TRAEFIK)) ./script/make.sh generate validate-lint validate-misspell validate-vendor
|
||||||
|
bash $(CURDIR)/script/validate-shell-script.sh
|
||||||
|
|
||||||
|
## Clean up static directory and build a Docker Traefik image
|
||||||
|
build-image: binary
|
||||||
|
rm -rf static
|
||||||
|
docker build -t $(TRAEFIK_IMAGE) .
|
||||||
|
|
||||||
|
## Build a Docker Traefik image
|
||||||
|
build-image-dirty: binary
|
||||||
|
docker build -t $(TRAEFIK_IMAGE) .
|
||||||
|
|
||||||
|
## Start a shell inside the build env
|
||||||
|
shell: build-dev-image
|
||||||
|
$(DOCKER_RUN_TRAEFIK) /bin/bash
|
||||||
|
|
||||||
|
## Build documentation site
|
||||||
|
docs:
|
||||||
|
make -C ./docs docs
|
||||||
|
|
||||||
|
## Serve the documentation site localy
|
||||||
|
docs-serve:
|
||||||
|
make -C ./docs docs-serve
|
||||||
|
|
||||||
|
## Generate CRD clientset
|
||||||
|
generate-crd:
|
||||||
|
./script/update-generated-crd-code.sh
|
||||||
|
|
||||||
|
## Create packages for the release
|
||||||
|
release-packages: generate-webui build-dev-image
|
||||||
|
rm -rf dist
|
||||||
|
$(DOCKER_RUN_TRAEFIK_NOTTY) goreleaser release --skip-publish
|
||||||
|
$(DOCKER_RUN_TRAEFIK_NOTTY) tar cfz dist/traefik-${VERSION}.src.tar.gz \
|
||||||
|
--exclude-vcs \
|
||||||
|
--exclude .idea \
|
||||||
|
--exclude .travis \
|
||||||
|
--exclude .semaphoreci \
|
||||||
|
--exclude .github \
|
||||||
|
--exclude dist .
|
||||||
|
$(DOCKER_RUN_TRAEFIK_NOTTY) chown -R $(shell id -u):$(shell id -g) dist/
|
||||||
|
|
||||||
|
## Format the Code
|
||||||
fmt:
|
fmt:
|
||||||
gofmt -s -l -w $(SRCS)
|
gofmt -s -l -w $(SRCS)
|
||||||
|
|
||||||
pull-images:
|
run-dev:
|
||||||
for f in $(shell find ./integration/resources/compose/ -type f); do \
|
go generate
|
||||||
docker-compose -f $$f pull; \
|
GO111MODULE=on go build ./cmd/traefik
|
||||||
done
|
./traefik
|
||||||
|
|
||||||
help: ## this help
|
|
||||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
|
||||||
|
203
README.md
@@ -1,147 +1,176 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs/img/traefik.logo.png" alt="Træfik" title="Træfik" />
|
<img src="docs/content/assets/img/traefik.logo.png" alt="Traefik" title="Traefik" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://travis-ci.org/containous/traefik)
|
[](https://semaphoreci.com/containous/traefik)
|
||||||
[](https://docs.traefik.io)
|
[](https://docs.traefik.io)
|
||||||
[](http://goreportcard.com/report/containous/traefik)
|
[](http://goreportcard.com/report/containous/traefik)
|
||||||
[](https://microbadger.com/images/traefik)
|
[](https://microbadger.com/images/traefik)
|
||||||
[](https://github.com/containous/traefik/blob/master/LICENSE.md)
|
[](https://github.com/containous/traefik/blob/master/LICENSE.md)
|
||||||
[](https://traefik.herokuapp.com)
|
[](https://community.containo.us/)
|
||||||
[](https://twitter.com/intent/follow?screen_name=traefikproxy)
|
[](https://twitter.com/intent/follow?screen_name=traefik)
|
||||||
|
|
||||||
|
|
||||||
Træfik (pronounced like [traffic](https://speak-ipa.bearbin.net/speak.cgi?speak=%CB%88tr%C3%A6f%C9%AAk)) is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
|
Traefik (pronounced _traffic_) is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy.
|
||||||
It supports several backends ([Docker](https://www.docker.com/), [Swarm](https://docs.docker.com/swarm), [Kubernetes](http://kubernetes.io), [Marathon](https://mesosphere.github.io/marathon/), [Mesos](https://github.com/apache/mesos), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Zookeeper](https://zookeeper.apache.org), [BoltDB](https://github.com/boltdb/bolt), [Eureka](https://github.com/Netflix/eureka), [Amazon DynamoDB](https://aws.amazon.com/dynamodb/), Rest API, file...) to manage its configuration automatically and dynamically.
|
Traefik integrates with your existing infrastructure components ([Docker](https://www.docker.com/), [Swarm mode](https://docs.docker.com/engine/swarm/), [Kubernetes](https://kubernetes.io), [Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Rancher](https://rancher.com), [Amazon ECS](https://aws.amazon.com/ecs), ...) and configures itself automatically and dynamically.
|
||||||
|
Pointing Traefik at your orchestrator should be the _only_ configuration step you need.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
. **[Overview](#overview)** .
|
||||||
|
**[Features](#features)** .
|
||||||
|
**[Supported backends](#supported-backends)** .
|
||||||
|
**[Quickstart](#quickstart)** .
|
||||||
|
**[Web UI](#web-ui)** .
|
||||||
|
**[Documentation](#documentation)** .
|
||||||
|
|
||||||
|
. **[Support](#support)** .
|
||||||
|
**[Release cycle](#release-cycle)** .
|
||||||
|
**[Contributing](#contributing)** .
|
||||||
|
**[Maintainers](#maintainers)** .
|
||||||
|
**[Credits](#credits)** .
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
:warning: Please be aware that the old configurations for Traefik v1.X are NOT compatible with the v2.X config as of now. If you're testing out v2, please ensure you are using a [v2 configuration](https://docs.traefik.io/v2.0/).
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Imagine that you have deployed a bunch of microservices on your infrastructure. You probably used a service registry (like etcd or consul) and/or an orchestrator (swarm, Mesos/Marathon) to manage all these services.
|
Imagine that you have deployed a bunch of microservices with the help of an orchestrator (like Swarm or Kubernetes) or a service registry (like etcd or consul).
|
||||||
If you want your users to access some of your microservices from the Internet, you will have to use a reverse proxy and configure it using virtual hosts or prefix paths:
|
Now you want users to access these microservices, and you need a reverse proxy.
|
||||||
|
|
||||||
- domain `api.domain.com` will point the microservice `api` in your private network
|
Traditional reverse-proxies require that you configure _each_ route that will connect paths and subdomains to _each_ microservice.
|
||||||
- path `domain.com/web` will point the microservice `web` in your private network
|
In an environment where you add, remove, kill, upgrade, or scale your services _many_ times a day, the task of keeping the routes up to date becomes tedious.
|
||||||
- domain `backoffice.domain.com` will point the microservices `backoffice` in your private network, load-balancing between your multiple instances
|
|
||||||
|
|
||||||
But a microservices architecture is dynamic... Services are added, removed, killed or upgraded often, eventually several times a day.
|
**This is when Traefik can help you!**
|
||||||
|
|
||||||
Traditional reverse-proxies are not natively dynamic. You can't change their configuration and hot-reload easily.
|
Traefik listens to your service registry/orchestrator API and instantly generates the routes so your microservices are connected to the outside world -- without further intervention from your part.
|
||||||
|
|
||||||
Here enters Træfik.
|
**Run Traefik and let it do the work for you!**
|
||||||
|
_(But if you'd rather configure some of your routes manually, Traefik supports that too!)_
|
||||||

|
|
||||||
|
|
||||||
Træfik can listen to your service registry/orchestrator API, and knows each time a microservice is added, removed, killed or upgraded, and can generate its configuration automatically.
|
|
||||||
Routes to your services will be created instantly.
|
|
||||||
|
|
||||||
Run it and forget it!
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [It's fast](http://docs.traefik.io/benchmarks)
|
- Continuously updates its configuration (No restarts!)
|
||||||
- No dependency hell, single binary made with go
|
- Supports multiple load balancing algorithms
|
||||||
- Rest API
|
- Provides HTTPS to your microservices by leveraging [Let's Encrypt](https://letsencrypt.org) (wildcard certificates support)
|
||||||
- Multiple backends supported: Docker, Swarm, Kubernetes, Marathon, Mesos, Consul, Etcd, and more to come
|
- Circuit breakers, retry
|
||||||
- Watchers for backends, can listen for changes in backends to apply a new configuration automatically
|
- See the magic through its clean web UI
|
||||||
- Hot-reloading of configuration. No need to restart the process
|
- Websocket, HTTP/2, GRPC ready
|
||||||
- Graceful shutdown http connections
|
- Provides metrics (Rest, Prometheus, Datadog, Statsd, InfluxDB)
|
||||||
- Circuit breakers on backends
|
- Keeps access logs (JSON, CLF)
|
||||||
- Round Robin, rebalancer load-balancers
|
- Fast
|
||||||
- Rest Metrics
|
- Exposes a Rest API
|
||||||
- [Tiny](https://microbadger.com/images/traefik) [official](https://hub.docker.com/r/_/traefik/) docker image included
|
- Packaged as a single binary file (made with :heart: with go) and available as a [tiny](https://microbadger.com/images/traefik) [official](https://hub.docker.com/r/_/traefik/) docker image
|
||||||
- SSL backends support
|
|
||||||
- SSL frontend support (with SNI)
|
|
||||||
- Clean AngularJS Web UI
|
## Supported Backends
|
||||||
- Websocket support
|
|
||||||
- HTTP/2 support
|
- [Docker](https://docs.traefik.io/configuration/backends/docker) / [Swarm mode](https://docs.traefik.io/configuration/backends/docker#docker-swarm-mode)
|
||||||
- Retry request if network error
|
- [Kubernetes](https://docs.traefik.io/configuration/backends/kubernetes)
|
||||||
- [Let's Encrypt](https://letsencrypt.org) support (Automatic HTTPS with renewal)
|
- [Mesos](https://docs.traefik.io/configuration/backends/mesos) / [Marathon](https://docs.traefik.io/configuration/backends/marathon)
|
||||||
- High Availability with cluster mode
|
- [Rancher](https://docs.traefik.io/configuration/backends/rancher) (API, Metadata)
|
||||||
|
- [Azure Service Fabric](https://docs.traefik.io/configuration/backends/servicefabric)
|
||||||
|
- [Consul Catalog](https://docs.traefik.io/configuration/backends/consulcatalog)
|
||||||
|
- [Consul](https://docs.traefik.io/configuration/backends/consul) / [Etcd](https://docs.traefik.io/configuration/backends/etcd) / [Zookeeper](https://docs.traefik.io/configuration/backends/zookeeper) / [BoltDB](https://docs.traefik.io/configuration/backends/boltdb)
|
||||||
|
- [Eureka](https://docs.traefik.io/configuration/backends/eureka)
|
||||||
|
- [Amazon ECS](https://docs.traefik.io/configuration/backends/ecs)
|
||||||
|
- [Amazon DynamoDB](https://docs.traefik.io/configuration/backends/dynamodb)
|
||||||
|
- [File](https://docs.traefik.io/configuration/backends/file)
|
||||||
|
- [Rest](https://docs.traefik.io/configuration/backends/rest)
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
You can have a quick look at Træfik in this [Katacoda tutorial](https://www.katacoda.com/courses/traefik/deploy-load-balancer) that shows how to load balance requests between multiple Docker containers.
|
To get your hands on Traefik, you can use the [5-Minute Quickstart](http://docs.traefik.io/#the-traefik-quickstart-using-docker) in our documentation (you will need Docker).
|
||||||
|
|
||||||
Here is a talk given by [Ed Robinson](https://github.com/errm) at the [ContainerCamp UK](https://container.camp) conference.
|
|
||||||
You will learn fundamental Træfik features and see some demos with Kubernetes.
|
|
||||||
|
|
||||||
[](https://www.youtube.com/watch?v=aFtpIShV60I)
|
|
||||||
|
|
||||||
Here is a talk (in French) given by [Emile Vauge](https://github.com/emilevauge) at the [Devoxx France 2016](http://www.devoxx.fr) conference.
|
|
||||||
You will learn fundamental Træfik features and see some demos with Docker, Mesos/Marathon and Let's Encrypt.
|
|
||||||
|
|
||||||
[](http://www.youtube.com/watch?v=QvAz9mVx5TI)
|
|
||||||
|
|
||||||
## Web UI
|
## Web UI
|
||||||
|
|
||||||
You can access the simple HTML frontend of Træfik.
|
You can access the simple HTML frontend of Traefik.
|
||||||
|
|
||||||

|

|
||||||

|
|
||||||
|
|
||||||
## Plumbing
|
## Documentation
|
||||||
|
|
||||||
- [Oxy](https://github.com/vulcand/oxy): an awesome proxy library made by Mailgun guys
|
You can find the complete documentation at [https://docs.traefik.io](https://docs.traefik.io).
|
||||||
- [Gorilla mux](https://github.com/gorilla/mux): famous request router
|
A collection of contributions around Traefik can be found at [https://awesome.traefik.io](https://awesome.traefik.io).
|
||||||
- [Negroni](https://github.com/codegangsta/negroni): web middlewares made simple
|
|
||||||
- [Lego](https://github.com/xenolf/lego): the best [Let's Encrypt](https://letsencrypt.org) library in go
|
|
||||||
|
|
||||||
## Test it
|
:warning: If you're testing out v2, please ensure you are using the [v2 documentation](https://docs.traefik.io/v2.0/).
|
||||||
|
|
||||||
- The simple way: grab the latest binary from the [releases](https://github.com/containous/traefik/releases) page and just run it with the [sample configuration file](https://raw.githubusercontent.com/containous/traefik/master/traefik.sample.toml):
|
## Support
|
||||||
|
|
||||||
|
To get community support, you can:
|
||||||
|
- join the Traefik community forum: [](https://community.containo.us/)
|
||||||
|
|
||||||
|
If you need commercial support, please contact [Containo.us](https://containo.us) by mail: <mailto:support@containo.us>.
|
||||||
|
|
||||||
|
## Download
|
||||||
|
|
||||||
|
- Grab the latest binary from the [releases](https://github.com/containous/traefik/releases) page and run it with the [sample configuration file](https://raw.githubusercontent.com/containous/traefik/master/traefik.sample.toml):
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
./traefik --configFile=traefik.toml
|
./traefik --configFile=traefik.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
- Use the tiny Docker image:
|
- Or use the official tiny Docker image and run it with the [sample configuration file](https://raw.githubusercontent.com/containous/traefik/master/traefik.sample.toml):
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.toml traefik
|
docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.toml traefik
|
||||||
```
|
```
|
||||||
|
|
||||||
- From sources:
|
- Or get the sources:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/containous/traefik
|
git clone https://github.com/containous/traefik
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Introductory Videos
|
||||||
|
|
||||||
You can find the complete documentation [here](https://docs.traefik.io).
|
:warning: Please be aware that these videos are for v1.X. The old configurations for Traefik v1.X are NOT compatible with Traefik v2. If you're testing out v2, please ensure you are using a [v2 configuration](https://docs.traefik.io/v2.0/).
|
||||||
|
|
||||||
## Contributing
|
Here is a talk given by [Emile Vauge](https://github.com/emilevauge) at GopherCon 2017.
|
||||||
|
You will learn Traefik basics in less than 10 minutes.
|
||||||
|
|
||||||
Please refer to [this section](.github/CONTRIBUTING.md).
|
[](https://www.youtube.com/watch?v=RgudiksfL-k)
|
||||||
|
|
||||||
## Code Of Conduct
|
Here is a talk given by [Ed Robinson](https://github.com/errm) at [ContainerCamp UK](https://container.camp) conference.
|
||||||
|
You will learn fundamental Traefik features and see some demos with Kubernetes.
|
||||||
|
|
||||||
Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
|
[](https://www.youtube.com/watch?v=aFtpIShV60I)
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
You can join [](https://traefik.herokuapp.com) to get basic support.
|
|
||||||
If you prefer commercial support, please contact [containo.us](https://containo.us) by mail: <mailto:support@containo.us>.
|
|
||||||
|
|
||||||
## Maintainers
|
## Maintainers
|
||||||
|
|
||||||
- Emile Vauge [@emilevauge](https://github.com/emilevauge)
|
[Information about process and maintainers](docs/content/contributing/maintainers.md)
|
||||||
- Vincent Demeester [@vdemeester](https://github.com/vdemeester)
|
|
||||||
- Russell Clare [@Russell-IO](https://github.com/Russell-IO)
|
## Contributing
|
||||||
- Ed Robinson [@errm](https://github.com/errm)
|
|
||||||
- Daniel Tomcej [@dtomcej](https://github.com/dtomcej)
|
If you'd like to contribute to the project, refer to the [contributing documentation](CONTRIBUTING.md).
|
||||||
- Manuel Laufenberg [@SantoDE](https://github.com/SantoDE)
|
|
||||||
- Thomas Recloux [@trecloux](https://github.com/trecloux)
|
Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md).
|
||||||
- Timo Reimann [@timoreimann](https://github.com/timoreimann)
|
By participating in this project, you agree to abide by its terms.
|
||||||
|
|
||||||
|
## Release Cycle
|
||||||
|
|
||||||
|
- We release a new version (e.g. 1.1.0, 1.2.0, 1.3.0) every other month.
|
||||||
|
- Release Candidates are available before the release (e.g. 1.1.0-rc1, 1.1.0-rc2, 1.1.0-rc3, 1.1.0-rc4, before 1.1.0)
|
||||||
|
- Bug-fixes (e.g. 1.1.1, 1.1.2, 1.2.1, 1.2.3) are released as needed (no additional features are delivered in those versions, bug-fixes only)
|
||||||
|
|
||||||
|
Each version is supported until the next one is released (e.g. 1.1.x will be supported until 1.2.0 is out)
|
||||||
|
|
||||||
|
We use [Semantic Versioning](http://semver.org/)
|
||||||
|
|
||||||
|
## Mailing lists
|
||||||
|
|
||||||
|
- General announcements, new releases: mail at news+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/news)
|
||||||
|
- Security announcements: mail at security+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/security).
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
Kudos to [Peka](http://peka.byethost11.com/photoblog/) for his awesome work on the logo .
|
Kudos to [Peka](http://peka.byethost11.com/photoblog/) for his awesome work on the logo .
|
||||||
Traefik's logo licensed under the Creative Commons 3.0 Attributions license.
|
|
||||||
|
Traefik's logo is licensed under the Creative Commons 3.0 Attributions license.
|
||||||
|
|
||||||
Traefik's logo was inspired by the gopher stickers made by Takuya Ueda (https://twitter.com/tenntenn).
|
Traefik's logo was inspired by the gopher stickers made by Takuya Ueda (https://twitter.com/tenntenn).
|
||||||
The original Go gopher was designed by Renee French (http://reneefrench.blogspot.com/).
|
The original Go gopher was designed by Renee French (http://reneefrench.blogspot.com/).
|
||||||
|
245
acme/account.go
@@ -1,245 +0,0 @@
|
|||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"errors"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/containous/traefik/log"
|
|
||||||
"github.com/xenolf/lego/acme"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Account is used to store lets encrypt registration info
|
|
||||||
type Account struct {
|
|
||||||
Email string
|
|
||||||
Registration *acme.RegistrationResource
|
|
||||||
PrivateKey []byte
|
|
||||||
DomainsCertificate DomainsCertificates
|
|
||||||
ChallengeCerts map[string]*ChallengeCert
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChallengeCert stores a challenge certificate
|
|
||||||
type ChallengeCert struct {
|
|
||||||
Certificate []byte
|
|
||||||
PrivateKey []byte
|
|
||||||
certificate *tls.Certificate
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init inits acccount struct
|
|
||||||
func (a *Account) Init() error {
|
|
||||||
err := a.DomainsCertificate.Init()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, cert := range a.ChallengeCerts {
|
|
||||||
if cert.certificate == nil {
|
|
||||||
certificate, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cert.certificate = &certificate
|
|
||||||
}
|
|
||||||
if cert.certificate.Leaf == nil {
|
|
||||||
leaf, err := x509.ParseCertificate(cert.certificate.Certificate[0])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cert.certificate.Leaf = leaf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAccount creates an account
|
|
||||||
func NewAccount(email string) (*Account, error) {
|
|
||||||
// Create a user. New accounts need an email and private key to start
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
domainsCerts := DomainsCertificates{Certs: []*DomainsCertificate{}}
|
|
||||||
domainsCerts.Init()
|
|
||||||
return &Account{
|
|
||||||
Email: email,
|
|
||||||
PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey),
|
|
||||||
DomainsCertificate: DomainsCertificates{Certs: domainsCerts.Certs},
|
|
||||||
ChallengeCerts: map[string]*ChallengeCert{}}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEmail returns email
|
|
||||||
func (a *Account) GetEmail() string {
|
|
||||||
return a.Email
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRegistration returns lets encrypt registration resource
|
|
||||||
func (a *Account) GetRegistration() *acme.RegistrationResource {
|
|
||||||
return a.Registration
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPrivateKey returns private key
|
|
||||||
func (a *Account) GetPrivateKey() crypto.PrivateKey {
|
|
||||||
if privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey); err == nil {
|
|
||||||
return privateKey
|
|
||||||
}
|
|
||||||
log.Errorf("Cannot unmarshall private key %+v", a.PrivateKey)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Certificate is used to store certificate info
|
|
||||||
type Certificate struct {
|
|
||||||
Domain string
|
|
||||||
CertURL string
|
|
||||||
CertStableURL string
|
|
||||||
PrivateKey []byte
|
|
||||||
Certificate []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// DomainsCertificates stores a certificate for multiple domains
|
|
||||||
type DomainsCertificates struct {
|
|
||||||
Certs []*DomainsCertificate
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dc *DomainsCertificates) Len() int {
|
|
||||||
return len(dc.Certs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dc *DomainsCertificates) Swap(i, j int) {
|
|
||||||
dc.Certs[i], dc.Certs[j] = dc.Certs[j], dc.Certs[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dc *DomainsCertificates) Less(i, j int) bool {
|
|
||||||
if reflect.DeepEqual(dc.Certs[i].Domains, dc.Certs[j].Domains) {
|
|
||||||
return dc.Certs[i].tlsCert.Leaf.NotAfter.After(dc.Certs[j].tlsCert.Leaf.NotAfter)
|
|
||||||
}
|
|
||||||
if dc.Certs[i].Domains.Main == dc.Certs[j].Domains.Main {
|
|
||||||
return strings.Join(dc.Certs[i].Domains.SANs, ",") < strings.Join(dc.Certs[j].Domains.SANs, ",")
|
|
||||||
}
|
|
||||||
return dc.Certs[i].Domains.Main < dc.Certs[j].Domains.Main
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dc *DomainsCertificates) removeDuplicates() {
|
|
||||||
sort.Sort(dc)
|
|
||||||
for i := 0; i < len(dc.Certs); i++ {
|
|
||||||
for i2 := i + 1; i2 < len(dc.Certs); i2++ {
|
|
||||||
if reflect.DeepEqual(dc.Certs[i].Domains, dc.Certs[i2].Domains) {
|
|
||||||
// delete
|
|
||||||
log.Warnf("Remove duplicate cert: %+v, expiration :%s", dc.Certs[i2].Domains, dc.Certs[i2].tlsCert.Leaf.NotAfter.String())
|
|
||||||
dc.Certs = append(dc.Certs[:i2], dc.Certs[i2+1:]...)
|
|
||||||
i2--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init inits DomainsCertificates
|
|
||||||
func (dc *DomainsCertificates) Init() error {
|
|
||||||
dc.lock.Lock()
|
|
||||||
defer dc.lock.Unlock()
|
|
||||||
for _, domainsCertificate := range dc.Certs {
|
|
||||||
tlsCert, err := tls.X509KeyPair(domainsCertificate.Certificate.Certificate, domainsCertificate.Certificate.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
domainsCertificate.tlsCert = &tlsCert
|
|
||||||
if domainsCertificate.tlsCert.Leaf == nil {
|
|
||||||
leaf, err := x509.ParseCertificate(domainsCertificate.tlsCert.Certificate[0])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
domainsCertificate.tlsCert.Leaf = leaf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dc.removeDuplicates()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dc *DomainsCertificates) renewCertificates(acmeCert *Certificate, domain Domain) error {
|
|
||||||
dc.lock.Lock()
|
|
||||||
defer dc.lock.Unlock()
|
|
||||||
|
|
||||||
for _, domainsCertificate := range dc.Certs {
|
|
||||||
if reflect.DeepEqual(domain, domainsCertificate.Domains) {
|
|
||||||
tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
domainsCertificate.Certificate = acmeCert
|
|
||||||
domainsCertificate.tlsCert = &tlsCert
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors.New("Certificate to renew not found for domain " + domain.Main)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dc *DomainsCertificates) addCertificateForDomains(acmeCert *Certificate, domain Domain) (*DomainsCertificate, error) {
|
|
||||||
dc.lock.Lock()
|
|
||||||
defer dc.lock.Unlock()
|
|
||||||
|
|
||||||
tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cert := DomainsCertificate{Domains: domain, Certificate: acmeCert, tlsCert: &tlsCert}
|
|
||||||
dc.Certs = append(dc.Certs, &cert)
|
|
||||||
return &cert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dc *DomainsCertificates) getCertificateForDomain(domainToFind string) (*DomainsCertificate, bool) {
|
|
||||||
dc.lock.RLock()
|
|
||||||
defer dc.lock.RUnlock()
|
|
||||||
for _, domainsCertificate := range dc.Certs {
|
|
||||||
domains := []string{}
|
|
||||||
domains = append(domains, domainsCertificate.Domains.Main)
|
|
||||||
domains = append(domains, domainsCertificate.Domains.SANs...)
|
|
||||||
for _, domain := range domains {
|
|
||||||
if domain == domainToFind {
|
|
||||||
return domainsCertificate, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dc *DomainsCertificates) exists(domainToFind Domain) (*DomainsCertificate, bool) {
|
|
||||||
dc.lock.RLock()
|
|
||||||
defer dc.lock.RUnlock()
|
|
||||||
for _, domainsCertificate := range dc.Certs {
|
|
||||||
if reflect.DeepEqual(domainToFind, domainsCertificate.Domains) {
|
|
||||||
return domainsCertificate, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// DomainsCertificate contains a certificate for multiple domains
|
|
||||||
type DomainsCertificate struct {
|
|
||||||
Domains Domain
|
|
||||||
Certificate *Certificate
|
|
||||||
tlsCert *tls.Certificate
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dc *DomainsCertificate) needRenew() bool {
|
|
||||||
for _, c := range dc.tlsCert.Certificate {
|
|
||||||
crt, err := x509.ParseCertificate(c)
|
|
||||||
if err != nil {
|
|
||||||
// If there's an error, we assume the cert is broken, and needs update
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// <= 30 days left, renew certificate
|
|
||||||
if crt.NotAfter.Before(time.Now().Add(time.Duration(24 * 30 * time.Hour))) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
635
acme/acme.go
@@ -1,635 +0,0 @@
|
|||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
fmtlog "log"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/BurntSushi/ty/fun"
|
|
||||||
"github.com/cenk/backoff"
|
|
||||||
"github.com/containous/staert"
|
|
||||||
"github.com/containous/traefik/cluster"
|
|
||||||
"github.com/containous/traefik/log"
|
|
||||||
"github.com/containous/traefik/safe"
|
|
||||||
"github.com/containous/traefik/types"
|
|
||||||
"github.com/eapache/channels"
|
|
||||||
"github.com/xenolf/lego/acme"
|
|
||||||
"github.com/xenolf/lego/providers/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// OSCPMustStaple enables OSCP stapling as from https://github.com/xenolf/lego/issues/270
|
|
||||||
OSCPMustStaple = false
|
|
||||||
)
|
|
||||||
|
|
||||||
// ACME allows to connect to lets encrypt and retrieve certs
|
|
||||||
type ACME struct {
|
|
||||||
Email string `description:"Email address used for registration"`
|
|
||||||
Domains []Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"`
|
|
||||||
Storage string `description:"File or key used for certificates storage."`
|
|
||||||
StorageFile string // deprecated
|
|
||||||
OnDemand bool `description:"Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."`
|
|
||||||
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
|
|
||||||
CAServer string `description:"CA server to use."`
|
|
||||||
EntryPoint string `description:"Entrypoint to proxy acme challenge to."`
|
|
||||||
DNSProvider string `description:"Use a DNS based challenge provider rather than HTTPS."`
|
|
||||||
DelayDontCheckDNS int `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."`
|
|
||||||
ACMELogging bool `description:"Enable debug logging of ACME actions."`
|
|
||||||
client *acme.Client
|
|
||||||
defaultCertificate *tls.Certificate
|
|
||||||
store cluster.Store
|
|
||||||
challengeProvider *challengeProvider
|
|
||||||
checkOnDemandDomain func(domain string) bool
|
|
||||||
jobs *channels.InfiniteChannel
|
|
||||||
TLSConfig *tls.Config `description:"TLS config in case wildcard certs are used"`
|
|
||||||
}
|
|
||||||
|
|
||||||
//Domains parse []Domain
|
|
||||||
type Domains []Domain
|
|
||||||
|
|
||||||
//Set []Domain
|
|
||||||
func (ds *Domains) Set(str string) error {
|
|
||||||
fargs := func(c rune) bool {
|
|
||||||
return c == ',' || c == ';'
|
|
||||||
}
|
|
||||||
// get function
|
|
||||||
slice := strings.FieldsFunc(str, fargs)
|
|
||||||
if len(slice) < 1 {
|
|
||||||
return fmt.Errorf("Parse error ACME.Domain. Imposible to parse %s", str)
|
|
||||||
}
|
|
||||||
d := Domain{
|
|
||||||
Main: slice[0],
|
|
||||||
SANs: []string{},
|
|
||||||
}
|
|
||||||
if len(slice) > 1 {
|
|
||||||
d.SANs = slice[1:]
|
|
||||||
}
|
|
||||||
*ds = append(*ds, d)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//Get []Domain
|
|
||||||
func (ds *Domains) Get() interface{} { return []Domain(*ds) }
|
|
||||||
|
|
||||||
//String returns []Domain in string
|
|
||||||
func (ds *Domains) String() string { return fmt.Sprintf("%+v", *ds) }
|
|
||||||
|
|
||||||
//SetValue sets []Domain into the parser
|
|
||||||
func (ds *Domains) SetValue(val interface{}) {
|
|
||||||
*ds = Domains(val.([]Domain))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Domain holds a domain name with SANs
|
|
||||||
type Domain struct {
|
|
||||||
Main string
|
|
||||||
SANs []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ACME) init() error {
|
|
||||||
if a.ACMELogging {
|
|
||||||
acme.Logger = fmtlog.New(os.Stderr, "legolog: ", fmtlog.LstdFlags)
|
|
||||||
} else {
|
|
||||||
acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
|
|
||||||
}
|
|
||||||
// no certificates in TLS config, so we add a default one
|
|
||||||
cert, err := generateDefaultCertificate()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
a.defaultCertificate = cert
|
|
||||||
// TODO: to remove in the futurs
|
|
||||||
if len(a.StorageFile) > 0 && len(a.Storage) == 0 {
|
|
||||||
log.Warnf("ACME.StorageFile is deprecated, use ACME.Storage instead")
|
|
||||||
a.Storage = a.StorageFile
|
|
||||||
}
|
|
||||||
a.jobs = channels.NewInfiniteChannel()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateClusterConfig creates a tls.config using ACME configuration in cluster mode
|
|
||||||
func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tls.Config, checkOnDemandDomain func(domain string) bool) error {
|
|
||||||
err := a.init()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(a.Storage) == 0 {
|
|
||||||
return errors.New("Empty Store, please provide a key for certs storage")
|
|
||||||
}
|
|
||||||
a.checkOnDemandDomain = checkOnDemandDomain
|
|
||||||
tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate)
|
|
||||||
tlsConfig.GetCertificate = a.getCertificate
|
|
||||||
a.TLSConfig = tlsConfig
|
|
||||||
listener := func(object cluster.Object) error {
|
|
||||||
account := object.(*Account)
|
|
||||||
account.Init()
|
|
||||||
if !leadership.IsLeader() {
|
|
||||||
a.client, err = a.buildACMEClient(account)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error building ACME client %+v: %s", object, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
datastore, err := cluster.NewDataStore(
|
|
||||||
leadership.Pool.Ctx(),
|
|
||||||
staert.KvSource{
|
|
||||||
Store: leadership.Store,
|
|
||||||
Prefix: a.Storage,
|
|
||||||
},
|
|
||||||
&Account{},
|
|
||||||
listener)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
a.store = datastore
|
|
||||||
a.challengeProvider = &challengeProvider{store: a.store}
|
|
||||||
|
|
||||||
ticker := time.NewTicker(24 * time.Hour)
|
|
||||||
leadership.Pool.AddGoCtx(func(ctx context.Context) {
|
|
||||||
log.Infof("Starting ACME renew job...")
|
|
||||||
defer log.Infof("Stopped ACME renew job...")
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
a.renewCertificates()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
leadership.AddListener(func(elected bool) error {
|
|
||||||
if elected {
|
|
||||||
object, err := a.store.Load()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
transaction, object, err := a.store.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
account := object.(*Account)
|
|
||||||
account.Init()
|
|
||||||
var needRegister bool
|
|
||||||
if account == nil || len(account.Email) == 0 {
|
|
||||||
account, err = NewAccount(a.Email)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
needRegister = true
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
a.client, err = a.buildACMEClient(account)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if needRegister {
|
|
||||||
// New users will need to register; be sure to save it
|
|
||||||
log.Debugf("Register...")
|
|
||||||
reg, err := a.client.Register()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
account.Registration = reg
|
|
||||||
}
|
|
||||||
// The client has a URL to the current Let's Encrypt Subscriber
|
|
||||||
// Agreement. The user will need to agree to it.
|
|
||||||
log.Debugf("AgreeToTOS...")
|
|
||||||
err = a.client.AgreeToTOS()
|
|
||||||
if err != nil {
|
|
||||||
// Let's Encrypt Subscriber Agreement renew ?
|
|
||||||
reg, err := a.client.QueryRegistration()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
account.Registration = reg
|
|
||||||
err = a.client.AgreeToTOS()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error sending ACME agreement to TOS: %+v: %s", account, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = transaction.Commit(account)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
a.retrieveCertificates()
|
|
||||||
a.renewCertificates()
|
|
||||||
a.runJobs()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateLocalConfig creates a tls.config using local ACME configuration
|
|
||||||
func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func(domain string) bool) error {
|
|
||||||
err := a.init()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(a.Storage) == 0 {
|
|
||||||
return errors.New("Empty Store, please provide a filename for certs storage")
|
|
||||||
}
|
|
||||||
a.checkOnDemandDomain = checkOnDemandDomain
|
|
||||||
tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate)
|
|
||||||
tlsConfig.GetCertificate = a.getCertificate
|
|
||||||
a.TLSConfig = tlsConfig
|
|
||||||
localStore := NewLocalStore(a.Storage)
|
|
||||||
a.store = localStore
|
|
||||||
a.challengeProvider = &challengeProvider{store: a.store}
|
|
||||||
|
|
||||||
var needRegister bool
|
|
||||||
var account *Account
|
|
||||||
|
|
||||||
if fileInfo, fileErr := os.Stat(a.Storage); fileErr == nil && fileInfo.Size() != 0 {
|
|
||||||
log.Infof("Loading ACME Account...")
|
|
||||||
// load account
|
|
||||||
object, err := localStore.Load()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
account = object.(*Account)
|
|
||||||
} else {
|
|
||||||
log.Infof("Generating ACME Account...")
|
|
||||||
account, err = NewAccount(a.Email)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
needRegister = true
|
|
||||||
}
|
|
||||||
|
|
||||||
a.client, err = a.buildACMEClient(account)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if needRegister {
|
|
||||||
// New users will need to register; be sure to save it
|
|
||||||
log.Infof("Register...")
|
|
||||||
reg, err := a.client.Register()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
account.Registration = reg
|
|
||||||
}
|
|
||||||
|
|
||||||
// The client has a URL to the current Let's Encrypt Subscriber
|
|
||||||
// Agreement. The user will need to agree to it.
|
|
||||||
log.Debugf("AgreeToTOS...")
|
|
||||||
err = a.client.AgreeToTOS()
|
|
||||||
if err != nil {
|
|
||||||
// Let's Encrypt Subscriber Agreement renew ?
|
|
||||||
reg, err := a.client.QueryRegistration()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
account.Registration = reg
|
|
||||||
err = a.client.AgreeToTOS()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error sending ACME agreement to TOS: %+v: %s", account, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// save account
|
|
||||||
transaction, _, err := a.store.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = transaction.Commit(account)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
a.retrieveCertificates()
|
|
||||||
a.renewCertificates()
|
|
||||||
a.runJobs()
|
|
||||||
|
|
||||||
ticker := time.NewTicker(24 * time.Hour)
|
|
||||||
safe.Go(func() {
|
|
||||||
for range ticker.C {
|
|
||||||
a.renewCertificates()
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
||||||
domain := types.CanonicalDomain(clientHello.ServerName)
|
|
||||||
account := a.store.Get().(*Account)
|
|
||||||
|
|
||||||
if providedCertificate := a.getProvidedCertificate([]string{domain}); providedCertificate != nil {
|
|
||||||
return providedCertificate, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if challengeCert, ok := a.challengeProvider.getCertificate(domain); ok {
|
|
||||||
log.Debugf("ACME got challenge %s", domain)
|
|
||||||
return challengeCert, nil
|
|
||||||
}
|
|
||||||
if domainCert, ok := account.DomainsCertificate.getCertificateForDomain(domain); ok {
|
|
||||||
log.Debugf("ACME got domain cert %s", domain)
|
|
||||||
return domainCert.tlsCert, nil
|
|
||||||
}
|
|
||||||
if a.OnDemand {
|
|
||||||
if a.checkOnDemandDomain != nil && !a.checkOnDemandDomain(domain) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return a.loadCertificateOnDemand(clientHello)
|
|
||||||
}
|
|
||||||
log.Debugf("ACME got nothing %s", domain)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ACME) retrieveCertificates() {
|
|
||||||
a.jobs.In() <- func() {
|
|
||||||
log.Infof("Retrieving ACME certificates...")
|
|
||||||
for _, domain := range a.Domains {
|
|
||||||
// check if cert isn't already loaded
|
|
||||||
account := a.store.Get().(*Account)
|
|
||||||
if _, exists := account.DomainsCertificate.exists(domain); !exists {
|
|
||||||
domains := []string{}
|
|
||||||
domains = append(domains, domain.Main)
|
|
||||||
domains = append(domains, domain.SANs...)
|
|
||||||
certificateResource, err := a.getDomainsCertificates(domains)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error getting ACME certificate for domain %s: %s", domains, err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
transaction, object, err := a.store.Begin()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error creating ACME store transaction from domain %s: %s", domain, err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
account = object.(*Account)
|
|
||||||
_, err = account.DomainsCertificate.addCertificateForDomains(certificateResource, domain)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error adding ACME certificate for domain %s: %s", domains, err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = transaction.Commit(account); err != nil {
|
|
||||||
log.Errorf("Error Saving ACME account %+v: %s", account, err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Infof("Retrieved ACME certificates")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ACME) renewCertificates() {
|
|
||||||
a.jobs.In() <- func() {
|
|
||||||
log.Debugf("Testing certificate renew...")
|
|
||||||
account := a.store.Get().(*Account)
|
|
||||||
for _, certificateResource := range account.DomainsCertificate.Certs {
|
|
||||||
if certificateResource.needRenew() {
|
|
||||||
log.Debugf("Renewing certificate %+v", certificateResource.Domains)
|
|
||||||
renewedCert, err := a.client.RenewCertificate(acme.CertificateResource{
|
|
||||||
Domain: certificateResource.Certificate.Domain,
|
|
||||||
CertURL: certificateResource.Certificate.CertURL,
|
|
||||||
CertStableURL: certificateResource.Certificate.CertStableURL,
|
|
||||||
PrivateKey: certificateResource.Certificate.PrivateKey,
|
|
||||||
Certificate: certificateResource.Certificate.Certificate,
|
|
||||||
}, true, OSCPMustStaple)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error renewing certificate: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Debugf("Renewed certificate %+v", certificateResource.Domains)
|
|
||||||
renewedACMECert := &Certificate{
|
|
||||||
Domain: renewedCert.Domain,
|
|
||||||
CertURL: renewedCert.CertURL,
|
|
||||||
CertStableURL: renewedCert.CertStableURL,
|
|
||||||
PrivateKey: renewedCert.PrivateKey,
|
|
||||||
Certificate: renewedCert.Certificate,
|
|
||||||
}
|
|
||||||
transaction, object, err := a.store.Begin()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error renewing certificate: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
account = object.(*Account)
|
|
||||||
err = account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error renewing certificate: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = transaction.Commit(account); err != nil {
|
|
||||||
log.Errorf("Error Saving ACME account %+v: %s", account, err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dnsOverrideDelay(delay int) error {
|
|
||||||
var err error
|
|
||||||
if delay > 0 {
|
|
||||||
log.Debugf("Delaying %d seconds rather than validating DNS propagation", delay)
|
|
||||||
acme.PreCheckDNS = func(_, _ string) (bool, error) {
|
|
||||||
time.Sleep(time.Duration(delay) * time.Second)
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
} else if delay < 0 {
|
|
||||||
err = fmt.Errorf("Invalid negative DelayDontCheckDNS: %d", delay)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ACME) buildACMEClient(account *Account) (*acme.Client, error) {
|
|
||||||
log.Debugf("Building ACME client...")
|
|
||||||
caServer := "https://acme-v01.api.letsencrypt.org/directory"
|
|
||||||
if len(a.CAServer) > 0 {
|
|
||||||
caServer = a.CAServer
|
|
||||||
}
|
|
||||||
client, err := acme.NewClient(caServer, account, acme.RSA4096)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(a.DNSProvider) > 0 {
|
|
||||||
log.Debugf("Using DNS Challenge provider: %s", a.DNSProvider)
|
|
||||||
|
|
||||||
err = dnsOverrideDelay(a.DelayDontCheckDNS)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var provider acme.ChallengeProvider
|
|
||||||
provider, err = dns.NewDNSChallengeProviderByName(a.DNSProvider)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
|
|
||||||
err = client.SetChallengeProvider(acme.DNS01, provider)
|
|
||||||
} else {
|
|
||||||
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01})
|
|
||||||
err = client.SetChallengeProvider(acme.TLSSNI01, a.challengeProvider)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
||||||
domain := types.CanonicalDomain(clientHello.ServerName)
|
|
||||||
account := a.store.Get().(*Account)
|
|
||||||
if certificateResource, ok := account.DomainsCertificate.getCertificateForDomain(domain); ok {
|
|
||||||
return certificateResource.tlsCert, nil
|
|
||||||
}
|
|
||||||
certificate, err := a.getDomainsCertificates([]string{domain})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Debugf("Got certificate on demand for domain %s", domain)
|
|
||||||
|
|
||||||
transaction, object, err := a.store.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
account = object.(*Account)
|
|
||||||
cert, err := account.DomainsCertificate.addCertificateForDomains(certificate, Domain{Main: domain})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err = transaction.Commit(account); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return cert.tlsCert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadCertificateForDomains loads certificates from ACME for given domains
|
|
||||||
func (a *ACME) LoadCertificateForDomains(domains []string) {
|
|
||||||
a.jobs.In() <- func() {
|
|
||||||
log.Debugf("LoadCertificateForDomains %v...", domains)
|
|
||||||
|
|
||||||
if len(domains) == 0 {
|
|
||||||
// no domain
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
domains = fun.Map(types.CanonicalDomain, domains).([]string)
|
|
||||||
|
|
||||||
// Check provided certificates
|
|
||||||
if a.getProvidedCertificate(domains) != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
operation := func() error {
|
|
||||||
if a.client == nil {
|
|
||||||
return fmt.Errorf("ACME client still not built")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
notify := func(err error, time time.Duration) {
|
|
||||||
log.Errorf("Error getting ACME client: %v, retrying in %s", err, time)
|
|
||||||
}
|
|
||||||
ebo := backoff.NewExponentialBackOff()
|
|
||||||
ebo.MaxElapsedTime = 30 * time.Second
|
|
||||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), ebo, notify)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error getting ACME client: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
account := a.store.Get().(*Account)
|
|
||||||
var domain Domain
|
|
||||||
if len(domains) > 1 {
|
|
||||||
domain = Domain{Main: domains[0], SANs: domains[1:]}
|
|
||||||
} else {
|
|
||||||
domain = Domain{Main: domains[0]}
|
|
||||||
}
|
|
||||||
if _, exists := account.DomainsCertificate.exists(domain); exists {
|
|
||||||
// domain already exists
|
|
||||||
return
|
|
||||||
}
|
|
||||||
certificate, err := a.getDomainsCertificates(domains)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error getting ACME certificates %+v : %v", domains, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Debugf("Got certificate for domains %+v", domains)
|
|
||||||
transaction, object, err := a.store.Begin()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error creating transaction %+v : %v", domains, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
account = object.(*Account)
|
|
||||||
_, err = account.DomainsCertificate.addCertificateForDomains(certificate, domain)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error adding ACME certificates %+v : %v", domains, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = transaction.Commit(account); err != nil {
|
|
||||||
log.Errorf("Error Saving ACME account %+v: %v", account, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get provided certificate which check a domains list (Main and SANs)
|
|
||||||
func (a *ACME) getProvidedCertificate(domains []string) *tls.Certificate {
|
|
||||||
// Use regex to test for provided certs that might have been added into TLSConfig
|
|
||||||
providedCertMatch := false
|
|
||||||
log.Debugf("Look for provided certificate to validate %s...", domains)
|
|
||||||
for k := range a.TLSConfig.NameToCertificate {
|
|
||||||
selector := "^" + strings.Replace(k, "*.", "[^\\.]*\\.?", -1) + "$"
|
|
||||||
for _, domainToCheck := range domains {
|
|
||||||
providedCertMatch, _ = regexp.MatchString(selector, domainToCheck)
|
|
||||||
if !providedCertMatch {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if providedCertMatch {
|
|
||||||
log.Debugf("Got provided certificate for domains %s", domains)
|
|
||||||
return a.TLSConfig.NameToCertificate[k]
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Debugf("No provided certificate found for domains %s, get ACME certificate.", domains)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ACME) getDomainsCertificates(domains []string) (*Certificate, error) {
|
|
||||||
domains = fun.Map(types.CanonicalDomain, domains).([]string)
|
|
||||||
log.Debugf("Loading ACME certificates %s...", domains)
|
|
||||||
bundle := true
|
|
||||||
certificate, failures := a.client.ObtainCertificate(domains, bundle, nil, OSCPMustStaple)
|
|
||||||
if len(failures) > 0 {
|
|
||||||
log.Error(failures)
|
|
||||||
return nil, fmt.Errorf("Cannot obtain certificates %s+v", failures)
|
|
||||||
}
|
|
||||||
log.Debugf("Loaded ACME certificates %s", domains)
|
|
||||||
return &Certificate{
|
|
||||||
Domain: certificate.Domain,
|
|
||||||
CertURL: certificate.CertURL,
|
|
||||||
CertStableURL: certificate.CertStableURL,
|
|
||||||
PrivateKey: certificate.PrivateKey,
|
|
||||||
Certificate: certificate.Certificate,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ACME) runJobs() {
|
|
||||||
safe.Go(func() {
|
|
||||||
for job := range a.jobs.Out() {
|
|
||||||
function := job.(func())
|
|
||||||
function()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@@ -1,296 +0,0 @@
|
|||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/base64"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"reflect"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/xenolf/lego/acme"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDomainsSet(t *testing.T) {
|
|
||||||
checkMap := map[string]Domains{
|
|
||||||
"": {},
|
|
||||||
"foo.com": {Domain{Main: "foo.com", SANs: []string{}}},
|
|
||||||
"foo.com,bar.net": {Domain{Main: "foo.com", SANs: []string{"bar.net"}}},
|
|
||||||
"foo.com,bar1.net,bar2.net,bar3.net": {Domain{Main: "foo.com", SANs: []string{"bar1.net", "bar2.net", "bar3.net"}}},
|
|
||||||
}
|
|
||||||
for in, check := range checkMap {
|
|
||||||
ds := Domains{}
|
|
||||||
ds.Set(in)
|
|
||||||
if !reflect.DeepEqual(check, ds) {
|
|
||||||
t.Errorf("Expected %+v\nGot %+v", check, ds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDomainsSetAppend(t *testing.T) {
|
|
||||||
inSlice := []string{
|
|
||||||
"",
|
|
||||||
"foo1.com",
|
|
||||||
"foo2.com,bar.net",
|
|
||||||
"foo3.com,bar1.net,bar2.net,bar3.net",
|
|
||||||
}
|
|
||||||
checkSlice := []Domains{
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
Domain{
|
|
||||||
Main: "foo1.com",
|
|
||||||
SANs: []string{}}},
|
|
||||||
{
|
|
||||||
Domain{
|
|
||||||
Main: "foo1.com",
|
|
||||||
SANs: []string{}},
|
|
||||||
Domain{
|
|
||||||
Main: "foo2.com",
|
|
||||||
SANs: []string{"bar.net"}}},
|
|
||||||
{
|
|
||||||
Domain{
|
|
||||||
Main: "foo1.com",
|
|
||||||
SANs: []string{}},
|
|
||||||
Domain{
|
|
||||||
Main: "foo2.com",
|
|
||||||
SANs: []string{"bar.net"}},
|
|
||||||
Domain{Main: "foo3.com",
|
|
||||||
SANs: []string{"bar1.net", "bar2.net", "bar3.net"}}},
|
|
||||||
}
|
|
||||||
ds := Domains{}
|
|
||||||
for i, in := range inSlice {
|
|
||||||
ds.Set(in)
|
|
||||||
if !reflect.DeepEqual(checkSlice[i], ds) {
|
|
||||||
t.Errorf("Expected %s %+v\nGot %+v", in, checkSlice[i], ds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCertificatesRenew(t *testing.T) {
|
|
||||||
foo1Cert, foo1Key, _ := generateKeyPair("foo1.com", time.Now())
|
|
||||||
foo2Cert, foo2Key, _ := generateKeyPair("foo2.com", time.Now())
|
|
||||||
domainsCertificates := DomainsCertificates{
|
|
||||||
lock: sync.RWMutex{},
|
|
||||||
Certs: []*DomainsCertificate{
|
|
||||||
{
|
|
||||||
Domains: Domain{
|
|
||||||
Main: "foo1.com",
|
|
||||||
SANs: []string{}},
|
|
||||||
Certificate: &Certificate{
|
|
||||||
Domain: "foo1.com",
|
|
||||||
CertURL: "url",
|
|
||||||
CertStableURL: "url",
|
|
||||||
PrivateKey: foo1Key,
|
|
||||||
Certificate: foo1Cert,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Domains: Domain{
|
|
||||||
Main: "foo2.com",
|
|
||||||
SANs: []string{}},
|
|
||||||
Certificate: &Certificate{
|
|
||||||
Domain: "foo2.com",
|
|
||||||
CertURL: "url",
|
|
||||||
CertStableURL: "url",
|
|
||||||
PrivateKey: foo2Key,
|
|
||||||
Certificate: foo2Cert,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
foo1Cert, foo1Key, _ = generateKeyPair("foo1.com", time.Now())
|
|
||||||
newCertificate := &Certificate{
|
|
||||||
Domain: "foo1.com",
|
|
||||||
CertURL: "url",
|
|
||||||
CertStableURL: "url",
|
|
||||||
PrivateKey: foo1Key,
|
|
||||||
Certificate: foo1Cert,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := domainsCertificates.renewCertificates(
|
|
||||||
newCertificate,
|
|
||||||
Domain{
|
|
||||||
Main: "foo1.com",
|
|
||||||
SANs: []string{}})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error in renewCertificates :%v", err)
|
|
||||||
}
|
|
||||||
if len(domainsCertificates.Certs) != 2 {
|
|
||||||
t.Errorf("Expected domainsCertificates length %d %+v\nGot %+v", 2, domainsCertificates.Certs, len(domainsCertificates.Certs))
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(domainsCertificates.Certs[0].Certificate, newCertificate) {
|
|
||||||
t.Errorf("Expected new certificate %+v \nGot %+v", newCertificate, domainsCertificates.Certs[0].Certificate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveDuplicates(t *testing.T) {
|
|
||||||
now := time.Now()
|
|
||||||
fooCert, fooKey, _ := generateKeyPair("foo.com", now)
|
|
||||||
foo24Cert, foo24Key, _ := generateKeyPair("foo.com", now.Add(24*time.Hour))
|
|
||||||
foo48Cert, foo48Key, _ := generateKeyPair("foo.com", now.Add(48*time.Hour))
|
|
||||||
barCert, barKey, _ := generateKeyPair("bar.com", now)
|
|
||||||
domainsCertificates := DomainsCertificates{
|
|
||||||
lock: sync.RWMutex{},
|
|
||||||
Certs: []*DomainsCertificate{
|
|
||||||
{
|
|
||||||
Domains: Domain{
|
|
||||||
Main: "foo.com",
|
|
||||||
SANs: []string{}},
|
|
||||||
Certificate: &Certificate{
|
|
||||||
Domain: "foo.com",
|
|
||||||
CertURL: "url",
|
|
||||||
CertStableURL: "url",
|
|
||||||
PrivateKey: foo24Key,
|
|
||||||
Certificate: foo24Cert,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Domains: Domain{
|
|
||||||
Main: "foo.com",
|
|
||||||
SANs: []string{}},
|
|
||||||
Certificate: &Certificate{
|
|
||||||
Domain: "foo.com",
|
|
||||||
CertURL: "url",
|
|
||||||
CertStableURL: "url",
|
|
||||||
PrivateKey: foo48Key,
|
|
||||||
Certificate: foo48Cert,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Domains: Domain{
|
|
||||||
Main: "foo.com",
|
|
||||||
SANs: []string{}},
|
|
||||||
Certificate: &Certificate{
|
|
||||||
Domain: "foo.com",
|
|
||||||
CertURL: "url",
|
|
||||||
CertStableURL: "url",
|
|
||||||
PrivateKey: fooKey,
|
|
||||||
Certificate: fooCert,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Domains: Domain{
|
|
||||||
Main: "bar.com",
|
|
||||||
SANs: []string{}},
|
|
||||||
Certificate: &Certificate{
|
|
||||||
Domain: "bar.com",
|
|
||||||
CertURL: "url",
|
|
||||||
CertStableURL: "url",
|
|
||||||
PrivateKey: barKey,
|
|
||||||
Certificate: barCert,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Domains: Domain{
|
|
||||||
Main: "foo.com",
|
|
||||||
SANs: []string{}},
|
|
||||||
Certificate: &Certificate{
|
|
||||||
Domain: "foo.com",
|
|
||||||
CertURL: "url",
|
|
||||||
CertStableURL: "url",
|
|
||||||
PrivateKey: foo48Key,
|
|
||||||
Certificate: foo48Cert,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
domainsCertificates.Init()
|
|
||||||
|
|
||||||
if len(domainsCertificates.Certs) != 2 {
|
|
||||||
t.Errorf("Expected domainsCertificates length %d %+v\nGot %+v", 2, domainsCertificates.Certs, len(domainsCertificates.Certs))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, cert := range domainsCertificates.Certs {
|
|
||||||
switch cert.Domains.Main {
|
|
||||||
case "bar.com":
|
|
||||||
continue
|
|
||||||
case "foo.com":
|
|
||||||
if !cert.tlsCert.Leaf.NotAfter.Equal(now.Add(48 * time.Hour).Truncate(1 * time.Second)) {
|
|
||||||
t.Errorf("Bad expiration %s date for domain %+v, now %s", cert.tlsCert.Leaf.NotAfter.String(), cert, now.Add(48*time.Hour).Truncate(1*time.Second).String())
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Errorf("Unknown domain %+v", cert)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNoPreCheckOverride(t *testing.T) {
|
|
||||||
acme.PreCheckDNS = nil // Irreversable - but not expecting real calls into this during testing process
|
|
||||||
err := dnsOverrideDelay(0)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error in dnsOverrideDelay :%v", err)
|
|
||||||
}
|
|
||||||
if acme.PreCheckDNS != nil {
|
|
||||||
t.Errorf("Unexpected change to acme.PreCheckDNS when leaving DNS verification as is.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSillyPreCheckOverride(t *testing.T) {
|
|
||||||
err := dnsOverrideDelay(-5)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Missing expected error in dnsOverrideDelay!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPreCheckOverride(t *testing.T) {
|
|
||||||
acme.PreCheckDNS = nil // Irreversable - but not expecting real calls into this during testing process
|
|
||||||
err := dnsOverrideDelay(5)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error in dnsOverrideDelay :%v", err)
|
|
||||||
}
|
|
||||||
if acme.PreCheckDNS == nil {
|
|
||||||
t.Errorf("No change to acme.PreCheckDNS when meant to be adding enforcing override function.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAcmeClientCreation(t *testing.T) {
|
|
||||||
acme.PreCheckDNS = nil // Irreversable - but not expecting real calls into this during testing process
|
|
||||||
// Lengthy setup to avoid external web requests - oh for easier golang testing!
|
|
||||||
account := &Account{Email: "f@f"}
|
|
||||||
account.PrivateKey, _ = base64.StdEncoding.DecodeString(`
|
|
||||||
MIIBPAIBAAJBAMp2Ni92FfEur+CAvFkgC12LT4l9D53ApbBpDaXaJkzzks+KsLw9zyAxvlrfAyTCQ
|
|
||||||
7tDnEnIltAXyQ0uOFUUdcMCAwEAAQJAK1FbipATZcT9cGVa5x7KD7usytftLW14heQUPXYNV80r/3
|
|
||||||
lmnpvjL06dffRpwkYeN8DATQF/QOcy3NNNGDw/4QIhAPAKmiZFxA/qmRXsuU8Zhlzf16WrNZ68K64
|
|
||||||
asn/h3qZrAiEA1+wFR3WXCPIolOvd7AHjfgcTKQNkoMPywU4FYUNQ1AkCIQDv8yk0qPjckD6HVCPJ
|
|
||||||
llJh9MC0svjevGtNlxJoE3lmEQIhAKXy1wfZ32/XtcrnENPvi6lzxI0T94X7s5pP3aCoPPoJAiEAl
|
|
||||||
cijFkALeQp/qyeXdFld2v9gUN3eCgljgcl0QweRoIc=---`)
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Write([]byte(`{
|
|
||||||
"new-authz": "https://foo/acme/new-authz",
|
|
||||||
"new-cert": "https://foo/acme/new-cert",
|
|
||||||
"new-reg": "https://foo/acme/new-reg",
|
|
||||||
"revoke-cert": "https://foo/acme/revoke-cert"
|
|
||||||
}`))
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
a := ACME{DNSProvider: "manual", DelayDontCheckDNS: 10, CAServer: ts.URL}
|
|
||||||
|
|
||||||
client, err := a.buildACMEClient(account)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error in buildACMEClient: %v", err)
|
|
||||||
}
|
|
||||||
if client == nil {
|
|
||||||
t.Errorf("No client from buildACMEClient!")
|
|
||||||
}
|
|
||||||
if acme.PreCheckDNS == nil {
|
|
||||||
t.Errorf("No change to acme.PreCheckDNS when meant to be adding enforcing override function.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAcme_getProvidedCertificate(t *testing.T) {
|
|
||||||
mm := make(map[string]*tls.Certificate)
|
|
||||||
mm["*.containo.us"] = &tls.Certificate{}
|
|
||||||
mm["traefik.acme.io"] = &tls.Certificate{}
|
|
||||||
|
|
||||||
a := ACME{TLSConfig: &tls.Config{NameToCertificate: mm}}
|
|
||||||
|
|
||||||
domains := []string{"traefik.containo.us", "trae.containo.us"}
|
|
||||||
certificate := a.getProvidedCertificate(domains)
|
|
||||||
assert.NotNil(t, certificate)
|
|
||||||
domains = []string{"traefik.acme.io", "trae.acme.io"}
|
|
||||||
certificate = a.getProvidedCertificate(domains)
|
|
||||||
assert.Nil(t, certificate)
|
|
||||||
}
|
|
@@ -1,97 +0,0 @@
|
|||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/cenk/backoff"
|
|
||||||
"github.com/containous/traefik/cluster"
|
|
||||||
"github.com/containous/traefik/log"
|
|
||||||
"github.com/containous/traefik/safe"
|
|
||||||
"github.com/xenolf/lego/acme"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ acme.ChallengeProviderTimeout = (*challengeProvider)(nil)
|
|
||||||
|
|
||||||
type challengeProvider struct {
|
|
||||||
store cluster.Store
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *challengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) {
|
|
||||||
log.Debugf("Challenge GetCertificate %s", domain)
|
|
||||||
if !strings.HasSuffix(domain, ".acme.invalid") {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
c.lock.RLock()
|
|
||||||
defer c.lock.RUnlock()
|
|
||||||
account := c.store.Get().(*Account)
|
|
||||||
if account.ChallengeCerts == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
account.Init()
|
|
||||||
var result *tls.Certificate
|
|
||||||
operation := func() error {
|
|
||||||
for _, cert := range account.ChallengeCerts {
|
|
||||||
for _, dns := range cert.certificate.Leaf.DNSNames {
|
|
||||||
if domain == dns {
|
|
||||||
result = cert.certificate
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("Cannot find challenge cert for domain %s", domain)
|
|
||||||
}
|
|
||||||
notify := func(err error, time time.Duration) {
|
|
||||||
log.Errorf("Error getting cert: %v, retrying in %s", err, time)
|
|
||||||
}
|
|
||||||
ebo := backoff.NewExponentialBackOff()
|
|
||||||
ebo.MaxElapsedTime = 60 * time.Second
|
|
||||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), ebo, notify)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error getting cert: %v", err)
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return result, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *challengeProvider) Present(domain, token, keyAuth string) error {
|
|
||||||
log.Debugf("Challenge Present %s", domain)
|
|
||||||
cert, _, err := TLSSNI01ChallengeCert(keyAuth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
transaction, object, err := c.store.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
account := object.(*Account)
|
|
||||||
if account.ChallengeCerts == nil {
|
|
||||||
account.ChallengeCerts = map[string]*ChallengeCert{}
|
|
||||||
}
|
|
||||||
account.ChallengeCerts[domain] = &cert
|
|
||||||
return transaction.Commit(account)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *challengeProvider) CleanUp(domain, token, keyAuth string) error {
|
|
||||||
log.Debugf("Challenge CleanUp %s", domain)
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
transaction, object, err := c.store.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
account := object.(*Account)
|
|
||||||
delete(account.ChallengeCerts, domain)
|
|
||||||
return transaction.Commit(account)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *challengeProvider) Timeout() (timeout, interval time.Duration) {
|
|
||||||
return 60 * time.Second, 5 * time.Second
|
|
||||||
}
|
|
135
acme/crypto.go
@@ -1,135 +0,0 @@
|
|||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func generateDefaultCertificate() (*tls.Certificate, error) {
|
|
||||||
randomBytes := make([]byte, 100)
|
|
||||||
_, err := rand.Read(randomBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
zBytes := sha256.Sum256(randomBytes)
|
|
||||||
z := hex.EncodeToString(zBytes[:sha256.Size])
|
|
||||||
domain := fmt.Sprintf("%s.%s.traefik.default", z[:32], z[32:])
|
|
||||||
|
|
||||||
certPEM, keyPEM, err := generateKeyPair(domain, time.Time{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
certificate, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &certificate, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateKeyPair(domain string, expiration time.Time) ([]byte, []byte, error) {
|
|
||||||
rsaPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rsaPrivKey)})
|
|
||||||
|
|
||||||
certPEM, err := generatePemCert(rsaPrivKey, domain, expiration)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return certPEM, keyPEM, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generatePemCert(privKey *rsa.PrivateKey, domain string, expiration time.Time) ([]byte, error) {
|
|
||||||
derBytes, err := generateDerCert(privKey, expiration, domain)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) {
|
|
||||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
||||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if expiration.IsZero() {
|
|
||||||
expiration = time.Now().Add(365)
|
|
||||||
}
|
|
||||||
|
|
||||||
template := x509.Certificate{
|
|
||||||
SerialNumber: serialNumber,
|
|
||||||
Subject: pkix.Name{
|
|
||||||
CommonName: "TRAEFIK DEFAULT CERT",
|
|
||||||
},
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: expiration,
|
|
||||||
|
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment,
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
DNSNames: []string{domain},
|
|
||||||
}
|
|
||||||
|
|
||||||
return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TLSSNI01ChallengeCert returns a certificate and target domain for the `tls-sni-01` challenge
|
|
||||||
func TLSSNI01ChallengeCert(keyAuth string) (ChallengeCert, string, error) {
|
|
||||||
// generate a new RSA key for the certificates
|
|
||||||
var tempPrivKey crypto.PrivateKey
|
|
||||||
tempPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
return ChallengeCert{}, "", err
|
|
||||||
}
|
|
||||||
rsaPrivKey := tempPrivKey.(*rsa.PrivateKey)
|
|
||||||
rsaPrivPEM := pemEncode(rsaPrivKey)
|
|
||||||
|
|
||||||
zBytes := sha256.Sum256([]byte(keyAuth))
|
|
||||||
z := hex.EncodeToString(zBytes[:sha256.Size])
|
|
||||||
domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
|
|
||||||
tempCertPEM, err := generatePemCert(rsaPrivKey, domain, time.Time{})
|
|
||||||
if err != nil {
|
|
||||||
return ChallengeCert{}, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
|
|
||||||
if err != nil {
|
|
||||||
return ChallengeCert{}, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ChallengeCert{Certificate: tempCertPEM, PrivateKey: rsaPrivPEM, certificate: &certificate}, domain, nil
|
|
||||||
}
|
|
||||||
func pemEncode(data interface{}) []byte {
|
|
||||||
var pemBlock *pem.Block
|
|
||||||
switch key := data.(type) {
|
|
||||||
case *ecdsa.PrivateKey:
|
|
||||||
keyBytes, _ := x509.MarshalECPrivateKey(key)
|
|
||||||
pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
|
|
||||||
case *rsa.PrivateKey:
|
|
||||||
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
|
|
||||||
break
|
|
||||||
case *x509.CertificateRequest:
|
|
||||||
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
|
|
||||||
break
|
|
||||||
case []byte:
|
|
||||||
pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.([]byte))}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pem.EncodeToMemory(pemBlock)
|
|
||||||
}
|
|
@@ -1,97 +0,0 @@
|
|||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/containous/traefik/cluster"
|
|
||||||
"github.com/containous/traefik/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ cluster.Store = (*LocalStore)(nil)
|
|
||||||
|
|
||||||
// LocalStore is a store using a file as storage
|
|
||||||
type LocalStore struct {
|
|
||||||
file string
|
|
||||||
storageLock sync.RWMutex
|
|
||||||
account *Account
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLocalStore create a LocalStore
|
|
||||||
func NewLocalStore(file string) *LocalStore {
|
|
||||||
return &LocalStore{
|
|
||||||
file: file,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get atomically a struct from the file storage
|
|
||||||
func (s *LocalStore) Get() cluster.Object {
|
|
||||||
s.storageLock.RLock()
|
|
||||||
defer s.storageLock.RUnlock()
|
|
||||||
return s.account
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load loads file into store
|
|
||||||
func (s *LocalStore) Load() (cluster.Object, error) {
|
|
||||||
s.storageLock.Lock()
|
|
||||||
defer s.storageLock.Unlock()
|
|
||||||
account := &Account{}
|
|
||||||
|
|
||||||
err := checkPermissions(s.file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
f, err := os.Open(s.file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
file, err := ioutil.ReadAll(f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(file, &account); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
account.Init()
|
|
||||||
s.account = account
|
|
||||||
log.Infof("Loaded ACME config from store %s", s.file)
|
|
||||||
return account, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Begin creates a transaction with the KV store.
|
|
||||||
func (s *LocalStore) Begin() (cluster.Transaction, cluster.Object, error) {
|
|
||||||
s.storageLock.Lock()
|
|
||||||
return &localTransaction{LocalStore: s}, s.account, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ cluster.Transaction = (*localTransaction)(nil)
|
|
||||||
|
|
||||||
type localTransaction struct {
|
|
||||||
*LocalStore
|
|
||||||
dirty bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit allows to set an object in the file storage
|
|
||||||
func (t *localTransaction) Commit(object cluster.Object) error {
|
|
||||||
t.LocalStore.account = object.(*Account)
|
|
||||||
defer t.storageLock.Unlock()
|
|
||||||
if t.dirty {
|
|
||||||
return fmt.Errorf("transaction already used, please begin a new one")
|
|
||||||
}
|
|
||||||
|
|
||||||
// write account to file
|
|
||||||
data, err := json.MarshalIndent(object, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = ioutil.WriteFile(t.file, data, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.dirty = true
|
|
||||||
return nil
|
|
||||||
}
|
|
@@ -1,25 +0,0 @@
|
|||||||
// +build !windows
|
|
||||||
|
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check file permissions
|
|
||||||
func checkPermissions(name string) error {
|
|
||||||
f, err := os.Open(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
fi, err := f.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if fi.Mode().Perm()&0077 != 0 {
|
|
||||||
return fmt.Errorf("permissions %o for %s are too open, please use 600", fi.Mode().Perm(), name)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@@ -1,6 +0,0 @@
|
|||||||
package acme
|
|
||||||
|
|
||||||
// Do not check file permissions on Windows right now
|
|
||||||
func checkPermissions(name string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@@ -1,35 +1,37 @@
|
|||||||
FROM golang:1.8
|
FROM golang:1.13-alpine
|
||||||
|
|
||||||
# Install a more recent version of mercurial to avoid mismatching results
|
RUN apk --update upgrade \
|
||||||
# between glide run on a decently updated host system and the build container.
|
&& apk --no-cache --no-progress add git mercurial bash gcc musl-dev curl tar ca-certificates tzdata \
|
||||||
RUN awk '$1 ~ "^deb" { $3 = $3 "-backports"; print; exit }' /etc/apt/sources.list > /etc/apt/sources.list.d/backports.list && \
|
&& update-ca-certificates \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get update && \
|
&& rm -rf /var/cache/apk/*
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -t jessie-backports --yes --no-install-recommends mercurial=3.9.1-1~bpo8+1 && \
|
|
||||||
rm -fr /var/lib/apt/lists/
|
|
||||||
|
|
||||||
RUN go get github.com/jteeuwen/go-bindata/... \
|
|
||||||
&& go get github.com/golang/lint/golint \
|
|
||||||
&& go get github.com/kisielk/errcheck \
|
|
||||||
&& go get github.com/client9/misspell/cmd/misspell \
|
|
||||||
&& go get github.com/mattfarina/glide-hash \
|
|
||||||
&& go get github.com/sgotti/glide-vc
|
|
||||||
|
|
||||||
# Which docker version to test on
|
# Which docker version to test on
|
||||||
ARG DOCKER_VERSION=17.03.1
|
ARG DOCKER_VERSION=18.09.7
|
||||||
|
|
||||||
|
|
||||||
# Which glide version to test on
|
|
||||||
ARG GLIDE_VERSION=v0.12.3
|
|
||||||
|
|
||||||
# Download glide
|
|
||||||
RUN mkdir -p /usr/local/bin \
|
|
||||||
&& curl -fL https://github.com/Masterminds/glide/releases/download/${GLIDE_VERSION}/glide-${GLIDE_VERSION}-linux-amd64.tar.gz \
|
|
||||||
| tar -xzC /usr/local/bin --transform 's#^.+/##x'
|
|
||||||
|
|
||||||
# Download docker
|
# Download docker
|
||||||
RUN mkdir -p /usr/local/bin \
|
RUN mkdir -p /usr/local/bin \
|
||||||
&& curl -fL https://get.docker.com/builds/Linux/x86_64/docker-${DOCKER_VERSION}-ce.tgz \
|
&& curl -fL https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz \
|
||||||
| tar -xzC /usr/local/bin --transform 's#^.+/##x'
|
| tar -xzC /usr/local/bin --transform 's#^.+/##x'
|
||||||
|
|
||||||
|
# Download go-bindata binary to bin folder in $GOPATH
|
||||||
|
RUN mkdir -p /usr/local/bin \
|
||||||
|
&& curl -fsSL -o /usr/local/bin/go-bindata https://github.com/containous/go-bindata/releases/download/v1.0.0/go-bindata \
|
||||||
|
&& chmod +x /usr/local/bin/go-bindata
|
||||||
|
|
||||||
|
# Download golangci-lint binary to bin folder in $GOPATH
|
||||||
|
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.18.0
|
||||||
|
|
||||||
|
# Download golangci-lint and misspell binary to bin folder in $GOPATH
|
||||||
|
RUN GO111MODULE=off go get github.com/client9/misspell/cmd/misspell
|
||||||
|
|
||||||
|
# Download goreleaser binary to bin folder in $GOPATH
|
||||||
|
RUN curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh
|
||||||
|
|
||||||
WORKDIR /go/src/github.com/containous/traefik
|
WORKDIR /go/src/github.com/containous/traefik
|
||||||
|
|
||||||
|
# Download go modules
|
||||||
|
COPY go.mod .
|
||||||
|
COPY go.sum .
|
||||||
|
RUN GO111MODULE=on GOPROXY=https://proxy.golang.org go mod download
|
||||||
|
|
||||||
COPY . /go/src/github.com/containous/traefik
|
COPY . /go/src/github.com/containous/traefik
|
||||||
|
@@ -1,255 +0,0 @@
|
|||||||
package cluster
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/cenk/backoff"
|
|
||||||
"github.com/containous/staert"
|
|
||||||
"github.com/containous/traefik/job"
|
|
||||||
"github.com/containous/traefik/log"
|
|
||||||
"github.com/containous/traefik/safe"
|
|
||||||
"github.com/docker/libkv/store"
|
|
||||||
"github.com/satori/go.uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Metadata stores Object plus metadata
|
|
||||||
type Metadata struct {
|
|
||||||
object Object
|
|
||||||
Object []byte
|
|
||||||
Lock string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMetadata returns new Metadata
|
|
||||||
func NewMetadata(object Object) *Metadata {
|
|
||||||
return &Metadata{object: object}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshall marshalls object
|
|
||||||
func (m *Metadata) Marshall() error {
|
|
||||||
var err error
|
|
||||||
m.Object, err = json.Marshal(m.object)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Metadata) unmarshall() error {
|
|
||||||
if len(m.Object) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return json.Unmarshal(m.Object, m.object)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listener is called when Object has been changed in KV store
|
|
||||||
type Listener func(Object) error
|
|
||||||
|
|
||||||
var _ Store = (*Datastore)(nil)
|
|
||||||
|
|
||||||
// Datastore holds a struct synced in a KV store
|
|
||||||
type Datastore struct {
|
|
||||||
kv staert.KvSource
|
|
||||||
ctx context.Context
|
|
||||||
localLock *sync.RWMutex
|
|
||||||
meta *Metadata
|
|
||||||
lockKey string
|
|
||||||
listener Listener
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDataStore creates a Datastore
|
|
||||||
func NewDataStore(ctx context.Context, kvSource staert.KvSource, object Object, listener Listener) (*Datastore, error) {
|
|
||||||
datastore := Datastore{
|
|
||||||
kv: kvSource,
|
|
||||||
ctx: ctx,
|
|
||||||
meta: &Metadata{object: object},
|
|
||||||
lockKey: kvSource.Prefix + "/lock",
|
|
||||||
localLock: &sync.RWMutex{},
|
|
||||||
listener: listener,
|
|
||||||
}
|
|
||||||
err := datastore.watchChanges()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &datastore, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Datastore) watchChanges() error {
|
|
||||||
stopCh := make(chan struct{})
|
|
||||||
kvCh, err := d.kv.Watch(d.lockKey, stopCh)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
ctx, cancel := context.WithCancel(d.ctx)
|
|
||||||
operation := func() error {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
stopCh <- struct{}{}
|
|
||||||
return nil
|
|
||||||
case _, ok := <-kvCh:
|
|
||||||
if !ok {
|
|
||||||
cancel()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = d.reload()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// log.Debugf("Datastore object change received: %+v", d.meta)
|
|
||||||
if d.listener != nil {
|
|
||||||
err := d.listener(d.meta.object)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error calling datastore listener: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notify := func(err error, time time.Duration) {
|
|
||||||
log.Errorf("Error in watch datastore: %+v, retrying in %s", err, time)
|
|
||||||
}
|
|
||||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error in watch datastore: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Datastore) reload() error {
|
|
||||||
log.Debugf("Datastore reload")
|
|
||||||
d.localLock.Lock()
|
|
||||||
err := d.kv.LoadConfig(d.meta)
|
|
||||||
if err != nil {
|
|
||||||
d.localLock.Unlock()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = d.meta.unmarshall()
|
|
||||||
if err != nil {
|
|
||||||
d.localLock.Unlock()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.localLock.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Begin creates a transaction with the KV store.
|
|
||||||
func (d *Datastore) Begin() (Transaction, Object, error) {
|
|
||||||
id := uuid.NewV4().String()
|
|
||||||
log.Debugf("Transaction %s begins", id)
|
|
||||||
remoteLock, err := d.kv.NewLock(d.lockKey, &store.LockOptions{TTL: 20 * time.Second, Value: []byte(id)})
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
stopCh := make(chan struct{})
|
|
||||||
ctx, cancel := context.WithCancel(d.ctx)
|
|
||||||
var errLock error
|
|
||||||
go func() {
|
|
||||||
_, errLock = remoteLock.Lock(stopCh)
|
|
||||||
cancel()
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
if errLock != nil {
|
|
||||||
return nil, nil, errLock
|
|
||||||
}
|
|
||||||
case <-d.ctx.Done():
|
|
||||||
stopCh <- struct{}{}
|
|
||||||
return nil, nil, d.ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// we got the lock! Now make sure we are synced with KV store
|
|
||||||
operation := func() error {
|
|
||||||
meta := d.get()
|
|
||||||
if meta.Lock != id {
|
|
||||||
return fmt.Errorf("Object lock value: expected %s, got %s", id, meta.Lock)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
notify := func(err error, time time.Duration) {
|
|
||||||
log.Errorf("Datastore sync error: %v, retrying in %s", err, time)
|
|
||||||
err = d.reload()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error reloading: %+v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ebo := backoff.NewExponentialBackOff()
|
|
||||||
ebo.MaxElapsedTime = 60 * time.Second
|
|
||||||
err = backoff.RetryNotify(safe.OperationWithRecover(operation), ebo, notify)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("Datastore cannot sync: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// we synced with KV store, we can now return Setter
|
|
||||||
return &datastoreTransaction{
|
|
||||||
Datastore: d,
|
|
||||||
remoteLock: remoteLock,
|
|
||||||
id: id,
|
|
||||||
}, d.meta.object, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Datastore) get() *Metadata {
|
|
||||||
d.localLock.RLock()
|
|
||||||
defer d.localLock.RUnlock()
|
|
||||||
return d.meta
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load load atomically a struct from the KV store
|
|
||||||
func (d *Datastore) Load() (Object, error) {
|
|
||||||
d.localLock.Lock()
|
|
||||||
defer d.localLock.Unlock()
|
|
||||||
err := d.kv.LoadConfig(d.meta)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = d.meta.unmarshall()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return d.meta.object, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get atomically a struct from the KV store
|
|
||||||
func (d *Datastore) Get() Object {
|
|
||||||
d.localLock.RLock()
|
|
||||||
defer d.localLock.RUnlock()
|
|
||||||
return d.meta.object
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Transaction = (*datastoreTransaction)(nil)
|
|
||||||
|
|
||||||
type datastoreTransaction struct {
|
|
||||||
*Datastore
|
|
||||||
remoteLock store.Locker
|
|
||||||
dirty bool
|
|
||||||
id string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit allows to set an object in the KV store
|
|
||||||
func (s *datastoreTransaction) Commit(object Object) error {
|
|
||||||
s.localLock.Lock()
|
|
||||||
defer s.localLock.Unlock()
|
|
||||||
if s.dirty {
|
|
||||||
return fmt.Errorf("Transaction already used, please begin a new one")
|
|
||||||
}
|
|
||||||
s.Datastore.meta.object = object
|
|
||||||
err := s.Datastore.meta.Marshall()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Marshall error: %s", err)
|
|
||||||
}
|
|
||||||
err = s.kv.StoreConfig(s.Datastore.meta)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("StoreConfig error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.remoteLock.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Unlock error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.dirty = true
|
|
||||||
log.Debugf("Transaction committed %s", s.id)
|
|
||||||
return nil
|
|
||||||
}
|
|
@@ -1,104 +0,0 @@
|
|||||||
package cluster
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/cenk/backoff"
|
|
||||||
"github.com/containous/traefik/log"
|
|
||||||
"github.com/containous/traefik/safe"
|
|
||||||
"github.com/containous/traefik/types"
|
|
||||||
"github.com/docker/leadership"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Leadership allows leadership election using a KV store
|
|
||||||
type Leadership struct {
|
|
||||||
*safe.Pool
|
|
||||||
*types.Cluster
|
|
||||||
candidate *leadership.Candidate
|
|
||||||
leader *safe.Safe
|
|
||||||
listeners []LeaderListener
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLeadership creates a leadership
|
|
||||||
func NewLeadership(ctx context.Context, cluster *types.Cluster) *Leadership {
|
|
||||||
return &Leadership{
|
|
||||||
Pool: safe.NewPool(ctx),
|
|
||||||
Cluster: cluster,
|
|
||||||
candidate: leadership.NewCandidate(cluster.Store, cluster.Store.Prefix+"/leader", cluster.Node, 20*time.Second),
|
|
||||||
listeners: []LeaderListener{},
|
|
||||||
leader: safe.New(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LeaderListener is called when leadership has changed
|
|
||||||
type LeaderListener func(elected bool) error
|
|
||||||
|
|
||||||
// Participate tries to be a leader
|
|
||||||
func (l *Leadership) Participate(pool *safe.Pool) {
|
|
||||||
pool.GoCtx(func(ctx context.Context) {
|
|
||||||
log.Debugf("Node %s running for election", l.Cluster.Node)
|
|
||||||
defer log.Debugf("Node %s no more running for election", l.Cluster.Node)
|
|
||||||
backOff := backoff.NewExponentialBackOff()
|
|
||||||
operation := func() error {
|
|
||||||
return l.run(ctx, l.candidate)
|
|
||||||
}
|
|
||||||
|
|
||||||
notify := func(err error, time time.Duration) {
|
|
||||||
log.Errorf("Leadership election error %+v, retrying in %s", err, time)
|
|
||||||
}
|
|
||||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), backOff, notify)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Cannot elect leadership %+v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddListener adds a leadership listerner
|
|
||||||
func (l *Leadership) AddListener(listener LeaderListener) {
|
|
||||||
l.listeners = append(l.listeners, listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resign resigns from being a leader
|
|
||||||
func (l *Leadership) Resign() {
|
|
||||||
l.candidate.Resign()
|
|
||||||
log.Infof("Node %s resigned", l.Cluster.Node)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Leadership) run(ctx context.Context, candidate *leadership.Candidate) error {
|
|
||||||
electedCh, errCh := candidate.RunForElection()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case elected := <-electedCh:
|
|
||||||
l.onElection(elected)
|
|
||||||
case err := <-errCh:
|
|
||||||
return err
|
|
||||||
case <-ctx.Done():
|
|
||||||
l.candidate.Resign()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Leadership) onElection(elected bool) {
|
|
||||||
if elected {
|
|
||||||
log.Infof("Node %s elected leader ♚", l.Cluster.Node)
|
|
||||||
l.leader.Set(true)
|
|
||||||
l.Start()
|
|
||||||
} else {
|
|
||||||
log.Infof("Node %s elected slave ♝", l.Cluster.Node)
|
|
||||||
l.leader.Set(false)
|
|
||||||
l.Stop()
|
|
||||||
}
|
|
||||||
for _, listener := range l.listeners {
|
|
||||||
err := listener(elected)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error calling Leadership listener: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLeader returns true if current node is leader
|
|
||||||
func (l *Leadership) IsLeader() bool {
|
|
||||||
return l.leader.Get().(bool)
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
package cluster
|
|
||||||
|
|
||||||
// Object is the struct to store
|
|
||||||
type Object interface{}
|
|
||||||
|
|
||||||
// Store is a generic interface to represents a storage
|
|
||||||
type Store interface {
|
|
||||||
Load() (Object, error)
|
|
||||||
Get() Object
|
|
||||||
Begin() (Transaction, Object, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transaction allows to set a struct in the KV store
|
|
||||||
type Transaction interface {
|
|
||||||
Commit(object Object) error
|
|
||||||
}
|
|
34
cmd/configuration.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/static"
|
||||||
|
"github.com/containous/traefik/v2/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TraefikCmdConfiguration wraps the static configuration and extra parameters.
|
||||||
|
type TraefikCmdConfiguration struct {
|
||||||
|
static.Configuration `export:"true"`
|
||||||
|
// ConfigFile is the path to the configuration file.
|
||||||
|
ConfigFile string `description:"Configuration file to use. If specified all other flags are ignored." export:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTraefikConfiguration creates a TraefikCmdConfiguration with default values.
|
||||||
|
func NewTraefikConfiguration() *TraefikCmdConfiguration {
|
||||||
|
return &TraefikCmdConfiguration{
|
||||||
|
Configuration: static.Configuration{
|
||||||
|
Global: &static.Global{
|
||||||
|
CheckNewVersion: true,
|
||||||
|
},
|
||||||
|
EntryPoints: make(static.EntryPoints),
|
||||||
|
Providers: &static.Providers{
|
||||||
|
ProvidersThrottleDuration: types.Duration(2 * time.Second),
|
||||||
|
},
|
||||||
|
ServersTransport: &static.ServersTransport{
|
||||||
|
MaxIdleConnsPerHost: 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ConfigFile: "",
|
||||||
|
}
|
||||||
|
}
|
22
cmd/context.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContextWithSignal creates a context canceled when SIGINT or SIGTERM are notified
|
||||||
|
func ContextWithSignal(ctx context.Context) context.Context {
|
||||||
|
newCtx, cancel := context.WithCancel(ctx)
|
||||||
|
signals := make(chan os.Signal)
|
||||||
|
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-signals:
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return newCtx
|
||||||
|
}
|
74
cmd/healthcheck/healthcheck.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package healthcheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/v2/pkg/cli"
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/static"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewCmd builds a new HealthCheck command.
|
||||||
|
func NewCmd(traefikConfiguration *static.Configuration, loaders []cli.ResourceLoader) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "healthcheck",
|
||||||
|
Description: `Calls Traefik /ping to check the health of Traefik (the API must be enabled).`,
|
||||||
|
Configuration: traefikConfiguration,
|
||||||
|
Run: runCmd(traefikConfiguration),
|
||||||
|
Resources: loaders,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCmd(traefikConfiguration *static.Configuration) func(_ []string) error {
|
||||||
|
return func(_ []string) error {
|
||||||
|
traefikConfiguration.SetEffectiveConfiguration()
|
||||||
|
|
||||||
|
resp, errPing := Do(*traefikConfiguration)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
if errPing != nil {
|
||||||
|
fmt.Printf("Error calling healthcheck: %s\n", errPing)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
fmt.Printf("Bad healthcheck status: %s\n", resp.Status)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("OK: %s\n", resp.Request.URL)
|
||||||
|
os.Exit(0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do try to do a healthcheck
|
||||||
|
func Do(staticConfiguration static.Configuration) (*http.Response, error) {
|
||||||
|
if staticConfiguration.Ping == nil {
|
||||||
|
return nil, errors.New("please enable `ping` to use health check")
|
||||||
|
}
|
||||||
|
|
||||||
|
pingEntryPoint, ok := staticConfiguration.EntryPoints["traefik"]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("missing `ping` entrypoint")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
protocol := "http"
|
||||||
|
|
||||||
|
// FIXME Handle TLS on ping etc...
|
||||||
|
// if pingEntryPoint.TLS != nil {
|
||||||
|
// protocol = "https"
|
||||||
|
// tr := &http.Transport{
|
||||||
|
// TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
// }
|
||||||
|
// client.Transport = tr
|
||||||
|
// }
|
||||||
|
|
||||||
|
path := "/"
|
||||||
|
|
||||||
|
return client.Head(protocol + "://" + pingEntryPoint.Address + path + "ping")
|
||||||
|
}
|
@@ -1,152 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"os/exec"
|
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/containous/flaeg"
|
|
||||||
"github.com/mvdan/xurls"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
bugtracker = "https://github.com/containous/traefik/issues/new"
|
|
||||||
bugTemplate = `<!--
|
|
||||||
PLEASE READ THIS MESSAGE.
|
|
||||||
|
|
||||||
Please keep in mind that the GitHub issue tracker is not intended as a general support forum, but for reporting bugs and feature requests.
|
|
||||||
|
|
||||||
For other type of questions, consider using one of:
|
|
||||||
|
|
||||||
- the Traefik community Slack channel: https://traefik.herokuapp.com
|
|
||||||
- StackOverflow: https://stackoverflow.com/questions/tagged/traefik
|
|
||||||
|
|
||||||
HOW TO WRITE A GOOD ISSUE?
|
|
||||||
|
|
||||||
- if it's possible use the command` + "`" + `traefik bug` + "`" + `. See https://www.youtube.com/watch?v=Lyz62L8m93I.
|
|
||||||
- The title must be short and descriptive.
|
|
||||||
- Explain the conditions which led you to write this issue: the context.
|
|
||||||
- The context should lead to something, an idea or a problem that you’re facing.
|
|
||||||
- Remain clear and concise.
|
|
||||||
- Format your messages to help the reader focus on what matters and understand the structure of your message, use Markdown syntax https://help.github.com/articles/github-flavored-markdown
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Do you want to request a *feature* or report a *bug*?
|
|
||||||
|
|
||||||
|
|
||||||
### What did you do?
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### What did you expect to see?
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### What did you see instead?
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Output of ` + "`" + `traefik version` + "`" + `: (_What version of Traefik are you using?_)
|
|
||||||
|
|
||||||
` + "```" + `
|
|
||||||
{{.Version}}
|
|
||||||
` + "```" + `
|
|
||||||
|
|
||||||
### What is your environment & configuration (arguments, toml, provider, platform, ...)?
|
|
||||||
|
|
||||||
` + "```" + `toml
|
|
||||||
{{.Configuration}}
|
|
||||||
` + "```" + `
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Add more configuration information here.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### If applicable, please paste the log output in debug mode (` + "`" + `--debug` + "`" + ` switch)
|
|
||||||
|
|
||||||
` + "```" + `
|
|
||||||
(paste your output here)
|
|
||||||
` + "```" + `
|
|
||||||
|
|
||||||
`
|
|
||||||
)
|
|
||||||
|
|
||||||
// newBugCmd builds a new Bug command
|
|
||||||
func newBugCmd(traefikConfiguration interface{}, traefikPointersConfiguration interface{}) *flaeg.Command {
|
|
||||||
|
|
||||||
//version Command init
|
|
||||||
return &flaeg.Command{
|
|
||||||
Name: "bug",
|
|
||||||
Description: `Report an issue on Traefik bugtracker`,
|
|
||||||
Config: traefikConfiguration,
|
|
||||||
DefaultPointersConfig: traefikPointersConfiguration,
|
|
||||||
Run: func() error {
|
|
||||||
var version bytes.Buffer
|
|
||||||
if err := getVersionPrint(&version); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl, err := template.New("").Parse(bugTemplate)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
configJSON, err := json.MarshalIndent(traefikConfiguration, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
v := struct {
|
|
||||||
Version string
|
|
||||||
Configuration string
|
|
||||||
}{
|
|
||||||
Version: version.String(),
|
|
||||||
Configuration: anonymize(string(configJSON)),
|
|
||||||
}
|
|
||||||
|
|
||||||
var bug bytes.Buffer
|
|
||||||
if err := tmpl.Execute(&bug, v); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
body := bug.String()
|
|
||||||
URL := bugtracker + "?body=" + url.QueryEscape(body)
|
|
||||||
if err := openBrowser(URL); err != nil {
|
|
||||||
fmt.Print("Please file a new issue at " + bugtracker + " using this template:\n\n")
|
|
||||||
fmt.Print(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
Metadata: map[string]string{
|
|
||||||
"parseAllSources": "true",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func openBrowser(URL string) error {
|
|
||||||
var err error
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "linux":
|
|
||||||
err = exec.Command("xdg-open", URL).Start()
|
|
||||||
case "windows":
|
|
||||||
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", URL).Start()
|
|
||||||
case "darwin":
|
|
||||||
err = exec.Command("open", URL).Start()
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("unsupported platform")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func anonymize(input string) string {
|
|
||||||
replace := "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
||||||
mailExp := regexp.MustCompile(`\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,3}"`)
|
|
||||||
return xurls.Relaxed.ReplaceAllString(mailExp.ReplaceAllString(input, replace), replace)
|
|
||||||
}
|
|
@@ -1,313 +1,315 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
fmtlog "log"
|
stdlog "log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/containous/traefik/v2/autogen/genstatic"
|
||||||
"github.com/containous/flaeg"
|
"github.com/containous/traefik/v2/cmd"
|
||||||
"github.com/containous/staert"
|
"github.com/containous/traefik/v2/cmd/healthcheck"
|
||||||
"github.com/containous/traefik/acme"
|
cmdVersion "github.com/containous/traefik/v2/cmd/version"
|
||||||
"github.com/containous/traefik/cluster"
|
"github.com/containous/traefik/v2/pkg/cli"
|
||||||
"github.com/containous/traefik/log"
|
"github.com/containous/traefik/v2/pkg/collector"
|
||||||
"github.com/containous/traefik/middlewares"
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||||
"github.com/containous/traefik/provider/kubernetes"
|
"github.com/containous/traefik/v2/pkg/config/static"
|
||||||
"github.com/containous/traefik/safe"
|
"github.com/containous/traefik/v2/pkg/log"
|
||||||
"github.com/containous/traefik/server"
|
"github.com/containous/traefik/v2/pkg/provider/acme"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/v2/pkg/provider/aggregator"
|
||||||
"github.com/containous/traefik/version"
|
"github.com/containous/traefik/v2/pkg/safe"
|
||||||
|
"github.com/containous/traefik/v2/pkg/server"
|
||||||
|
"github.com/containous/traefik/v2/pkg/server/router"
|
||||||
|
traefiktls "github.com/containous/traefik/v2/pkg/tls"
|
||||||
|
"github.com/containous/traefik/v2/pkg/version"
|
||||||
"github.com/coreos/go-systemd/daemon"
|
"github.com/coreos/go-systemd/daemon"
|
||||||
"github.com/docker/libkv/store"
|
assetfs "github.com/elazarl/go-bindata-assetfs"
|
||||||
"github.com/satori/go.uuid"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/vulcand/oxy/roundrobin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
// traefik config inits
|
||||||
|
tConfig := cmd.NewTraefikConfiguration()
|
||||||
|
|
||||||
//traefik config inits
|
loaders := []cli.ResourceLoader{&cli.FileLoader{}, &cli.FlagLoader{}, &cli.EnvLoader{}}
|
||||||
traefikConfiguration := server.NewTraefikConfiguration()
|
|
||||||
traefikPointersConfiguration := server.NewTraefikDefaultPointersConfiguration()
|
cmdTraefik := &cli.Command{
|
||||||
//traefik Command init
|
|
||||||
traefikCmd := &flaeg.Command{
|
|
||||||
Name: "traefik",
|
Name: "traefik",
|
||||||
Description: `traefik is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
|
Description: `Traefik is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
|
||||||
Complete documentation is available at https://traefik.io`,
|
Complete documentation is available at https://traefik.io`,
|
||||||
Config: traefikConfiguration,
|
Configuration: tConfig,
|
||||||
DefaultPointersConfig: traefikPointersConfiguration,
|
Resources: loaders,
|
||||||
Run: func() error {
|
Run: func(_ []string) error {
|
||||||
run(traefikConfiguration)
|
return runCmd(&tConfig.Configuration)
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
//storeconfig Command init
|
err := cmdTraefik.AddCommand(healthcheck.NewCmd(&tConfig.Configuration, loaders))
|
||||||
var kv *staert.KvSource
|
|
||||||
var err error
|
|
||||||
|
|
||||||
storeconfigCmd := &flaeg.Command{
|
|
||||||
Name: "storeconfig",
|
|
||||||
Description: `Store the static traefik configuration into a Key-value stores. Traefik will not start.`,
|
|
||||||
Config: traefikConfiguration,
|
|
||||||
DefaultPointersConfig: traefikPointersConfiguration,
|
|
||||||
Run: func() error {
|
|
||||||
if kv == nil {
|
|
||||||
return fmt.Errorf("Error using command storeconfig, no Key-value store defined")
|
|
||||||
}
|
|
||||||
jsonConf, err := json.Marshal(traefikConfiguration.GlobalConfiguration)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmtlog.Printf("Storing configuration: %s\n", jsonConf)
|
|
||||||
err = kv.StoreConfig(traefikConfiguration.GlobalConfiguration)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if traefikConfiguration.GlobalConfiguration.ACME != nil && len(traefikConfiguration.GlobalConfiguration.ACME.StorageFile) > 0 {
|
|
||||||
// convert ACME json file to KV store
|
|
||||||
store := acme.NewLocalStore(traefikConfiguration.GlobalConfiguration.ACME.StorageFile)
|
|
||||||
object, err := store.Load()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
meta := cluster.NewMetadata(object)
|
|
||||||
err = meta.Marshall()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
source := staert.KvSource{
|
|
||||||
Store: kv,
|
|
||||||
Prefix: traefikConfiguration.GlobalConfiguration.ACME.Storage,
|
|
||||||
}
|
|
||||||
err = source.StoreConfig(meta)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
Metadata: map[string]string{
|
|
||||||
"parseAllSources": "true",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
//init flaeg source
|
|
||||||
f := flaeg.New(traefikCmd, os.Args[1:])
|
|
||||||
//add custom parsers
|
|
||||||
f.AddParser(reflect.TypeOf(server.EntryPoints{}), &server.EntryPoints{})
|
|
||||||
f.AddParser(reflect.TypeOf(server.DefaultEntryPoints{}), &server.DefaultEntryPoints{})
|
|
||||||
f.AddParser(reflect.TypeOf(types.Constraints{}), &types.Constraints{})
|
|
||||||
f.AddParser(reflect.TypeOf(kubernetes.Namespaces{}), &kubernetes.Namespaces{})
|
|
||||||
f.AddParser(reflect.TypeOf([]acme.Domain{}), &acme.Domains{})
|
|
||||||
f.AddParser(reflect.TypeOf(types.Buckets{}), &types.Buckets{})
|
|
||||||
|
|
||||||
//add commands
|
|
||||||
f.AddCommand(newVersionCmd())
|
|
||||||
f.AddCommand(newBugCmd(traefikConfiguration, traefikPointersConfiguration))
|
|
||||||
f.AddCommand(storeconfigCmd)
|
|
||||||
|
|
||||||
usedCmd, err := f.GetCommand()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmtlog.Println(err)
|
stdlog.Println(err)
|
||||||
os.Exit(-1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := f.Parse(usedCmd); err != nil {
|
err = cmdTraefik.AddCommand(cmdVersion.NewCmd())
|
||||||
fmtlog.Printf("Error parsing command: %s\n", err)
|
|
||||||
os.Exit(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
//staert init
|
|
||||||
s := staert.NewStaert(traefikCmd)
|
|
||||||
//init toml source
|
|
||||||
toml := staert.NewTomlSource("traefik", []string{traefikConfiguration.ConfigFile, "/etc/traefik/", "$HOME/.traefik/", "."})
|
|
||||||
|
|
||||||
//add sources to staert
|
|
||||||
s.AddSource(toml)
|
|
||||||
s.AddSource(f)
|
|
||||||
if _, err := s.LoadConfig(); err != nil {
|
|
||||||
fmtlog.Println(fmt.Errorf("Error reading TOML config file %s : %s", toml.ConfigFileUsed(), err))
|
|
||||||
os.Exit(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
traefikConfiguration.ConfigFile = toml.ConfigFileUsed()
|
|
||||||
|
|
||||||
kv, err = CreateKvSource(traefikConfiguration)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmtlog.Printf("Error creating kv store: %s\n", err)
|
stdlog.Println(err)
|
||||||
os.Exit(-1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IF a KV Store is enable and no sub-command called in args
|
err = cli.Execute(cmdTraefik)
|
||||||
if kv != nil && usedCmd == traefikCmd {
|
if err != nil {
|
||||||
if traefikConfiguration.Cluster == nil {
|
stdlog.Println(err)
|
||||||
traefikConfiguration.Cluster = &types.Cluster{Node: uuid.NewV4().String()}
|
os.Exit(1)
|
||||||
}
|
|
||||||
if traefikConfiguration.Cluster.Store == nil {
|
|
||||||
traefikConfiguration.Cluster.Store = &types.Store{Prefix: kv.Prefix, Store: kv.Store}
|
|
||||||
}
|
|
||||||
s.AddSource(kv)
|
|
||||||
if _, err := s.LoadConfig(); err != nil {
|
|
||||||
fmtlog.Printf("Error loading configuration: %s\n", err)
|
|
||||||
os.Exit(-1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Run(); err != nil {
|
|
||||||
fmtlog.Printf("Error running traefik: %s\n", err)
|
|
||||||
os.Exit(-1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(traefikConfiguration *server.TraefikConfiguration) {
|
func runCmd(staticConfiguration *static.Configuration) error {
|
||||||
fmtlog.SetFlags(fmtlog.Lshortfile | fmtlog.LstdFlags)
|
configureLogging(staticConfiguration)
|
||||||
|
|
||||||
// load global configuration
|
http.DefaultTransport.(*http.Transport).Proxy = http.ProxyFromEnvironment
|
||||||
globalConfiguration := traefikConfiguration.GlobalConfiguration
|
|
||||||
|
|
||||||
http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = globalConfiguration.MaxIdleConnsPerHost
|
if err := roundrobin.SetDefaultWeight(0); err != nil {
|
||||||
if globalConfiguration.InsecureSkipVerify {
|
log.WithoutContext().Errorf("Could not set roundrobin default weight: %v", err)
|
||||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
|
||||||
}
|
|
||||||
loggerMiddleware := middlewares.NewLogger(globalConfiguration.AccessLogsFile)
|
|
||||||
defer loggerMiddleware.Close()
|
|
||||||
|
|
||||||
if globalConfiguration.File != nil && len(globalConfiguration.File.Filename) == 0 {
|
|
||||||
// no filename, setting to global config file
|
|
||||||
if len(traefikConfiguration.ConfigFile) != 0 {
|
|
||||||
globalConfiguration.File.Filename = traefikConfiguration.ConfigFile
|
|
||||||
} else {
|
|
||||||
log.Errorln("Error using file configuration backend, no filename defined")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(globalConfiguration.EntryPoints) == 0 {
|
staticConfiguration.SetEffectiveConfiguration()
|
||||||
globalConfiguration.EntryPoints = map[string]*server.EntryPoint{"http": {Address: ":80"}}
|
if err := staticConfiguration.ValidateConfiguration(); err != nil {
|
||||||
globalConfiguration.DefaultEntryPoints = []string{"http"}
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalConfiguration.Debug {
|
log.WithoutContext().Infof("Traefik version %s built on %s", version.Version, version.BuildDate)
|
||||||
globalConfiguration.LogLevel = "DEBUG"
|
|
||||||
}
|
|
||||||
|
|
||||||
// logging
|
jsonConf, err := json.Marshal(staticConfiguration)
|
||||||
level, err := logrus.ParseLevel(strings.ToLower(globalConfiguration.LogLevel))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error getting level", err)
|
log.WithoutContext().Errorf("Could not marshal static configuration: %v", err)
|
||||||
}
|
log.WithoutContext().Debugf("Static configuration loaded [struct] %#v", staticConfiguration)
|
||||||
log.SetLevel(level)
|
|
||||||
if len(globalConfiguration.TraefikLogsFile) > 0 {
|
|
||||||
dir := filepath.Dir(globalConfiguration.TraefikLogsFile)
|
|
||||||
|
|
||||||
err := os.MkdirAll(dir, 0755)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to create log path %s: %s", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fi, err := os.OpenFile(globalConfiguration.TraefikLogsFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
|
||||||
defer func() {
|
|
||||||
if err := fi.Close(); err != nil {
|
|
||||||
log.Error("Error closing file", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error opening file", err)
|
|
||||||
} else {
|
|
||||||
log.SetOutput(fi)
|
|
||||||
log.SetFormatter(&logrus.TextFormatter{DisableColors: true, FullTimestamp: true, DisableSorting: true})
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
log.SetFormatter(&logrus.TextFormatter{FullTimestamp: true, DisableSorting: true})
|
log.WithoutContext().Debugf("Static configuration loaded %s", string(jsonConf))
|
||||||
}
|
}
|
||||||
jsonConf, _ := json.Marshal(globalConfiguration)
|
|
||||||
log.Infof("Traefik version %s built on %s", version.Version, version.BuildDate)
|
|
||||||
|
|
||||||
if globalConfiguration.CheckNewVersion {
|
if staticConfiguration.API != nil && staticConfiguration.API.Dashboard {
|
||||||
ticker := time.NewTicker(24 * time.Hour)
|
staticConfiguration.API.DashboardAssets = &assetfs.AssetFS{Asset: genstatic.Asset, AssetInfo: genstatic.AssetInfo, AssetDir: genstatic.AssetDir, Prefix: "static"}
|
||||||
safe.Go(func() {
|
}
|
||||||
version.CheckNewVersion()
|
|
||||||
for {
|
if staticConfiguration.Global.CheckNewVersion {
|
||||||
select {
|
checkNewVersion()
|
||||||
case <-ticker.C:
|
}
|
||||||
version.CheckNewVersion()
|
|
||||||
}
|
stats(staticConfiguration)
|
||||||
|
|
||||||
|
providerAggregator := aggregator.NewProviderAggregator(*staticConfiguration.Providers)
|
||||||
|
|
||||||
|
tlsManager := traefiktls.NewManager()
|
||||||
|
|
||||||
|
acmeProviders := initACMEProvider(staticConfiguration, &providerAggregator, tlsManager)
|
||||||
|
|
||||||
|
serverEntryPointsTCP := make(server.TCPEntryPoints)
|
||||||
|
for entryPointName, config := range staticConfiguration.EntryPoints {
|
||||||
|
ctx := log.With(context.Background(), log.Str(log.EntryPointName, entryPointName))
|
||||||
|
serverEntryPointsTCP[entryPointName], err = server.NewTCPEntryPoint(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while building entryPoint %s: %v", entryPointName, err)
|
||||||
|
}
|
||||||
|
serverEntryPointsTCP[entryPointName].RouteAppenderFactory = router.NewRouteAppenderFactory(*staticConfiguration, entryPointName, acmeProviders)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
svr := server.NewServer(*staticConfiguration, providerAggregator, serverEntryPointsTCP, tlsManager)
|
||||||
|
|
||||||
|
resolverNames := map[string]struct{}{}
|
||||||
|
|
||||||
|
for _, p := range acmeProviders {
|
||||||
|
resolverNames[p.ResolverName] = struct{}{}
|
||||||
|
svr.AddListener(p.ListenConfiguration)
|
||||||
|
}
|
||||||
|
|
||||||
|
svr.AddListener(func(config dynamic.Configuration) {
|
||||||
|
for rtName, rt := range config.HTTP.Routers {
|
||||||
|
if rt.TLS == nil || rt.TLS.CertResolver == "" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
if _, ok := resolverNames[rt.TLS.CertResolver]; !ok {
|
||||||
|
log.WithoutContext().Errorf("the router %s uses a non-existent resolver: %s", rtName, rt.TLS.CertResolver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := cmd.ContextWithSignal(context.Background())
|
||||||
|
|
||||||
|
if staticConfiguration.Ping != nil {
|
||||||
|
staticConfiguration.Ping.WithContext(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(traefikConfiguration.ConfigFile) != 0 {
|
svr.Start(ctx)
|
||||||
log.Infof("Using TOML configuration file %s", traefikConfiguration.ConfigFile)
|
|
||||||
}
|
|
||||||
log.Debugf("Global configuration loaded %s", string(jsonConf))
|
|
||||||
svr := server.NewServer(globalConfiguration)
|
|
||||||
svr.Start()
|
|
||||||
defer svr.Close()
|
defer svr.Close()
|
||||||
|
|
||||||
sent, err := daemon.SdNotify(false, "READY=1")
|
sent, err := daemon.SdNotify(false, "READY=1")
|
||||||
if !sent && err != nil {
|
if !sent && err != nil {
|
||||||
log.Error("Fail to notify", err)
|
log.WithoutContext().Errorf("Failed to notify: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := daemon.SdWatchdogEnabled(false)
|
t, err := daemon.SdWatchdogEnabled(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Problem with watchdog", err)
|
log.WithoutContext().Errorf("Could not enable Watchdog: %v", err)
|
||||||
} else if t != 0 {
|
} else if t != 0 {
|
||||||
// Send a ping each half time given
|
// Send a ping each half time given
|
||||||
t = t / 2
|
t /= 2
|
||||||
log.Info("Watchdog activated with timer each ", t)
|
log.WithoutContext().Infof("Watchdog activated with timer duration %s", t)
|
||||||
safe.Go(func() {
|
safe.Go(func() {
|
||||||
tick := time.Tick(t)
|
tick := time.Tick(t)
|
||||||
for range tick {
|
for range tick {
|
||||||
if ok, _ := daemon.SdNotify(false, "WATCHDOG=1"); !ok {
|
resp, errHealthCheck := healthcheck.Do(*staticConfiguration)
|
||||||
log.Error("Fail to tick watchdog")
|
if resp != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if staticConfiguration.Ping == nil || errHealthCheck == nil {
|
||||||
|
if ok, _ := daemon.SdNotify(false, "WATCHDOG=1"); !ok {
|
||||||
|
log.WithoutContext().Error("Fail to tick watchdog")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.WithoutContext().Error(errHealthCheck)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
svr.Wait()
|
svr.Wait()
|
||||||
log.Info("Shutting down")
|
log.WithoutContext().Info("Shutting down")
|
||||||
|
logrus.Exit(0)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateKvSource creates KvSource
|
// initACMEProvider creates an acme provider from the ACME part of globalConfiguration
|
||||||
// TLS support is enable for Consul and Etcd backends
|
func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.ProviderAggregator, tlsManager *traefiktls.Manager) []*acme.Provider {
|
||||||
func CreateKvSource(traefikConfiguration *server.TraefikConfiguration) (*staert.KvSource, error) {
|
challengeStore := acme.NewLocalChallengeStore()
|
||||||
var kv *staert.KvSource
|
localStores := map[string]*acme.LocalStore{}
|
||||||
var store store.Store
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch {
|
var resolvers []*acme.Provider
|
||||||
case traefikConfiguration.Consul != nil:
|
for name, resolver := range c.CertificatesResolvers {
|
||||||
store, err = traefikConfiguration.Consul.CreateStore()
|
if resolver.ACME != nil {
|
||||||
kv = &staert.KvSource{
|
if localStores[resolver.ACME.Storage] == nil {
|
||||||
Store: store,
|
localStores[resolver.ACME.Storage] = acme.NewLocalStore(resolver.ACME.Storage)
|
||||||
Prefix: traefikConfiguration.Consul.Prefix,
|
}
|
||||||
}
|
|
||||||
case traefikConfiguration.Etcd != nil:
|
p := &acme.Provider{
|
||||||
store, err = traefikConfiguration.Etcd.CreateStore()
|
Configuration: resolver.ACME,
|
||||||
kv = &staert.KvSource{
|
Store: localStores[resolver.ACME.Storage],
|
||||||
Store: store,
|
ChallengeStore: challengeStore,
|
||||||
Prefix: traefikConfiguration.Etcd.Prefix,
|
ResolverName: name,
|
||||||
}
|
}
|
||||||
case traefikConfiguration.Zookeeper != nil:
|
|
||||||
store, err = traefikConfiguration.Zookeeper.CreateStore()
|
if err := providerAggregator.AddProvider(p); err != nil {
|
||||||
kv = &staert.KvSource{
|
log.WithoutContext().Errorf("Unable to add ACME provider to the providers list: %v", err)
|
||||||
Store: store,
|
continue
|
||||||
Prefix: traefikConfiguration.Zookeeper.Prefix,
|
}
|
||||||
}
|
p.SetTLSManager(tlsManager)
|
||||||
case traefikConfiguration.Boltdb != nil:
|
if p.TLSChallenge != nil {
|
||||||
store, err = traefikConfiguration.Boltdb.CreateStore()
|
tlsManager.TLSAlpnGetter = p.GetTLSALPNCertificate
|
||||||
kv = &staert.KvSource{
|
}
|
||||||
Store: store,
|
p.SetConfigListenerChan(make(chan dynamic.Configuration))
|
||||||
Prefix: traefikConfiguration.Boltdb.Prefix,
|
resolvers = append(resolvers, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return kv, err
|
return resolvers
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureLogging(staticConfiguration *static.Configuration) {
|
||||||
|
// configure default log flags
|
||||||
|
stdlog.SetFlags(stdlog.Lshortfile | stdlog.LstdFlags)
|
||||||
|
|
||||||
|
// configure log level
|
||||||
|
// an explicitly defined log level always has precedence. if none is
|
||||||
|
// given and debug mode is disabled, the default is ERROR, and DEBUG
|
||||||
|
// otherwise.
|
||||||
|
levelStr := "error"
|
||||||
|
if staticConfiguration.Log != nil && staticConfiguration.Log.Level != "" {
|
||||||
|
levelStr = strings.ToLower(staticConfiguration.Log.Level)
|
||||||
|
}
|
||||||
|
|
||||||
|
level, err := logrus.ParseLevel(levelStr)
|
||||||
|
if err != nil {
|
||||||
|
log.WithoutContext().Errorf("Error getting level: %v", err)
|
||||||
|
}
|
||||||
|
log.SetLevel(level)
|
||||||
|
|
||||||
|
var logFile string
|
||||||
|
if staticConfiguration.Log != nil && len(staticConfiguration.Log.FilePath) > 0 {
|
||||||
|
logFile = staticConfiguration.Log.FilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// configure log format
|
||||||
|
var formatter logrus.Formatter
|
||||||
|
if staticConfiguration.Log != nil && staticConfiguration.Log.Format == "json" {
|
||||||
|
formatter = &logrus.JSONFormatter{}
|
||||||
|
} else {
|
||||||
|
disableColors := len(logFile) > 0
|
||||||
|
formatter = &logrus.TextFormatter{DisableColors: disableColors, FullTimestamp: true, DisableSorting: true}
|
||||||
|
}
|
||||||
|
log.SetFormatter(formatter)
|
||||||
|
|
||||||
|
if len(logFile) > 0 {
|
||||||
|
dir := filepath.Dir(logFile)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
log.WithoutContext().Errorf("Failed to create log path %s: %s", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = log.OpenFile(logFile)
|
||||||
|
logrus.RegisterExitHandler(func() {
|
||||||
|
if err := log.CloseFile(); err != nil {
|
||||||
|
log.WithoutContext().Errorf("Error while closing log: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.WithoutContext().Errorf("Error while opening log file %s: %v", logFile, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkNewVersion() {
|
||||||
|
ticker := time.Tick(24 * time.Hour)
|
||||||
|
safe.Go(func() {
|
||||||
|
for time.Sleep(10 * time.Minute); ; <-ticker {
|
||||||
|
version.CheckNewVersion()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func stats(staticConfiguration *static.Configuration) {
|
||||||
|
logger := log.WithoutContext()
|
||||||
|
|
||||||
|
if staticConfiguration.Global.SendAnonymousUsage {
|
||||||
|
logger.Info(`Stats collection is enabled.`)
|
||||||
|
logger.Info(`Many thanks for contributing to Traefik's improvement by allowing us to receive anonymous information from your configuration.`)
|
||||||
|
logger.Info(`Help us improve Traefik by leaving this feature on :)`)
|
||||||
|
logger.Info(`More details on: https://docs.traefik.io/v2.0/contributing/data-collection/`)
|
||||||
|
collect(staticConfiguration)
|
||||||
|
} else {
|
||||||
|
logger.Info(`
|
||||||
|
Stats collection is disabled.
|
||||||
|
Help us improve Traefik by turning this feature on :)
|
||||||
|
More details on: https://docs.traefik.io/v2.0/contributing/data-collection/
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collect(staticConfiguration *static.Configuration) {
|
||||||
|
ticker := time.Tick(24 * time.Hour)
|
||||||
|
safe.Go(func() {
|
||||||
|
for time.Sleep(10 * time.Minute); ; <-ticker {
|
||||||
|
if err := collector.Collect(staticConfiguration); err != nil {
|
||||||
|
log.WithoutContext().Debug(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@@ -1,63 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/containous/flaeg"
|
|
||||||
"github.com/containous/traefik/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
var versionTemplate = `Version: {{.Version}}
|
|
||||||
Codename: {{.Codename}}
|
|
||||||
Go version: {{.GoVersion}}
|
|
||||||
Built: {{.BuildTime}}
|
|
||||||
OS/Arch: {{.Os}}/{{.Arch}}`
|
|
||||||
|
|
||||||
// newVersionCmd builds a new Version command
|
|
||||||
func newVersionCmd() *flaeg.Command {
|
|
||||||
|
|
||||||
//version Command init
|
|
||||||
return &flaeg.Command{
|
|
||||||
Name: "version",
|
|
||||||
Description: `Print version`,
|
|
||||||
Config: struct{}{},
|
|
||||||
DefaultPointersConfig: struct{}{},
|
|
||||||
Run: func() error {
|
|
||||||
if err := getVersionPrint(os.Stdout); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Printf("\n")
|
|
||||||
return nil
|
|
||||||
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getVersionPrint(wr io.Writer) error {
|
|
||||||
tmpl, err := template.New("").Parse(versionTemplate)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
v := struct {
|
|
||||||
Version string
|
|
||||||
Codename string
|
|
||||||
GoVersion string
|
|
||||||
BuildTime string
|
|
||||||
Os string
|
|
||||||
Arch string
|
|
||||||
}{
|
|
||||||
Version: version.Version,
|
|
||||||
Codename: version.Codename,
|
|
||||||
GoVersion: runtime.Version(),
|
|
||||||
BuildTime: version.BuildDate,
|
|
||||||
Os: runtime.GOOS,
|
|
||||||
Arch: runtime.GOARCH,
|
|
||||||
}
|
|
||||||
|
|
||||||
return tmpl.Execute(wr, v)
|
|
||||||
}
|
|
60
cmd/version/version.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/v2/pkg/cli"
|
||||||
|
"github.com/containous/traefik/v2/pkg/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var versionTemplate = `Version: {{.Version}}
|
||||||
|
Codename: {{.Codename}}
|
||||||
|
Go version: {{.GoVersion}}
|
||||||
|
Built: {{.BuildTime}}
|
||||||
|
OS/Arch: {{.Os}}/{{.Arch}}`
|
||||||
|
|
||||||
|
// NewCmd builds a new Version command
|
||||||
|
func NewCmd() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "version",
|
||||||
|
Description: `Shows the current Traefik version.`,
|
||||||
|
Configuration: nil,
|
||||||
|
Run: func(_ []string) error {
|
||||||
|
if err := GetPrint(os.Stdout); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Print("\n")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrint write Printable version
|
||||||
|
func GetPrint(wr io.Writer) error {
|
||||||
|
tmpl, err := template.New("").Parse(versionTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v := struct {
|
||||||
|
Version string
|
||||||
|
Codename string
|
||||||
|
GoVersion string
|
||||||
|
BuildTime string
|
||||||
|
Os string
|
||||||
|
Arch string
|
||||||
|
}{
|
||||||
|
Version: version.Version,
|
||||||
|
Codename: version.Codename,
|
||||||
|
GoVersion: runtime.Version(),
|
||||||
|
BuildTime: version.BuildDate,
|
||||||
|
Os: runtime.GOOS,
|
||||||
|
Arch: runtime.GOARCH,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmpl.Execute(wr, v)
|
||||||
|
}
|
1374
contrib/grafana/traefik-kubernetes.json
Normal file
1055
contrib/grafana/traefik.json
Normal file
@@ -1,11 +1,41 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Traefik
|
Description=Traefik
|
||||||
|
Documentation=https://docs.traefik.io
|
||||||
|
#After=network-online.target
|
||||||
|
#AssertFileIsExecutable=/usr/bin/traefik
|
||||||
|
#AssertPathExists=/etc/traefik/traefik.toml
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
# Run traefik as its own user (create new user with: useradd -r -s /bin/false -U -M traefik)
|
||||||
|
#User=traefik
|
||||||
|
#AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
|
||||||
|
# configure service behavior
|
||||||
Type=notify
|
Type=notify
|
||||||
ExecStart=/usr/bin/traefik --configFile=/etc/traefik.toml
|
#ExecStart=/usr/bin/traefik --configFile=/etc/traefik/traefik.toml
|
||||||
Restart=always
|
Restart=always
|
||||||
WatchdogSec=1s
|
WatchdogSec=1s
|
||||||
|
|
||||||
|
# lock down system access
|
||||||
|
# prohibit any operating system and configuration modification
|
||||||
|
#ProtectSystem=strict
|
||||||
|
# create separate, new (and empty) /tmp and /var/tmp filesystems
|
||||||
|
#PrivateTmp=true
|
||||||
|
# make /home directories inaccessible
|
||||||
|
#ProtectHome=true
|
||||||
|
# turns off access to physical devices (/dev/...)
|
||||||
|
#PrivateDevices=true
|
||||||
|
# make kernel settings (procfs and sysfs) read-only
|
||||||
|
#ProtectKernelTunables=true
|
||||||
|
# make cgroups /sys/fs/cgroup read-only
|
||||||
|
#ProtectControlGroups=true
|
||||||
|
|
||||||
|
# allow writing of acme.json
|
||||||
|
#ReadWritePaths=/etc/traefik/acme.json
|
||||||
|
# depending on log and entrypoint configuration, you may need to allow writing to other paths, too
|
||||||
|
|
||||||
|
# limit number of processes in this unit
|
||||||
|
#LimitNPROC=1
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
1
docs/.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
site/
|
11
docs/.markdownlint.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"no-hard-tabs": false,
|
||||||
|
"MD007": { "indent": 4 },
|
||||||
|
"MD009": false,
|
||||||
|
"MD013": false,
|
||||||
|
"MD024": false,
|
||||||
|
"MD026": false,
|
||||||
|
"MD033": false,
|
||||||
|
"MD034": false,
|
||||||
|
"MD036": false
|
||||||
|
}
|
52
docs/Makefile
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
|
||||||
|
#######
|
||||||
|
# This Makefile contains all targets related to the documentation
|
||||||
|
#######
|
||||||
|
|
||||||
|
DOCS_VERIFY_SKIP ?= false
|
||||||
|
DOCS_LINT_SKIP ?= false
|
||||||
|
|
||||||
|
TRAEFIK_DOCS_BUILD_IMAGE ?= traefik-docs
|
||||||
|
TRAEFIK_DOCS_CHECK_IMAGE ?= $(TRAEFIK_DOCS_BUILD_IMAGE)-check
|
||||||
|
|
||||||
|
SITE_DIR := $(CURDIR)/site
|
||||||
|
|
||||||
|
DOCKER_RUN_DOC_PORT := 8000
|
||||||
|
DOCKER_RUN_DOC_MOUNTS := -v $(CURDIR):/mkdocs
|
||||||
|
DOCKER_RUN_DOC_OPTS := --rm $(DOCKER_RUN_DOC_MOUNTS) -p $(DOCKER_RUN_DOC_PORT):8000
|
||||||
|
|
||||||
|
# Default: generates the documentation into $(SITE_DIR)
|
||||||
|
docs: docs-clean docs-image docs-lint docs-build docs-verify
|
||||||
|
|
||||||
|
# Writer Mode: build and serve docs on http://localhost:8000 with livereload
|
||||||
|
docs-serve: docs-image
|
||||||
|
docker run $(DOCKER_RUN_DOC_OPTS) $(TRAEFIK_DOCS_BUILD_IMAGE) mkdocs serve
|
||||||
|
|
||||||
|
# Utilities Targets for each step
|
||||||
|
docs-image:
|
||||||
|
docker build -t $(TRAEFIK_DOCS_BUILD_IMAGE) -f docs.Dockerfile ./
|
||||||
|
|
||||||
|
docs-build: docs-image
|
||||||
|
docker run $(DOCKER_RUN_DOC_OPTS) $(TRAEFIK_DOCS_BUILD_IMAGE) sh -c "mkdocs build \
|
||||||
|
&& chown -R $(shell id -u):$(shell id -g) ./site"
|
||||||
|
|
||||||
|
docs-verify: docs-build
|
||||||
|
@if [ "$(DOCS_VERIFY_SKIP)" != "true" ]; then \
|
||||||
|
docker build -t $(TRAEFIK_DOCS_CHECK_IMAGE) -f check.Dockerfile ./; \
|
||||||
|
docker run --rm -v $(CURDIR):/app $(TRAEFIK_DOCS_CHECK_IMAGE) /verify.sh; \
|
||||||
|
else \
|
||||||
|
echo "DOCS_VERIFY_SKIP is true: no verification done."; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
docs-lint:
|
||||||
|
@if [ "$(DOCS_LINT_SKIP)" != "true" ]; then \
|
||||||
|
docker build -t $(TRAEFIK_DOCS_CHECK_IMAGE) -f check.Dockerfile ./ && \
|
||||||
|
docker run --rm -v $(CURDIR):/app $(TRAEFIK_DOCS_CHECK_IMAGE) /lint.sh; \
|
||||||
|
else \
|
||||||
|
echo "DOCS_LINT_SKIP is true: no linting done."; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
docs-clean:
|
||||||
|
rm -rf $(SITE_DIR)
|
||||||
|
|
||||||
|
.PHONY: all docs-verify docs docs-clean docs-build docs-lint
|
442
docs/basics.md
@@ -1,442 +0,0 @@
|
|||||||
|
|
||||||
# Concepts
|
|
||||||
|
|
||||||
Let's take our example from the [overview](https://docs.traefik.io/#overview) again:
|
|
||||||
|
|
||||||
|
|
||||||
> Imagine that you have deployed a bunch of microservices on your infrastructure. You probably used a service registry (like etcd or consul) and/or an orchestrator (swarm, Mesos/Marathon) to manage all these services.
|
|
||||||
> If you want your users to access some of your microservices from the Internet, you will have to use a reverse proxy and configure it using virtual hosts or prefix paths:
|
|
||||||
|
|
||||||
> - domain `api.domain.com` will point the microservice `api` in your private network
|
|
||||||
> - path `domain.com/web` will point the microservice `web` in your private network
|
|
||||||
> - domain `backoffice.domain.com` will point the microservices `backoffice` in your private network, load-balancing between your multiple instances
|
|
||||||
|
|
||||||
> 
|
|
||||||
|
|
||||||
Let's zoom on Træfik and have an overview of its internal architecture:
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- Incoming requests end on [entrypoints](#entrypoints), as the name suggests, they are the network entry points into Træfik (listening port, SSL, traffic redirection...).
|
|
||||||
- Traffic is then forwarded to a matching [frontend](#frontends). A frontend defines routes from [entrypoints](#entrypoints) to [backends](#backends).
|
|
||||||
Routes are created using requests fields (`Host`, `Path`, `Headers`...) and can match or not a request.
|
|
||||||
- The [frontend](#frontends) will then send the request to a [backend](#backends). A backend can be composed by one or more [servers](#servers), and by a load-balancing strategy.
|
|
||||||
- Finally, the [server](#servers) will forward the request to the corresponding microservice in the private network.
|
|
||||||
|
|
||||||
## Entrypoints
|
|
||||||
|
|
||||||
Entrypoints are the network entry points into Træfik.
|
|
||||||
They can be defined using:
|
|
||||||
|
|
||||||
- a port (80, 443...)
|
|
||||||
- SSL (Certificates, Keys, authentication with a client certificate signed by a trusted CA...)
|
|
||||||
- redirection to another entrypoint (redirect `HTTP` to `HTTPS`)
|
|
||||||
|
|
||||||
Here is an example of entrypoints definition:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[entryPoints]
|
|
||||||
[entryPoints.http]
|
|
||||||
address = ":80"
|
|
||||||
[entryPoints.http.redirect]
|
|
||||||
entryPoint = "https"
|
|
||||||
[entryPoints.https]
|
|
||||||
address = ":443"
|
|
||||||
[entryPoints.https.tls]
|
|
||||||
[[entryPoints.https.tls.certificates]]
|
|
||||||
certFile = "tests/traefik.crt"
|
|
||||||
keyFile = "tests/traefik.key"
|
|
||||||
```
|
|
||||||
|
|
||||||
- Two entrypoints are defined `http` and `https`.
|
|
||||||
- `http` listens on port `80` and `https` on port `443`.
|
|
||||||
- We enable SSL on `https` by giving a certificate and a key.
|
|
||||||
- We also redirect all the traffic from entrypoint `http` to `https`.
|
|
||||||
|
|
||||||
And here is another example with client certificate authentication:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[entryPoints]
|
|
||||||
[entryPoints.https]
|
|
||||||
address = ":443"
|
|
||||||
[entryPoints.https.tls]
|
|
||||||
clientCAFiles = ["tests/clientca1.crt", "tests/clientca2.crt"]
|
|
||||||
[[entryPoints.https.tls.certificates]]
|
|
||||||
certFile = "tests/traefik.crt"
|
|
||||||
keyFile = "tests/traefik.key"
|
|
||||||
```
|
|
||||||
|
|
||||||
- We enable SSL on `https` by giving a certificate and a key.
|
|
||||||
- One or several files containing Certificate Authorities in PEM format are added.
|
|
||||||
- It is possible to have multiple CA:s in the same file or keep them in separate files.
|
|
||||||
|
|
||||||
## Frontends
|
|
||||||
|
|
||||||
A frontend consists of a set of rules that determine how incoming requests are forwarded from an entrypoint to a backend.
|
|
||||||
|
|
||||||
Rules may be classified in one of two groups: Modifiers and matchers.
|
|
||||||
|
|
||||||
### Modifiers
|
|
||||||
|
|
||||||
Modifier rules only modify the request. They do not have any impact on routing decisions being made.
|
|
||||||
|
|
||||||
Following is the list of existing modifier rules:
|
|
||||||
|
|
||||||
- `AddPrefix: /products`: Add path prefix to the existing request path prior to forwarding the request to the backend.
|
|
||||||
- `ReplacePath: /serverless-path`: Replaces the path and adds the old path to the `X-Replaced-Path` header. Useful for mapping to AWS Lambda or Google Cloud Functions.
|
|
||||||
|
|
||||||
### Matchers
|
|
||||||
|
|
||||||
Matcher rules determine if a particular request should be forwarded to a backend.
|
|
||||||
|
|
||||||
Separate multiple rule values by `,` (comma) in order to enable ANY semantics (i.e., forward a request if any rule matches). Does not work for `Headers` and `HeadersRegexp`.
|
|
||||||
|
|
||||||
Separate multiple rule values by `;` (semicolon) in order to enable ALL semantics (i.e., forward a request if all rules match).
|
|
||||||
|
|
||||||
You can optionally enable `passHostHeader` to forward client `Host` header to the backend.
|
|
||||||
|
|
||||||
Following is the list of existing matcher rules along with examples:
|
|
||||||
|
|
||||||
- `Headers: Content-Type, application/json`: Match HTTP header. It accepts a comma-separated key/value pair where both key and value must be literals.
|
|
||||||
- `HeadersRegexp: Content-Type, application/(text|json)`: Match HTTP header. It accepts a comma-separated key/value pair where the key must be a literal and the value may be a literal or a regular expression.
|
|
||||||
- `Host: traefik.io, www.traefik.io`: Match request host. It accepts a sequence of literal hosts.
|
|
||||||
- `HostRegexp: traefik.io, {subdomain:[a-z]+}.traefik.io`: Match request host. It accepts a sequence of literal and regular expression hosts.
|
|
||||||
- `Method: GET, POST, PUT`: Match request HTTP method. It accepts a sequence of HTTP methods.
|
|
||||||
- `Path: /products/, /articles/{category}/{id:[0-9]+}`: Match exact request path. It accepts a sequence of literal and regular expression paths.
|
|
||||||
- `PathStrip: /products/`: Match exact path and strip off the path prior to forwarding the request to the backend. It accepts a sequence of literal paths.
|
|
||||||
- `PathStripRegex: /articles/{category}/{id:[0-9]+}`: Match exact path and strip off the path prior to forwarding the request to the backend. It accepts a sequence of literal and regular expression paths.
|
|
||||||
- `PathPrefix: /products/, /articles/{category}/{id:[0-9]+}`: Match request prefix path. It accepts a sequence of literal and regular expression prefix paths.
|
|
||||||
- `PathPrefixStrip: /products/`: Match request prefix path and strip off the path prefix prior to forwarding the request to the backend. It accepts a sequence of literal prefix paths. Starting with Traefik 1.3, the stripped prefix path will be available in the `X-Forwarded-Prefix` header.
|
|
||||||
- `PathPrefixStripRegex: /articles/{category}/{id:[0-9]+}`: Match request prefix path and strip off the path prefix prior to forwarding the request to the backend. It accepts a sequence of literal and regular expression prefix paths. Starting with Traefik 1.3, the stripped prefix path will be available in the `X-Forwarded-Prefix` header.
|
|
||||||
|
|
||||||
In order to use regular expressions with Host and Path matchers, you must declare an arbitrarily named variable followed by the colon-separated regular expression, all enclosed in curly braces. Any pattern supported by [Go's regexp package](https://golang.org/pkg/regexp/) may be used. Example: `/posts/{id:[0-9]+}`.
|
|
||||||
|
|
||||||
(Note that the variable has no special meaning; however, it is required by the gorilla/mux dependency which embeds the regular expression and defines the syntax.)
|
|
||||||
|
|
||||||
#### Path Matcher Usage Guidelines
|
|
||||||
|
|
||||||
This section explains when to use the various path matchers.
|
|
||||||
|
|
||||||
Use `Path` if your backend listens on the exact path only. For instance, `Path: /products` would match `/products` but not `/products/shoes`.
|
|
||||||
|
|
||||||
Use a `*Prefix*` matcher if your backend listens on a particular base path but also serves requests on sub-paths. For instance, `PathPrefix: /products` would match `/products` but also `/products/shoes` and `/products/shirts`. Since the path is forwarded as-is, your backend is expected to listen on `/products`.
|
|
||||||
|
|
||||||
Use a `*Strip` matcher if your backend listens on the root path (`/`) but should be routeable on a specific prefix. For instance, `PathPrefixStrip: /products` would match `/products` but also `/products/shoes` and `/products/shirts`. Since the path is stripped prior to forwarding, your backend is expected to listen on `/`.
|
|
||||||
If your backend is serving assets (e.g., images or Javascript files), chances are it must return properly constructed relative URLs. Continuing on the example, the backend should return `/products/shoes/image.png` (and not `/images.png` which Traefik would likely not be able to associate with the same backend). The `X-Forwarded-Prefix` header (available since Traefik 1.3) can be queried to build such URLs dynamically.
|
|
||||||
|
|
||||||
Instead of distinguishing your backends by path only, you can add a Host matcher to the mix. That way, namespacing of your backends happens on the basis of hosts in addition to paths.
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
Here is an example of frontends definition:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[frontends]
|
|
||||||
[frontends.frontend1]
|
|
||||||
backend = "backend2"
|
|
||||||
[frontends.frontend1.routes.test_1]
|
|
||||||
rule = "Host:test.localhost,test2.localhost"
|
|
||||||
[frontends.frontend2]
|
|
||||||
backend = "backend1"
|
|
||||||
passHostHeader = true
|
|
||||||
priority = 10
|
|
||||||
entrypoints = ["https"] # overrides defaultEntryPoints
|
|
||||||
[frontends.frontend2.routes.test_1]
|
|
||||||
rule = "HostRegexp:localhost,{subdomain:[a-z]+}.localhost"
|
|
||||||
[frontends.frontend3]
|
|
||||||
backend = "backend2"
|
|
||||||
[frontends.frontend3.routes.test_1]
|
|
||||||
rule = "Host:test3.localhost;Path:/test"
|
|
||||||
```
|
|
||||||
|
|
||||||
- Three frontends are defined: `frontend1`, `frontend2` and `frontend3`
|
|
||||||
- `frontend1` will forward the traffic to the `backend2` if the rule `Host:test.localhost,test2.localhost` is matched
|
|
||||||
- `frontend2` will forward the traffic to the `backend1` if the rule `Host:localhost,{subdomain:[a-z]+}.localhost` is matched (forwarding client `Host` header to the backend)
|
|
||||||
- `frontend3` will forward the traffic to the `backend2` if the rules `Host:test3.localhost` **AND** `Path:/test` are matched
|
|
||||||
|
|
||||||
### Combining multiple rules
|
|
||||||
|
|
||||||
As seen in the previous example, you can combine multiple rules.
|
|
||||||
In TOML file, you can use multiple routes:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[frontends.frontend3]
|
|
||||||
backend = "backend2"
|
|
||||||
[frontends.frontend3.routes.test_1]
|
|
||||||
rule = "Host:test3.localhost"
|
|
||||||
[frontends.frontend3.routes.test_2]
|
|
||||||
rule = "Path:/test"
|
|
||||||
```
|
|
||||||
|
|
||||||
Here `frontend3` will forward the traffic to the `backend2` if the rules `Host:test3.localhost` **AND** `Path:/test` are matched.
|
|
||||||
You can also use the notation using a `;` separator, same result:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[frontends.frontend3]
|
|
||||||
backend = "backend2"
|
|
||||||
[frontends.frontend3.routes.test_1]
|
|
||||||
rule = "Host:test3.localhost;Path:/test"
|
|
||||||
```
|
|
||||||
|
|
||||||
Finally, you can create a rule to bind multiple domains or Path to a frontend, using the `,` separator:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[frontends.frontend2]
|
|
||||||
[frontends.frontend2.routes.test_1]
|
|
||||||
rule = "Host:test1.localhost,test2.localhost"
|
|
||||||
[frontends.frontend3]
|
|
||||||
backend = "backend2"
|
|
||||||
[frontends.frontend3.routes.test_1]
|
|
||||||
rule = "Path:/test1,/test2"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rules Order
|
|
||||||
|
|
||||||
When combining `Modifier` rules with `Matcher` rules, it is important to remember that `Modifier` rules **ALWAYS** apply after the `Matcher` rules.
|
|
||||||
The following rules are both `Matchers` and `Modifiers`, so the `Matcher` portion of the rule will apply first, and the `Modifier` will apply later.
|
|
||||||
|
|
||||||
- `PathStrip`
|
|
||||||
- `PathStripRegex`
|
|
||||||
- `PathPrefixStrip`
|
|
||||||
- `PathPrefixStripRegex`
|
|
||||||
|
|
||||||
`Modifiers` will be applied in a pre-determined order regardless of their order in the `rule` configuration section.
|
|
||||||
|
|
||||||
1. `PathStrip`
|
|
||||||
2. `PathPrefixStrip`
|
|
||||||
3. `PathStripRegex`
|
|
||||||
4. `PathPrefixStripRegex`
|
|
||||||
5. `AddPrefix`
|
|
||||||
6. `ReplacePath`
|
|
||||||
|
|
||||||
### Priorities
|
|
||||||
|
|
||||||
By default, routes will be sorted (in descending order) using rules length (to avoid path overlap):
|
|
||||||
`PathPrefix:/12345` will be matched before `PathPrefix:/1234` that will be matched before `PathPrefix:/1`.
|
|
||||||
|
|
||||||
You can customize priority by frontend:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[frontends]
|
|
||||||
[frontends.frontend1]
|
|
||||||
backend = "backend1"
|
|
||||||
priority = 10
|
|
||||||
passHostHeader = true
|
|
||||||
[frontends.frontend1.routes.test_1]
|
|
||||||
rule = "PathPrefix:/to"
|
|
||||||
[frontends.frontend2]
|
|
||||||
priority = 5
|
|
||||||
backend = "backend2"
|
|
||||||
passHostHeader = true
|
|
||||||
[frontends.frontend2.routes.test_1]
|
|
||||||
rule = "PathPrefix:/toto"
|
|
||||||
```
|
|
||||||
|
|
||||||
Here, `frontend1` will be matched before `frontend2` (`10 > 5`).
|
|
||||||
|
|
||||||
## Backends
|
|
||||||
|
|
||||||
A backend is responsible to load-balance the traffic coming from one or more frontends to a set of http servers.
|
|
||||||
Various methods of load-balancing are supported:
|
|
||||||
|
|
||||||
- `wrr`: Weighted Round Robin
|
|
||||||
- `drr`: Dynamic Round Robin: increases weights on servers that perform better than others. It also rolls back to original weights if the servers have changed.
|
|
||||||
|
|
||||||
A circuit breaker can also be applied to a backend, preventing high loads on failing servers.
|
|
||||||
Initial state is Standby. CB observes the statistics and does not modify the request.
|
|
||||||
In case the condition matches, CB enters Tripped state, where it responds with predefined code or redirects to another frontend.
|
|
||||||
Once Tripped timer expires, CB enters Recovering state and resets all stats.
|
|
||||||
In case the condition does not match and recovery timer expires, CB enters Standby state.
|
|
||||||
|
|
||||||
It can be configured using:
|
|
||||||
|
|
||||||
- Methods: `LatencyAtQuantileMS`, `NetworkErrorRatio`, `ResponseCodeRatio`
|
|
||||||
- Operators: `AND`, `OR`, `EQ`, `NEQ`, `LT`, `LE`, `GT`, `GE`
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
- `NetworkErrorRatio() > 0.5`: watch error ratio over 10 second sliding window for a frontend
|
|
||||||
- `LatencyAtQuantileMS(50.0) > 50`: watch latency at quantile in milliseconds.
|
|
||||||
- `ResponseCodeRatio(500, 600, 0, 600) > 0.5`: ratio of response codes in range [500-600) to [0-600)
|
|
||||||
|
|
||||||
To proactively prevent backends from being overwhelmed with high load, a maximum connection limit can
|
|
||||||
also be applied to each backend.
|
|
||||||
|
|
||||||
Maximum connections can be configured by specifying an integer value for `maxconn.amount` and
|
|
||||||
`maxconn.extractorfunc` which is a strategy used to determine how to categorize requests in order to
|
|
||||||
evaluate the maximum connections.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
```toml
|
|
||||||
[backends]
|
|
||||||
[backends.backend1]
|
|
||||||
[backends.backend1.maxconn]
|
|
||||||
amount = 10
|
|
||||||
extractorfunc = "request.host"
|
|
||||||
```
|
|
||||||
|
|
||||||
- `backend1` will return `HTTP code 429 Too Many Requests` if there are already 10 requests in progress for the same Host header.
|
|
||||||
- Another possible value for `extractorfunc` is `client.ip` which will categorize requests based on client source ip.
|
|
||||||
- Lastly `extractorfunc` can take the value of `request.header.ANY_HEADER` which will categorize requests based on `ANY_HEADER` that you provide.
|
|
||||||
|
|
||||||
Sticky sessions are supported with both load balancers. When sticky sessions are enabled, a cookie called `_TRAEFIK_BACKEND` is set on the initial
|
|
||||||
request. On subsequent requests, the client will be directed to the backend stored in the cookie if it is still healthy. If not, a new backend
|
|
||||||
will be assigned.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
```toml
|
|
||||||
[backends]
|
|
||||||
[backends.backend1]
|
|
||||||
[backends.backend1.loadbalancer]
|
|
||||||
sticky = true
|
|
||||||
```
|
|
||||||
|
|
||||||
A health check can be configured in order to remove a backend from LB rotation
|
|
||||||
as long as it keeps returning HTTP status codes other than 200 OK to HTTP GET
|
|
||||||
requests periodically carried out by Traefik. The check is defined by a path
|
|
||||||
appended to the backend URL and an interval (given in a format understood by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration)) specifying how
|
|
||||||
often the health check should be executed (the default being 30 seconds). Each
|
|
||||||
backend must respond to the health check within 5 seconds.
|
|
||||||
|
|
||||||
A recovering backend returning 200 OK responses again is being returned to the
|
|
||||||
LB rotation pool.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
```toml
|
|
||||||
[backends]
|
|
||||||
[backends.backend1]
|
|
||||||
[backends.backend1.healthcheck]
|
|
||||||
path = "/health"
|
|
||||||
interval = "10s"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Servers
|
|
||||||
|
|
||||||
Servers are simply defined using a `URL`. You can also apply a custom `weight` to each server (this will be used by load-balancing).
|
|
||||||
|
|
||||||
Here is an example of backends and servers definition:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[backends]
|
|
||||||
[backends.backend1]
|
|
||||||
[backends.backend1.circuitbreaker]
|
|
||||||
expression = "NetworkErrorRatio() > 0.5"
|
|
||||||
[backends.backend1.servers.server1]
|
|
||||||
url = "http://172.17.0.2:80"
|
|
||||||
weight = 10
|
|
||||||
[backends.backend1.servers.server2]
|
|
||||||
url = "http://172.17.0.3:80"
|
|
||||||
weight = 1
|
|
||||||
[backends.backend2]
|
|
||||||
[backends.backend2.LoadBalancer]
|
|
||||||
method = "drr"
|
|
||||||
[backends.backend2.servers.server1]
|
|
||||||
url = "http://172.17.0.4:80"
|
|
||||||
weight = 1
|
|
||||||
[backends.backend2.servers.server2]
|
|
||||||
url = "http://172.17.0.5:80"
|
|
||||||
weight = 2
|
|
||||||
```
|
|
||||||
|
|
||||||
- Two backends are defined: `backend1` and `backend2`
|
|
||||||
- `backend1` will forward the traffic to two servers: `http://172.17.0.2:80"` with weight `10` and `http://172.17.0.3:80` with weight `1` using default `wrr` load-balancing strategy.
|
|
||||||
- `backend2` will forward the traffic to two servers: `http://172.17.0.4:80"` with weight `1` and `http://172.17.0.5:80` with weight `2` using `drr` load-balancing strategy.
|
|
||||||
- a circuit breaker is added on `backend1` using the expression `NetworkErrorRatio() > 0.5`: watch error ratio over 10 second sliding window
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
|
|
||||||
Træfik's configuration has two parts:
|
|
||||||
|
|
||||||
- The [static Træfik configuration](/basics#static-trfk-configuration) which is loaded only at the beginning.
|
|
||||||
- The [dynamic Træfik configuration](/basics#dynamic-trfk-configuration) which can be hot-reloaded (no need to restart the process).
|
|
||||||
|
|
||||||
|
|
||||||
## Static Træfik configuration
|
|
||||||
|
|
||||||
The static configuration is the global configuration which is setting up connections to configuration backends and entrypoints.
|
|
||||||
|
|
||||||
Træfik can be configured using many configuration sources with the following precedence order.
|
|
||||||
Each item takes precedence over the item below it:
|
|
||||||
|
|
||||||
- [Key-value Store](/basics/#key-value-stores)
|
|
||||||
- [Arguments](/basics/#arguments)
|
|
||||||
- [Configuration file](/basics/#configuration-file)
|
|
||||||
- Default
|
|
||||||
|
|
||||||
It means that arguments override configuration file, and Key-value Store overrides arguments.
|
|
||||||
|
|
||||||
### Configuration file
|
|
||||||
|
|
||||||
By default, Træfik will try to find a `traefik.toml` in the following places:
|
|
||||||
|
|
||||||
- `/etc/traefik/`
|
|
||||||
- `$HOME/.traefik/`
|
|
||||||
- `.` *the working directory*
|
|
||||||
|
|
||||||
You can override this by setting a `configFile` argument:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ traefik --configFile=foo/bar/myconfigfile.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
Please refer to the [global configuration](/toml/#global-configuration) section to get documentation on it.
|
|
||||||
|
|
||||||
### Arguments
|
|
||||||
|
|
||||||
Each argument (and command) is described in the help section:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ traefik --help
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that all default values will be displayed as well.
|
|
||||||
|
|
||||||
### Key-value stores
|
|
||||||
|
|
||||||
Træfik supports several Key-value stores:
|
|
||||||
|
|
||||||
- [Consul](https://consul.io)
|
|
||||||
- [etcd](https://coreos.com/etcd/)
|
|
||||||
- [ZooKeeper](https://zookeeper.apache.org/)
|
|
||||||
- [boltdb](https://github.com/boltdb/bolt)
|
|
||||||
|
|
||||||
Please refer to the [User Guide Key-value store configuration](/user-guide/kv-config/) section to get documentation on it.
|
|
||||||
|
|
||||||
## Dynamic Træfik configuration
|
|
||||||
|
|
||||||
The dynamic configuration concerns :
|
|
||||||
|
|
||||||
- [Frontends](/basics/#frontends)
|
|
||||||
- [Backends](/basics/#backends)
|
|
||||||
- [Servers](/basics/#servers)
|
|
||||||
|
|
||||||
Træfik can hot-reload those rules which could be provided by [multiple configuration backends](/toml/#configuration-backends).
|
|
||||||
|
|
||||||
We only need to enable `watch` option to make Træfik watch configuration backend changes and generate its configuration automatically.
|
|
||||||
Routes to services will be created and updated instantly at any changes.
|
|
||||||
|
|
||||||
Please refer to the [configuration backends](/toml/#configuration-backends) section to get documentation on it.
|
|
||||||
|
|
||||||
# Commands
|
|
||||||
|
|
||||||
Usage: `traefik [command] [--flag=flag_argument]`
|
|
||||||
|
|
||||||
List of Træfik available commands with description :
|
|
||||||
|
|
||||||
- `version` : Print version
|
|
||||||
- `storeconfig` : Store the static traefik configuration into a Key-value stores. Please refer to the [Store Træfik configuration](/user-guide/kv-config/#store-trfk-configuration) section to get documentation on it.
|
|
||||||
|
|
||||||
Each command may have related flags.
|
|
||||||
All those related flags will be displayed with :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ traefik [command] --help
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that each command is described at the beginning of the help section:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ traefik --help
|
|
||||||
```
|
|
||||||
|
|
@@ -1,213 +0,0 @@
|
|||||||
# Benchmarks
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
I would like to thanks [vincentbernat](https://github.com/vincentbernat) from [exoscale.ch](https://www.exoscale.ch) who kindly provided the infrastructure needed for the benchmarks.
|
|
||||||
|
|
||||||
I used 4 VMs for the tests with the following configuration:
|
|
||||||
|
|
||||||
- 32 GB RAM
|
|
||||||
- 8 CPU Cores
|
|
||||||
- 10 GB SSD
|
|
||||||
- Ubuntu 14.04 LTS 64-bit
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
1. One VM used to launch the benchmarking tool [wrk](https://github.com/wg/wrk)
|
|
||||||
2. One VM for traefik (v1.0.0-beta.416) / nginx (v1.4.6)
|
|
||||||
3. Two VMs for 2 backend servers in go [whoami](https://github.com/emilevauge/whoamI/)
|
|
||||||
|
|
||||||
Each VM has been tuned using the following limits:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sysctl -w fs.file-max="9999999"
|
|
||||||
sysctl -w fs.nr_open="9999999"
|
|
||||||
sysctl -w net.core.netdev_max_backlog="4096"
|
|
||||||
sysctl -w net.core.rmem_max="16777216"
|
|
||||||
sysctl -w net.core.somaxconn="65535"
|
|
||||||
sysctl -w net.core.wmem_max="16777216"
|
|
||||||
sysctl -w net.ipv4.ip_local_port_range="1025 65535"
|
|
||||||
sysctl -w net.ipv4.tcp_fin_timeout="30"
|
|
||||||
sysctl -w net.ipv4.tcp_keepalive_time="30"
|
|
||||||
sysctl -w net.ipv4.tcp_max_syn_backlog="20480"
|
|
||||||
sysctl -w net.ipv4.tcp_max_tw_buckets="400000"
|
|
||||||
sysctl -w net.ipv4.tcp_no_metrics_save="1"
|
|
||||||
sysctl -w net.ipv4.tcp_syn_retries="2"
|
|
||||||
sysctl -w net.ipv4.tcp_synack_retries="2"
|
|
||||||
sysctl -w net.ipv4.tcp_tw_recycle="1"
|
|
||||||
sysctl -w net.ipv4.tcp_tw_reuse="1"
|
|
||||||
sysctl -w vm.min_free_kbytes="65536"
|
|
||||||
sysctl -w vm.overcommit_memory="1"
|
|
||||||
ulimit -n 9999999
|
|
||||||
```
|
|
||||||
|
|
||||||
### Nginx
|
|
||||||
|
|
||||||
Here is the config Nginx file use `/etc/nginx/nginx.conf`:
|
|
||||||
|
|
||||||
```
|
|
||||||
user www-data;
|
|
||||||
worker_processes auto;
|
|
||||||
worker_rlimit_nofile 200000;
|
|
||||||
pid /var/run/nginx.pid;
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 10000;
|
|
||||||
use epoll;
|
|
||||||
multi_accept on;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
sendfile on;
|
|
||||||
tcp_nopush on;
|
|
||||||
tcp_nodelay on;
|
|
||||||
keepalive_timeout 300;
|
|
||||||
keepalive_requests 10000;
|
|
||||||
types_hash_max_size 2048;
|
|
||||||
|
|
||||||
open_file_cache max=200000 inactive=300s;
|
|
||||||
open_file_cache_valid 300s;
|
|
||||||
open_file_cache_min_uses 2;
|
|
||||||
open_file_cache_errors on;
|
|
||||||
|
|
||||||
server_tokens off;
|
|
||||||
dav_methods off;
|
|
||||||
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log combined;
|
|
||||||
error_log /var/log/nginx/error.log warn;
|
|
||||||
|
|
||||||
gzip off;
|
|
||||||
gzip_vary off;
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/*.conf;
|
|
||||||
include /etc/nginx/sites-enabled/*.conf;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Here is the Nginx vhost file used:
|
|
||||||
|
|
||||||
```
|
|
||||||
upstream whoami {
|
|
||||||
server IP-whoami1:80;
|
|
||||||
server IP-whoami2:80;
|
|
||||||
keepalive 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 8001;
|
|
||||||
server_name test.traefik;
|
|
||||||
access_log off;
|
|
||||||
error_log /dev/null crit;
|
|
||||||
if ($host != "test.traefik") {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
location / {
|
|
||||||
proxy_pass http://whoami;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Connection "";
|
|
||||||
proxy_set_header X-Forwarded-Host $host;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Traefik
|
|
||||||
|
|
||||||
Here is the `traefik.toml` file used:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
MaxIdleConnsPerHost = 100000
|
|
||||||
defaultEntryPoints = ["http"]
|
|
||||||
|
|
||||||
[entryPoints]
|
|
||||||
[entryPoints.http]
|
|
||||||
address = ":8000"
|
|
||||||
|
|
||||||
[file]
|
|
||||||
[backends]
|
|
||||||
[backends.backend1]
|
|
||||||
[backends.backend1.servers.server1]
|
|
||||||
url = "http://IP-whoami1:80"
|
|
||||||
weight = 1
|
|
||||||
[backends.backend1.servers.server2]
|
|
||||||
url = "http://IP-whoami2:80"
|
|
||||||
weight = 1
|
|
||||||
|
|
||||||
[frontends]
|
|
||||||
[frontends.frontend1]
|
|
||||||
backend = "backend1"
|
|
||||||
[frontends.frontend1.routes.test_1]
|
|
||||||
rule = "Host: test.traefik"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Results
|
|
||||||
|
|
||||||
### whoami:
|
|
||||||
```shell
|
|
||||||
wrk -t20 -c1000 -d60s -H "Host: test.traefik" --latency http://IP-whoami:80/bench
|
|
||||||
Running 1m test @ http://IP-whoami:80/bench
|
|
||||||
20 threads and 1000 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 70.28ms 134.72ms 1.91s 89.94%
|
|
||||||
Req/Sec 2.92k 742.42 8.78k 68.80%
|
|
||||||
Latency Distribution
|
|
||||||
50% 10.63ms
|
|
||||||
75% 75.64ms
|
|
||||||
90% 205.65ms
|
|
||||||
99% 668.28ms
|
|
||||||
3476705 requests in 1.00m, 384.61MB read
|
|
||||||
Socket errors: connect 0, read 0, write 0, timeout 103
|
|
||||||
Requests/sec: 57894.35
|
|
||||||
Transfer/sec: 6.40MB
|
|
||||||
```
|
|
||||||
|
|
||||||
### nginx:
|
|
||||||
```shell
|
|
||||||
wrk -t20 -c1000 -d60s -H "Host: test.traefik" --latency http://IP-nginx:8001/bench
|
|
||||||
Running 1m test @ http://IP-nginx:8001/bench
|
|
||||||
20 threads and 1000 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 101.25ms 180.09ms 1.99s 89.34%
|
|
||||||
Req/Sec 1.69k 567.69 9.39k 72.62%
|
|
||||||
Latency Distribution
|
|
||||||
50% 15.46ms
|
|
||||||
75% 129.11ms
|
|
||||||
90% 302.44ms
|
|
||||||
99% 846.59ms
|
|
||||||
2018427 requests in 1.00m, 298.36MB read
|
|
||||||
Socket errors: connect 0, read 0, write 0, timeout 90
|
|
||||||
Requests/sec: 33591.67
|
|
||||||
Transfer/sec: 4.97MB
|
|
||||||
```
|
|
||||||
|
|
||||||
### traefik:
|
|
||||||
```shell
|
|
||||||
wrk -t20 -c1000 -d60s -H "Host: test.traefik" --latency http://IP-traefik:8000/bench
|
|
||||||
Running 1m test @ http://IP-traefik:8000/bench
|
|
||||||
20 threads and 1000 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 91.72ms 150.43ms 2.00s 90.50%
|
|
||||||
Req/Sec 1.43k 266.37 2.97k 69.77%
|
|
||||||
Latency Distribution
|
|
||||||
50% 19.74ms
|
|
||||||
75% 121.98ms
|
|
||||||
90% 237.39ms
|
|
||||||
99% 687.49ms
|
|
||||||
1705073 requests in 1.00m, 188.63MB read
|
|
||||||
Socket errors: connect 0, read 0, write 0, timeout 7
|
|
||||||
Requests/sec: 28392.44
|
|
||||||
Transfer/sec: 3.14MB
|
|
||||||
```
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
Traefik is obviously slower than Nginx, but not so much: Traefik can serve 28392 requests/sec and Nginx 33591 requests/sec which gives a ratio of 85%.
|
|
||||||
Not bad for young project :) !
|
|
||||||
|
|
||||||
Some areas of possible improvements:
|
|
||||||
|
|
||||||
- Use [GO_REUSEPORT](https://github.com/kavu/go_reuseport) listener
|
|
||||||
- Run a separate server instance per CPU core with `GOMAXPROCS=1` (it appears during benchmarks that there is a lot more context switches with traefik than with nginx)
|
|
||||||
|
|
43
docs/check.Dockerfile
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
|
||||||
|
FROM alpine:3.9 as alpine
|
||||||
|
|
||||||
|
# The "build-dependencies" virtual package provides build tools for html-proofer installation.
|
||||||
|
# It compile ruby-nokogiri, because alpine native version is always out of date
|
||||||
|
# This virtual package is cleaned at the end.
|
||||||
|
RUN apk --no-cache --no-progress add \
|
||||||
|
libcurl \
|
||||||
|
ruby \
|
||||||
|
ruby-bigdecimal \
|
||||||
|
ruby-etc \
|
||||||
|
ruby-ffi \
|
||||||
|
ruby-json \
|
||||||
|
&& apk add --no-cache --virtual build-dependencies \
|
||||||
|
build-base \
|
||||||
|
libcurl \
|
||||||
|
libxml2-dev \
|
||||||
|
libxslt-dev \
|
||||||
|
ruby-dev \
|
||||||
|
&& gem install --no-document html-proofer -v 3.10.2 \
|
||||||
|
&& apk del build-dependencies
|
||||||
|
|
||||||
|
# After Ruby, some NodeJS YAY!
|
||||||
|
RUN apk --no-cache --no-progress add \
|
||||||
|
git \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
&& npm install markdownlint@0.12.0 markdownlint-cli@0.13.0 --global
|
||||||
|
|
||||||
|
# Finally the shell tools we need for later
|
||||||
|
# tini helps to terminate properly all the parallelized tasks when sending CTRL-C
|
||||||
|
RUN apk --no-cache --no-progress add \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
tini
|
||||||
|
|
||||||
|
COPY ./scripts/verify.sh /verify.sh
|
||||||
|
COPY ./scripts/lint.sh /lint.sh
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
VOLUME ["/tmp","/app"]
|
||||||
|
|
||||||
|
ENTRYPOINT ["/sbin/tini","-g","sh"]
|
BIN
docs/content/assets/img/architecture-overview.png
Normal file
After Width: | Height: | Size: 361 KiB |
BIN
docs/content/assets/img/entrypoints.png
Normal file
After Width: | Height: | Size: 376 KiB |
BIN
docs/content/assets/img/middleware/addprefix.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
docs/content/assets/img/middleware/authforward.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
docs/content/assets/img/middleware/basicauth.png
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
docs/content/assets/img/middleware/buffering.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
docs/content/assets/img/middleware/chain.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
docs/content/assets/img/middleware/circuitbreaker.png
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
docs/content/assets/img/middleware/compress.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
docs/content/assets/img/middleware/digestauth.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
docs/content/assets/img/middleware/errorpages.png
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
docs/content/assets/img/middleware/headers.png
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
docs/content/assets/img/middleware/inflightreq.png
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
docs/content/assets/img/middleware/ipwhitelist.png
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
docs/content/assets/img/middleware/overview.png
Normal file
After Width: | Height: | Size: 307 KiB |
BIN
docs/content/assets/img/providers.png
Normal file
After Width: | Height: | Size: 377 KiB |
BIN
docs/content/assets/img/providers/docker.png
Normal file
After Width: | Height: | Size: 228 KiB |
BIN
docs/content/assets/img/providers/rancher.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
docs/content/assets/img/quickstart-diagram.png
Normal file
After Width: | Height: | Size: 289 KiB |
BIN
docs/content/assets/img/routers.png
Normal file
After Width: | Height: | Size: 354 KiB |
BIN
docs/content/assets/img/services.png
Normal file
After Width: | Height: | Size: 339 KiB |
BIN
docs/content/assets/img/static-dynamic-configuration.png
Normal file
After Width: | Height: | Size: 378 KiB |
BIN
docs/content/assets/img/traefik-architecture.png
Normal file
After Width: | Height: | Size: 452 KiB |
BIN
docs/content/assets/img/traefik-concepts-1.png
Normal file
After Width: | Height: | Size: 182 KiB |
BIN
docs/content/assets/img/traefik-concepts-2.png
Normal file
After Width: | Height: | Size: 209 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
4
docs/content/assets/img/user-guides/grpc.svg
Normal file
After Width: | Height: | Size: 186 KiB |
BIN
docs/content/assets/img/webui-dashboard.png
Normal file
After Width: | Height: | Size: 125 KiB |
4
docs/content/assets/js/extra.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/* Highlight */
|
||||||
|
(function(hljs) {
|
||||||
|
hljs.initHighlightingOnLoad();
|
||||||
|
})(hljs);
|
24
docs/content/assets/js/hljs/LICENSE
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Copyright (c) 2006, Ivan Sagalaev
|
||||||
|
All rights reserved.
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of highlight.js nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
|
||||||
|
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
2
docs/content/assets/js/hljs/highlight.pack.js
Normal file
96
docs/content/assets/styles/atom-one-light.css
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
Atom One Light by Daniel Gamage
|
||||||
|
Original One Light Syntax theme from https://github.com/atom/one-light-syntax
|
||||||
|
|
||||||
|
base: #fafafa
|
||||||
|
mono-1: #383a42
|
||||||
|
mono-2: #686b77
|
||||||
|
mono-3: #a0a1a7
|
||||||
|
hue-1: #0184bb
|
||||||
|
hue-2: #4078f2
|
||||||
|
hue-3: #a626a4
|
||||||
|
hue-4: #50a14f
|
||||||
|
hue-5: #e45649
|
||||||
|
hue-5-2: #c91243
|
||||||
|
hue-6: #986801
|
||||||
|
hue-6-2: #c18401
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0.5em;
|
||||||
|
color: #383a42;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote {
|
||||||
|
color: #a0a1a7;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-doctag,
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-formula {
|
||||||
|
color: #a626a4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-section,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-selector-tag,
|
||||||
|
.hljs-deletion,
|
||||||
|
.hljs-subst {
|
||||||
|
color: #e45649;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-literal {
|
||||||
|
color: #0184bb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-addition,
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-meta-string {
|
||||||
|
color: #50a14f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-class .hljs-title {
|
||||||
|
color: #c18401;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-attr,
|
||||||
|
.hljs-variable,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-selector-class,
|
||||||
|
.hljs-selector-attr,
|
||||||
|
.hljs-selector-pseudo,
|
||||||
|
.hljs-number {
|
||||||
|
color: #986801;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-bullet,
|
||||||
|
.hljs-link,
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-title {
|
||||||
|
color: #4078f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-link {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
63
docs/content/assets/styles/extra.css
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css?family=Noto+Sans|Noto+Serif');
|
||||||
|
|
||||||
|
.md-logo img {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for Chrome */
|
||||||
|
.md-typeset__table td code {
|
||||||
|
word-break: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-typeset__table tr :nth-child(1) {
|
||||||
|
word-wrap: break-word;
|
||||||
|
max-width: 30em;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: rgba(0,0,0,.9) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: bold !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: bold !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-typeset h5 {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-style: italic;
|
||||||
|
color: #8D909F;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.subtitle {
|
||||||
|
color: rgba(0,0,0,.54);
|
||||||
|
padding-top: 0;
|
||||||
|
margin-top: -2em;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .task-list-item {
|
||||||
|
list-style-type: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .task-list-item input[type="checkbox"] {
|
||||||
|
margin: 0 4px 0.25em -20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
10
docs/content/contributing/advocating.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Advocating
|
||||||
|
|
||||||
|
Spread the Love & Tell Us about It
|
||||||
|
{: .subtitle }
|
||||||
|
|
||||||
|
There are many ways to contribute to the project, and there is one that always spark joy: when we see/read about users talking about how Traefik helps them solve their problems.
|
||||||
|
|
||||||
|
If you're talking about Traefik, [let us know](https://blog.containo.us/spread-the-love-ba5a40aa72e7) and we'll promote your enthusiasm!
|
||||||
|
|
||||||
|
Also, if you've written about Traefik or shared useful information you'd like to promote, feel free to add links in the [dedicated wiki page on Github](https://github.com/containous/traefik/wiki/Awesome-Traefik).
|
173
docs/content/contributing/building-testing.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Building and Testing
|
||||||
|
|
||||||
|
Compile and Test Your Own Traefik!
|
||||||
|
{: .subtitle }
|
||||||
|
|
||||||
|
So you want to build your own Traefik binary from the sources?
|
||||||
|
Let's see how.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
You need either [Docker](https://github.com/docker/docker) and `make` (Method 1), or `go` (Method 2) in order to build Traefik.
|
||||||
|
For changes to its dependencies, the `dep` dependency management tool is required.
|
||||||
|
|
||||||
|
### Method 1: Using `Docker` and `Makefile`
|
||||||
|
|
||||||
|
Run make with the `binary` target.
|
||||||
|
This will create binaries for the Linux platform in the `dist` folder.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ make binary
|
||||||
|
docker build -t traefik-webui -f webui/Dockerfile webui
|
||||||
|
Sending build context to Docker daemon 2.686MB
|
||||||
|
Step 1/11 : FROM node:8.15.0
|
||||||
|
---> 1f6c34f7921c
|
||||||
|
[...]
|
||||||
|
Successfully built ce4ff439c06a
|
||||||
|
Successfully tagged traefik-webui:latest
|
||||||
|
[...]
|
||||||
|
docker build -t "traefik-dev:4475--feature-documentation" -f build.Dockerfile .
|
||||||
|
Sending build context to Docker daemon 279MB
|
||||||
|
Step 1/10 : FROM golang:1.13-alpine
|
||||||
|
---> f4bfb3d22bda
|
||||||
|
[...]
|
||||||
|
Successfully built 5c3c1a911277
|
||||||
|
Successfully tagged traefik-dev:4475--feature-documentation
|
||||||
|
docker run -e "TEST_CONTAINER=1" -v "/var/run/docker.sock:/var/run/docker.sock" -it -e OS_ARCH_ARG -e OS_PLATFORM_ARG -e TESTFLAGS -e VERBOSE -e VERSION -e CODENAME -e TESTDIRS -e CI -e CONTAINER=DOCKER -v "/home/ldez/sources/go/src/github.com/containous/traefik/"dist":/go/src/github.com/containous/traefik/"dist"" "traefik-dev:4475--feature-documentation" ./script/make.sh generate binary
|
||||||
|
---> Making bundle: generate (in .)
|
||||||
|
removed 'autogen/genstatic/gen.go'
|
||||||
|
|
||||||
|
---> Making bundle: binary (in .)
|
||||||
|
|
||||||
|
$ ls dist/
|
||||||
|
traefik*
|
||||||
|
```
|
||||||
|
|
||||||
|
The following targets can be executed outside Docker by setting the variable `PRE_TARGET` to an empty string (we don't recommend that):
|
||||||
|
|
||||||
|
- `test-unit`
|
||||||
|
- `test-integration`
|
||||||
|
- `validate`
|
||||||
|
- `binary` (the webUI is still generated by using Docker)
|
||||||
|
|
||||||
|
ex:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PRE_TARGET= make test-unit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: Using `go`
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
- `go` v1.13+
|
||||||
|
- environment variable `GO111MODULE=on`
|
||||||
|
|
||||||
|
!!! tip "Source Directory"
|
||||||
|
|
||||||
|
It is recommended that you clone Traefik into the `~/go/src/github.com/containous/traefik` directory.
|
||||||
|
This is the official golang workspace hierarchy that will allow dependencies to be properly resolved.
|
||||||
|
|
||||||
|
!!! note "Environment"
|
||||||
|
|
||||||
|
Set your `GOPATH` and `PATH` variable to be set to `~/go` via:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GOPATH=~/go
|
||||||
|
export PATH=$PATH:$GOPATH/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
For convenience, add `GOPATH` and `PATH` to your `.bashrc` or `.bash_profile`
|
||||||
|
|
||||||
|
Verify your environment is setup properly by running `$ go env`.
|
||||||
|
Depending on your OS and environment, you should see an output similar to:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GOARCH="amd64"
|
||||||
|
GOBIN=""
|
||||||
|
GOEXE=""
|
||||||
|
GOHOSTARCH="amd64"
|
||||||
|
GOHOSTOS="linux"
|
||||||
|
GOOS="linux"
|
||||||
|
GOPATH="/home/<yourusername>/go"
|
||||||
|
GORACE=""
|
||||||
|
## ... and the list goes on
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Build Traefik
|
||||||
|
|
||||||
|
Once you've set up your go environment and cloned the source repository, you can build Traefik.
|
||||||
|
Beforehand, you need to get `go-bindata` (the first time) in order to be able to use the `go generate` command (which is part of the build process).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/go/src/github.com/containous/traefik
|
||||||
|
|
||||||
|
# Get go-bindata. (Important: the ellipses are required.)
|
||||||
|
GO111MODULE=off go get github.com/containous/go-bindata/...
|
||||||
|
|
||||||
|
# Let's build
|
||||||
|
|
||||||
|
# generate
|
||||||
|
# (required to merge non-code components into the final binary, such as the web dashboard and the provider's templates)
|
||||||
|
go generate
|
||||||
|
|
||||||
|
# Standard go build
|
||||||
|
go build ./cmd/traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
You will find the Traefik executable (`traefik`) in the `~/go/src/github.com/containous/traefik` directory.
|
||||||
|
|
||||||
|
### Updating the templates
|
||||||
|
|
||||||
|
If you happen to update the provider's templates (located in `/templates`), you must run `go generate` to update the `autogen` package.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Method 1: `Docker` and `make`
|
||||||
|
|
||||||
|
Run unit tests using the `test-unit` target.
|
||||||
|
Run integration tests using the `test-integration` target.
|
||||||
|
Run all tests (unit and integration) using the `test` target.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ make test-unit
|
||||||
|
docker build -t "traefik-dev:your-feature-branch" -f build.Dockerfile .
|
||||||
|
# […]
|
||||||
|
docker run --rm -it -e OS_ARCH_ARG -e OS_PLATFORM_ARG -e TESTFLAGS -v "/home/user/go/src/github/containous/traefik/dist:/go/src/github.com/containous/traefik/dist" "traefik-dev:your-feature-branch" ./script/make.sh generate test-unit
|
||||||
|
---> Making bundle: generate (in .)
|
||||||
|
removed 'gen.go'
|
||||||
|
|
||||||
|
---> Making bundle: test-unit (in .)
|
||||||
|
+ go test -cover -coverprofile=cover.out .
|
||||||
|
ok github.com/containous/traefik 0.005s coverage: 4.1% of statements
|
||||||
|
|
||||||
|
Test success
|
||||||
|
```
|
||||||
|
|
||||||
|
For development purposes, you can specify which tests to run by using (only works the `test-integration` target):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run every tests in the MyTest suite
|
||||||
|
TESTFLAGS="-check.f MyTestSuite" make test-integration
|
||||||
|
|
||||||
|
# Run the test "MyTest" in the MyTest suite
|
||||||
|
TESTFLAGS="-check.f MyTestSuite.MyTest" make test-integration
|
||||||
|
|
||||||
|
# Run every tests starting with "My", in the MyTest suite
|
||||||
|
TESTFLAGS="-check.f MyTestSuite.My" make test-integration
|
||||||
|
|
||||||
|
# Run every tests ending with "Test", in the MyTest suite
|
||||||
|
TESTFLAGS="-check.f MyTestSuite.*Test" make test-integration
|
||||||
|
```
|
||||||
|
|
||||||
|
More: https://labix.org/gocheck
|
||||||
|
|
||||||
|
### Method 2: `go`
|
||||||
|
|
||||||
|
Unit tests can be run from the cloned directory using `$ go test ./...` which should return `ok`, similar to:
|
||||||
|
|
||||||
|
```test
|
||||||
|
ok _/home/user/go/src/github/containous/traefik 0.004s
|
||||||
|
```
|
||||||
|
|
||||||
|
Integration tests must be run from the `integration/` directory and require the `-integration` switch: `$ cd integration && go test -integration ./...`.
|
98
docs/content/contributing/data-collection.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# Data Collection
|
||||||
|
|
||||||
|
Understanding How Traefik is Being Used
|
||||||
|
{: .subtitle }
|
||||||
|
|
||||||
|
## Configuration Example
|
||||||
|
|
||||||
|
Understanding how you use Traefik is very important to us: it helps us improve the solution in many different ways.
|
||||||
|
For this very reason, the sendAnonymousUsage option is mandatory: we want you to take time to consider whether or not you wish to share anonymous data with us so we can benefit from your experience and use cases.
|
||||||
|
|
||||||
|
!!! example "Enabling Data Collection"
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[global]
|
||||||
|
# Send anonymous usage data
|
||||||
|
sendAnonymousUsage = true
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
global:
|
||||||
|
# Send anonymous usage data
|
||||||
|
sendAnonymousUsage: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
# Send anonymous usage data
|
||||||
|
--global.sendAnonymousUsage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Collected Data
|
||||||
|
|
||||||
|
This feature comes from the public proposal [here](https://github.com/containous/traefik/issues/2369).
|
||||||
|
|
||||||
|
In order to help us learn more about how Traefik is being used and improve it, we collect anonymous usage statistics from running instances.
|
||||||
|
Those data help us prioritize our developments and focus on what's important for our users (for example, which provider is popular, and which is not).
|
||||||
|
|
||||||
|
### What's collected / when ?
|
||||||
|
|
||||||
|
Once a day (the first call begins 10 minutes after the start of Traefik), we collect:
|
||||||
|
|
||||||
|
- the Traefik version number
|
||||||
|
- a hash of the configuration
|
||||||
|
- an **anonymized version** of the static configuration (token, user name, password, URL, IP, domain, email, etc, are removed).
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
We do not collect the dynamic configuration information (routers & services).
|
||||||
|
We do not collect these data to run advertising programs.
|
||||||
|
We do not sell these data to third-parties.
|
||||||
|
|
||||||
|
### Example of Collected Data
|
||||||
|
|
||||||
|
??? example "Original configuration"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":80"
|
||||||
|
|
||||||
|
[api]
|
||||||
|
|
||||||
|
[providers.docker]
|
||||||
|
endpoint = "tcp://10.10.10.10:2375"
|
||||||
|
exposedByDefault = true
|
||||||
|
swarmMode = true
|
||||||
|
|
||||||
|
[providers.docker.TLS]
|
||||||
|
ca = "dockerCA"
|
||||||
|
cert = "dockerCert"
|
||||||
|
key = "dockerKey"
|
||||||
|
insecureSkipVerify = true
|
||||||
|
```
|
||||||
|
|
||||||
|
??? example "Resulting Obfuscated Configuration"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":80"
|
||||||
|
|
||||||
|
[api]
|
||||||
|
|
||||||
|
[providers.docker]
|
||||||
|
endpoint = "xxxx"
|
||||||
|
exposedByDefault = true
|
||||||
|
swarmMode = true
|
||||||
|
|
||||||
|
[providers.docker.TLS]
|
||||||
|
ca = "xxxx"
|
||||||
|
cert = "xxxx"
|
||||||
|
key = "xxxx"
|
||||||
|
insecureSkipVerify = false
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Code for Data Collection
|
||||||
|
|
||||||
|
If you want to dig into more details, here is the source code of the collecting system: [collector.go](https://github.com/containous/traefik/blob/master/pkg/collector/collector.go)
|
||||||
|
|
||||||
|
By default we anonymize all configuration fields, except fields tagged with `export=true`.
|
100
docs/content/contributing/documentation.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Documentation
|
||||||
|
|
||||||
|
Features Are Better When You Know How to Use Them
|
||||||
|
{: .subtitle }
|
||||||
|
|
||||||
|
You've found something unclear in the documentation and want to give a try at explaining it better?
|
||||||
|
Let's see how.
|
||||||
|
|
||||||
|
## Building Documentation
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
This [documentation](https://docs.traefik.io/) is built with [mkdocs](https://mkdocs.org/).
|
||||||
|
|
||||||
|
### Method 1: `Docker` and `make`
|
||||||
|
|
||||||
|
You can build the documentation and test it locally (with live reloading), using the `docs` target:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ make docs
|
||||||
|
docker build -t traefik-docs -f docs.Dockerfile .
|
||||||
|
# […]
|
||||||
|
docker run --rm -v /home/user/go/github/containous/traefik:/mkdocs -p 8000:8000 traefik-docs mkdocs serve
|
||||||
|
# […]
|
||||||
|
[I 170828 20:47:48 server:283] Serving on http://0.0.0.0:8000
|
||||||
|
[I 170828 20:47:48 handlers:60] Start watching changes
|
||||||
|
[I 170828 20:47:48 handlers:62] Start detecting changes
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tip "Default URL"
|
||||||
|
|
||||||
|
Your local documentation server will run by default on [http://127.0.0.1:8000](http://127.0.0.1:8000).
|
||||||
|
|
||||||
|
If you only want to build the documentation without serving it locally, you can use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ make docs-build
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: `mkdocs`
|
||||||
|
|
||||||
|
First, make sure you have `python` and `pip` installed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ python --version
|
||||||
|
Python 2.7.2
|
||||||
|
$ pip --version
|
||||||
|
pip 1.5.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, install mkdocs with `pip`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install --user -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
To build the documentation locally and serve it locally, run `mkdocs serve` from the root directory.
|
||||||
|
This will start a local server.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mkdocs serve
|
||||||
|
INFO - Building documentation...
|
||||||
|
INFO - Cleaning site directory
|
||||||
|
[I 160505 22:31:24 server:281] Serving on http://127.0.0.1:8000
|
||||||
|
[I 160505 22:31:24 handlers:59] Start watching changes
|
||||||
|
[I 160505 22:31:24 handlers:61] Start detecting changes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check the Documentation
|
||||||
|
|
||||||
|
To check that the documentation meets standard expectations (no dead links, html markup validity, ...), use the `docs-verify` target.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ make docs-verify
|
||||||
|
docker build -t traefik-docs-verify ./script/docs-verify-docker-image ## Build Validator image
|
||||||
|
...
|
||||||
|
docker run --rm -v /home/travis/build/containous/traefik:/app traefik-docs-verify ## Check for dead links and w3c compliance
|
||||||
|
=== Checking HTML content...
|
||||||
|
Running ["HtmlCheck", "ImageCheck", "ScriptCheck", "LinkCheck"] on /app/site/basics/index.html on *.html...
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note "Clean & Verify"
|
||||||
|
|
||||||
|
If you've made changes to the documentation, it's safter to clean it before verifying it.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ make docs-clean docs-verify
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note "Disabling Documentation Verification"
|
||||||
|
|
||||||
|
Verification can be disabled by setting the environment variable `DOCS_VERIFY_SKIP` to `true`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
DOCS_VERIFY_SKIP=true make docs-verify
|
||||||
|
...
|
||||||
|
DOCS_LINT_SKIP is true: no linting done.
|
||||||
|
```
|
176
docs/content/contributing/maintainers.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# Maintainers
|
||||||
|
|
||||||
|
## The team
|
||||||
|
|
||||||
|
* Emile Vauge [@emilevauge](https://github.com/emilevauge)
|
||||||
|
* Vincent Demeester [@vdemeester](https://github.com/vdemeester)
|
||||||
|
* Ed Robinson [@errm](https://github.com/errm)
|
||||||
|
* Daniel Tomcej [@dtomcej](https://github.com/dtomcej)
|
||||||
|
* Manuel Zapf [@SantoDE](https://github.com/SantoDE)
|
||||||
|
* Timo Reimann [@timoreimann](https://github.com/timoreimann)
|
||||||
|
* Ludovic Fernandez [@ldez](https://github.com/ldez)
|
||||||
|
* Julien Salleyron [@juliens](https://github.com/juliens)
|
||||||
|
* Nicolas Mengin [@nmengin](https://github.com/nmengin)
|
||||||
|
* Marco Jantke [@marco-jantke](https://github.com/marco-jantke)
|
||||||
|
* Michaël Matur [@mmatur](https://github.com/mmatur)
|
||||||
|
* Gérald Croës [@geraldcroes](https://github.com/geraldcroes)
|
||||||
|
* Jean-Baptiste Doumenjou [@jbdoumenjou](https://github.com/jbdoumenjou)
|
||||||
|
* Damien Duportal [@dduportal](https://github.com/dduportal)
|
||||||
|
* Mathieu Lonjaret [@mpl](https://github.com/mpl)
|
||||||
|
|
||||||
|
## Contributions Daily Meeting
|
||||||
|
|
||||||
|
* 3 Maintainers should attend to a Contributions Daily Meeting where we sort and label new issues ([is:issue label:status/0-needs-triage](https://github.com/containous/traefik/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3Astatus%2F0-needs-triage+)), and review every Pull Requests
|
||||||
|
* Every pull request should be checked during the Contributions Daily Meeting
|
||||||
|
* Even if it’s already assigned
|
||||||
|
* Even PR labelled with `contributor/waiting-for-corrections` or `contributor/waiting-for-feedback`
|
||||||
|
* Issues labeled with `priority/P0` and `priority/P1` should be assigned.
|
||||||
|
* Modifying an issue or a pull request (labels, assignees, milestone) is only possible:
|
||||||
|
* During the Contributions Daily Meeting
|
||||||
|
* By an assigned maintainer
|
||||||
|
* In case of emergency, if a change proposal is approved by 2 other maintainers (on Slack, Discord, Discourse, etc)
|
||||||
|
|
||||||
|
## PR review process:
|
||||||
|
|
||||||
|
* The status `needs-design-review` is only used in complex/heavy/tricky PRs.
|
||||||
|
* From `1` to `2`: 1 comment that says “design LGTM” (by a senior maintainer).
|
||||||
|
* From `2` to `3`: 3 LGTM approvals by any maintainer.
|
||||||
|
* If needed, a specific maintainer familiar with a particular domain can be requested for the review.
|
||||||
|
* If a PR has been implemented in pair programming, one peer's LGTM goes into the review for free
|
||||||
|
* Amending someone else's pull request is authorized only in emergency, if a rebase is needed, or if the initial contributor is silent
|
||||||
|
|
||||||
|
We use [PRM](https://github.com/ldez/prm) to manage locally pull requests.
|
||||||
|
|
||||||
|
## Bots
|
||||||
|
|
||||||
|
### [Myrmica Lobicornis](https://github.com/containous/lobicornis/)
|
||||||
|
|
||||||
|
Update and Merge Pull Request.
|
||||||
|
|
||||||
|
The maintainer giving the final LGTM must add the `status/3-needs-merge` label to trigger the merge bot.
|
||||||
|
|
||||||
|
By default, a squash-rebase merge will be carried out.
|
||||||
|
To preserve commits, add `bot/merge-method-rebase` before `status/3-needs-merge`.
|
||||||
|
|
||||||
|
The status `status/4-merge-in-progress` is only used by the bot.
|
||||||
|
|
||||||
|
If the bot is not able to perform the merge, the label `bot/need-human-merge` is added.
|
||||||
|
In such a situation, solve the conflicts/CI/... and then remove the label `bot/need-human-merge`.
|
||||||
|
|
||||||
|
To prevent the bot from automatically merging a PR, add the label `bot/no-merge`.
|
||||||
|
|
||||||
|
The label `bot/light-review` decreases the number of required LGTM from 3 to 1.
|
||||||
|
|
||||||
|
This label is used when:
|
||||||
|
|
||||||
|
* Updating the vendors from previously reviewed PRs
|
||||||
|
* Merging branches into the master
|
||||||
|
* Preparing the release
|
||||||
|
|
||||||
|
### [Myrmica Bibikoffi](https://github.com/containous/bibikoffi/)
|
||||||
|
|
||||||
|
* closes stale issues [cron]
|
||||||
|
* use some criterion as number of days between creation, last update, labels, ...
|
||||||
|
|
||||||
|
### [Myrmica Aloba](https://github.com/containous/aloba)
|
||||||
|
|
||||||
|
Manage GitHub labels.
|
||||||
|
|
||||||
|
* Add labels on new PR [GitHub WebHook]
|
||||||
|
* Add milestone to a new PR based on a branch version (1.4, 1.3, ...) [GitHub WebHook]
|
||||||
|
* Add and remove `contributor/waiting-for-corrections` label when a review request changes [GitHub WebHook]
|
||||||
|
* Weekly report of PR status on Slack (CaptainPR) [cron]
|
||||||
|
|
||||||
|
## Labels
|
||||||
|
|
||||||
|
A maintainer that looks at an issue/PR must define its `kind/*`, `area/*`, and `status/*`.
|
||||||
|
|
||||||
|
### Status - Workflow
|
||||||
|
|
||||||
|
The `status/*` labels represent the desired state in the workflow.
|
||||||
|
|
||||||
|
* `status/0-needs-triage`: all the new issues and PRs have this status. _[bot only]_
|
||||||
|
* `status/1-needs-design-review`: needs a design review. **(only for PR)**
|
||||||
|
* `status/2-needs-review`: needs a code/documentation review. **(only for PR)**
|
||||||
|
* `status/3-needs-merge`: ready to merge. **(only for PR)**
|
||||||
|
* `status/4-merge-in-progress`: merge is in progress. _[bot only]_
|
||||||
|
|
||||||
|
### Contributor
|
||||||
|
|
||||||
|
* `contributor/need-more-information`: we need more information from the contributor in order to analyze a problem.
|
||||||
|
* `contributor/waiting-for-feedback`: we need the contributor to give us feedback.
|
||||||
|
* `contributor/waiting-for-corrections`: we need the contributor to take actions in order to move forward with a PR. **(only for PR)** _[bot, humans]_
|
||||||
|
* `contributor/needs-resolve-conflicts`: use it only when there is some conflicts (and an automatic rebase is not possible). **(only for PR)** _[bot, humans]_
|
||||||
|
|
||||||
|
### Kind
|
||||||
|
|
||||||
|
* `kind/enhancement`: a new or improved feature.
|
||||||
|
* `kind/question`: a question. **(only for issue)**
|
||||||
|
* `kind/proposal`: a proposal that needs to be discussed.
|
||||||
|
* _Proposal issues_ are design proposals
|
||||||
|
* _Proposal PRs_ are technical prototypes that need to be refined with multiple contributors.
|
||||||
|
|
||||||
|
* `kind/bug/possible`: a possible bug that needs analysis before it is confirmed or fixed. **(only for issues)**
|
||||||
|
* `kind/bug/confirmed`: a confirmed bug (reproducible). **(only for issues)**
|
||||||
|
* `kind/bug/fix`: a bug fix. **(only for PR)**
|
||||||
|
|
||||||
|
### Resolution
|
||||||
|
|
||||||
|
* `resolution/duplicate`: a duplicate issue/PR.
|
||||||
|
* `resolution/declined`: declined (Rule #1 of open-source: no is temporary, yes is forever).
|
||||||
|
* `WIP`: Work In Progress. **(only for PR)**
|
||||||
|
|
||||||
|
### Platform
|
||||||
|
|
||||||
|
* `platform/windows`: Windows related.
|
||||||
|
|
||||||
|
### Area
|
||||||
|
|
||||||
|
* `area/acme`: ACME related.
|
||||||
|
* `area/api`: Traefik API related.
|
||||||
|
* `area/authentication`: Authentication related.
|
||||||
|
* `area/cluster`: Traefik clustering related.
|
||||||
|
* `area/documentation`: Documentation related.
|
||||||
|
* `area/infrastructure`: CI or Traefik building scripts related.
|
||||||
|
* `area/healthcheck`: Health-check related.
|
||||||
|
* `area/logs`: Logs related.
|
||||||
|
* `area/middleware`: Middleware related.
|
||||||
|
* `area/middleware/metrics`: Metrics related. (Prometheus, StatsD, ...)
|
||||||
|
* `area/middleware/tracing`: Tracing related. (Jaeger, Zipkin, ...)
|
||||||
|
* `area/oxy`: Oxy related.
|
||||||
|
* `area/provider`: related to all providers.
|
||||||
|
* `area/provider/boltdb`: Boltd DB related.
|
||||||
|
* `area/provider/consul`: Consul related.
|
||||||
|
* `area/provider/docker`: Docker and Swarm related.
|
||||||
|
* `area/provider/ecs`: ECS related.
|
||||||
|
* `area/provider/etcd`: Etcd related.
|
||||||
|
* `area/provider/eureka`: Eureka related.
|
||||||
|
* `area/provider/file`: file provider related.
|
||||||
|
* `area/provider/k8s`: Kubernetes related.
|
||||||
|
* `area/provider/kv`: KV related.
|
||||||
|
* `area/provider/marathon`: Marathon related.
|
||||||
|
* `area/provider/mesos`: Mesos related.
|
||||||
|
* `area/provider/rancher`: Rancher related.
|
||||||
|
* `area/provider/servicefabric`: Azure service fabric related.
|
||||||
|
* `area/provider/zk`: Zoo Keeper related.
|
||||||
|
* `area/rules`: Rules related.
|
||||||
|
* `area/server`: Server related.
|
||||||
|
* `area/sticky-session`: Sticky session related.
|
||||||
|
* `area/tls`: TLS related.
|
||||||
|
* `area/websocket`: WebSocket related.
|
||||||
|
* `area/webui`: Web UI related.
|
||||||
|
|
||||||
|
### Issues Priority
|
||||||
|
|
||||||
|
* `priority/P0`: needs hot fix.
|
||||||
|
* `priority/P1`: need to be fixed in next release.
|
||||||
|
* `priority/P2`: need to be fixed in the future.
|
||||||
|
* `priority/P3`: maybe.
|
||||||
|
|
||||||
|
### PR size
|
||||||
|
|
||||||
|
Automatically set by a bot.
|
||||||
|
|
||||||
|
* `size/S`: small PR.
|
||||||
|
* `size/M`: medium PR.
|
||||||
|
* `size/L`: Large PR.
|