Compare commits
836 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c15458cd10 | ||
|
|
8fa672edd1 | ||
|
|
3d68a8ddb0 | ||
|
|
c8ded47fce | ||
|
|
f9e7274c55 | ||
|
|
a86b57e326 | ||
|
|
b312df68cf | ||
|
|
590bc364f3 | ||
|
|
5fc4314462 | ||
|
|
a2ed4aa847 | ||
|
|
9ffd00006d | ||
|
|
748ae21788 | ||
|
|
2b3ad7e7c4 | ||
|
|
d07bb785cf | ||
|
|
37fed1c94c | ||
|
|
601d725ff8 | ||
|
|
4914517330 | ||
|
|
bbaff67dbd | ||
|
|
d228cc6dee | ||
|
|
1810764092 | ||
|
|
aaec84a2ae | ||
|
|
6f40303aa3 | ||
|
|
5c4922538a | ||
|
|
156c6abffc | ||
|
|
deaa66117a | ||
|
|
bb63e17fbc | ||
|
|
7806ba76ab | ||
|
|
3d2c892973 | ||
|
|
d2226836a1 | ||
|
|
f510fef4ce | ||
|
|
cb7f2aafda | ||
|
|
c02401e94a | ||
|
|
aec4df9260 | ||
|
|
8928e191cc | ||
|
|
45ba37fa5a | ||
|
|
f20d5f88c5 | ||
|
|
cfc88af2d4 | ||
|
|
9575af7a2b | ||
|
|
6e180f2c29 | ||
|
|
b9e986abc0 | ||
|
|
efaf1fb336 | ||
|
|
0168670bca | ||
|
|
bc96c3e387 | ||
|
|
6473b7f96c | ||
|
|
8fe6bbfa35 | ||
|
|
0ad19f7dde | ||
|
|
fc1e9588ec | ||
|
|
1e63c503ff | ||
|
|
c5384ba086 | ||
|
|
220c2af8bc | ||
|
|
0f8734801b | ||
|
|
e88221b616 | ||
|
|
8fe2013f32 | ||
|
|
f4d893ede1 | ||
|
|
f8bba86158 | ||
|
|
eb86e37521 | ||
|
|
c2d6eb2329 | ||
|
|
e8ac3944a8 | ||
|
|
bbc2dd0cbe | ||
|
|
3babcb3302 | ||
|
|
a062a89401 | ||
|
|
2a71c5a32b | ||
|
|
e379c664f1 | ||
|
|
8b0b1bbeac | ||
|
|
ff093efe14 | ||
|
|
a4a34c624f | ||
|
|
8a021f4afb | ||
|
|
983d92c46f | ||
|
|
da50df986a | ||
|
|
471e7ad875 | ||
|
|
c3efac3bf5 | ||
|
|
a0f87f6fe2 | ||
|
|
87d95b1b37 | ||
|
|
43c2bc65a8 | ||
|
|
312632d55e | ||
|
|
0492869496 | ||
|
|
92905bcd1c | ||
|
|
aa72a7e049 | ||
|
|
68b2d8a11a | ||
|
|
deea6df2fe | ||
|
|
2c81b0ff74 | ||
|
|
53aee9178d | ||
|
|
abbc5d4b22 | ||
|
|
979e06b437 | ||
|
|
b1d7ff77e9 | ||
|
|
666f21d5af | ||
|
|
ff98621150 | ||
|
|
45c0e0b532 | ||
|
|
8544e497a5 | ||
|
|
5dcbeaad15 | ||
|
|
c124937ce9 | ||
|
|
6b6643c077 | ||
|
|
f90030d081 | ||
|
|
3d70c52f40 | ||
|
|
24bae7b645 | ||
|
|
8888aec7fc | ||
|
|
ac2bbea915 | ||
|
|
3ee936d6e7 | ||
|
|
7b61e7b047 | ||
|
|
e6156a9142 | ||
|
|
670282f19f | ||
|
|
3c1226db5b | ||
|
|
e1d5d96df1 | ||
|
|
5d8c2a697a | ||
|
|
ece93e239a | ||
|
|
d8624693cd | ||
|
|
882d759828 | ||
|
|
e365603bcd | ||
|
|
22b198a06d | ||
|
|
c2be62af10 | ||
|
|
87b8acea1c | ||
|
|
35c587e08e | ||
|
|
7195bab2ee | ||
|
|
980ea3fe6a | ||
|
|
c931574615 | ||
|
|
59e784c7ed | ||
|
|
439f97f1e1 | ||
|
|
7acec88b03 | ||
|
|
d22a4dff72 | ||
|
|
82570ce986 | ||
|
|
6388d7d3fc | ||
|
|
ee301949a9 | ||
|
|
3a4be02bc9 | ||
|
|
ecb9594bd0 | ||
|
|
29caf91d79 | ||
|
|
2574114d3e | ||
|
|
4097c3e90e | ||
|
|
d98e841bc8 | ||
|
|
af86681f16 | ||
|
|
5f228e4bdc | ||
|
|
177ca6e6f9 | ||
|
|
772b8a4f2d | ||
|
|
8bcaf051e2 | ||
|
|
a6258bbdfb | ||
|
|
52e73534c7 | ||
|
|
6c4996905a | ||
|
|
7dc8076840 | ||
|
|
19ce70ba1c | ||
|
|
f026e7291c | ||
|
|
129b5a75b2 | ||
|
|
d354c2b751 | ||
|
|
bae3ecca7b | ||
|
|
a7241b6fbe | ||
|
|
e66f3f5a59 | ||
|
|
237032a714 | ||
|
|
fb622085ce | ||
|
|
b5a653e305 | ||
|
|
41ee1c569a | ||
|
|
d48b4a262f | ||
|
|
64cdc9457d | ||
|
|
1cf1121856 | ||
|
|
dd2a732525 | ||
|
|
67cf6734ff | ||
|
|
abfd6753da | ||
|
|
65d1ea047b | ||
|
|
cc3fd3fe67 | ||
|
|
acc39b3097 | ||
|
|
773e736123 | ||
|
|
2414ef7424 | ||
|
|
1ab60cba59 | ||
|
|
78685301fb | ||
|
|
841f000c75 | ||
|
|
caa83c17d9 | ||
|
|
0a17e06d8f | ||
|
|
d3650f33a3 | ||
|
|
77c2d7f0c5 | ||
|
|
9821a78847 | ||
|
|
a3cd0a5743 | ||
|
|
fd0091fb4c | ||
|
|
fc34a9dd08 | ||
|
|
747dba19b7 | ||
|
|
6e826b08f3 | ||
|
|
922b8ca832 | ||
|
|
175015ce9e | ||
|
|
ed13c83b0e | ||
|
|
e1a6681f5f | ||
|
|
e14b340f00 | ||
|
|
b465f58a74 | ||
|
|
ea62473ab8 | ||
|
|
3f924fc5ad | ||
|
|
cc9dda416b | ||
|
|
d31a1ccea9 | ||
|
|
a86a86a4ba | ||
|
|
f2706e4fc6 | ||
|
|
ed3672cb54 | ||
|
|
14373f5123 | ||
|
|
5154002cb2 | ||
|
|
9d0f8cd63a | ||
|
|
b8fa7a591f | ||
|
|
6a912b2784 | ||
|
|
5755224c2a | ||
|
|
676a80038b | ||
|
|
3eafab1e0b | ||
|
|
f66a9c14c7 | ||
|
|
1906a86374 | ||
|
|
b29bb29654 | ||
|
|
fa7912f829 | ||
|
|
42f4f67087 | ||
|
|
1e4763c450 | ||
|
|
ca20f7c0a3 | ||
|
|
825491fb06 | ||
|
|
40ebe10b93 | ||
|
|
60186225a7 | ||
|
|
dbeb53198b | ||
|
|
08a6f0edeb | ||
|
|
f3bc71a07b | ||
|
|
4736d96752 | ||
|
|
9c0832a8b5 | ||
|
|
1c302cfac7 | ||
|
|
d6c7e90311 | ||
|
|
456e6fa087 | ||
|
|
ce8186cc15 | ||
|
|
17a9a6777a | ||
|
|
0752bb9a10 | ||
|
|
0360950eec | ||
|
|
99f7e0d980 | ||
|
|
b04e950358 | ||
|
|
328bc80c14 | ||
|
|
203e6ab780 | ||
|
|
2f467ff83e | ||
|
|
e9234a827c | ||
|
|
b773e61c69 | ||
|
|
898b5379da | ||
|
|
68833e4786 | ||
|
|
6a26c4ecf0 | ||
|
|
6118cda6de | ||
|
|
d5bac298ec | ||
|
|
2286bc65a7 | ||
|
|
254faa7145 | ||
|
|
a927ba66a5 | ||
|
|
6c9b6b7768 | ||
|
|
77abe748f0 | ||
|
|
6e17e178da | ||
|
|
55f4cff699 | ||
|
|
6ed4dcc86a | ||
|
|
fad25076f7 | ||
|
|
6c796ecd2c | ||
|
|
df9c0f6863 | ||
|
|
6be5d7e42d | ||
|
|
69dd53d669 | ||
|
|
3673ea9b72 | ||
|
|
0a4f53b7c9 | ||
|
|
f5a84c249a | ||
|
|
63a9f24193 | ||
|
|
fa37625f36 | ||
|
|
a73fda2a29 | ||
|
|
75c80bdc96 | ||
|
|
2414a65371 | ||
|
|
83ca9b0a89 | ||
|
|
6bceaee801 | ||
|
|
70dfa399e4 | ||
|
|
8e89aa7a28 | ||
|
|
9a1938c071 | ||
|
|
7e150daac7 | ||
|
|
3386dd3af9 | ||
|
|
293a03153c | ||
|
|
092fa34e1a | ||
|
|
20d42bf83c | ||
|
|
a7ee8c6061 | ||
|
|
1afcedc3e3 | ||
|
|
8e49c76039 | ||
|
|
c9599676c9 | ||
|
|
d547a4ca81 | ||
|
|
f2764fad0c | ||
|
|
cf381dfc1a | ||
|
|
0806405099 | ||
|
|
d4f2b50a5e | ||
|
|
584166f80d | ||
|
|
23f5d5e4e7 | ||
|
|
57d0c70ad7 | ||
|
|
d3e02e24c5 | ||
|
|
9fd2f387c5 | ||
|
|
2f71ef6063 | ||
|
|
1a7416d5c7 | ||
|
|
eec14bbe18 | ||
|
|
9a9997d5e0 | ||
|
|
2c041bc351 | ||
|
|
8f98c28b96 | ||
|
|
da3071cfa0 | ||
|
|
5484947ee3 | ||
|
|
c0f31b2ac5 | ||
|
|
d39c0e65aa | ||
|
|
d090da9dcf | ||
|
|
2ea63a2609 | ||
|
|
5766f7d1c5 | ||
|
|
0b762f6b88 | ||
|
|
51e708742d | ||
|
|
99d74aefa4 | ||
|
|
0105f6d1a4 | ||
|
|
9168739140 | ||
|
|
429c82c372 | ||
|
|
4f65f43267 | ||
|
|
bae86f4956 | ||
|
|
e0f550374f | ||
|
|
2dc9b9d921 | ||
|
|
bb11e88e26 | ||
|
|
63392b9aab | ||
|
|
6fe24d3bbf | ||
|
|
340f4d9baa | ||
|
|
aa37ca02b5 | ||
|
|
34b3b7e1e3 | ||
|
|
11067205e9 | ||
|
|
753e909411 | ||
|
|
14d2ae53bc | ||
|
|
93ab440dd9 | ||
|
|
612cf9408f | ||
|
|
da6287d060 | ||
|
|
b84b5a2d8d | ||
|
|
b219c984c4 | ||
|
|
b11dc46d7b | ||
|
|
bff0f0a5bb | ||
|
|
61637599d8 | ||
|
|
d9642eec3f | ||
|
|
d277b4e878 | ||
|
|
e84c9c874b | ||
|
|
682822eef7 | ||
|
|
9fcbcadba7 | ||
|
|
198ba36dbd | ||
|
|
3a48f9f194 | ||
|
|
df0b584c8e | ||
|
|
d794ca1c7f | ||
|
|
00c143a8c8 | ||
|
|
c2d8f330af | ||
|
|
c5d89dc981 | ||
|
|
784bc40e76 | ||
|
|
74e13abded | ||
|
|
2519bf2815 | ||
|
|
7b0477755d | ||
|
|
a6cec39b16 | ||
|
|
277648b4ff | ||
|
|
b76f0aa3f8 | ||
|
|
5a35458586 | ||
|
|
fb0bec2950 | ||
|
|
7af36da76e | ||
|
|
4017da77fb | ||
|
|
6f336bbcae | ||
|
|
6db4c9ba65 | ||
|
|
8fb35f9ff6 | ||
|
|
e41e34d45c | ||
|
|
cb82e5d221 | ||
|
|
23aba02e41 | ||
|
|
c281818579 | ||
|
|
42a8a9af80 | ||
|
|
33e86ac18e | ||
|
|
b4dae48a5e | ||
|
|
8b9704bc69 | ||
|
|
def504ca04 | ||
|
|
451dbefc93 | ||
|
|
1e35ffb1fe | ||
|
|
570b0246b7 | ||
|
|
b91360220c | ||
|
|
1c82ba87ba | ||
|
|
14e64b8460 | ||
|
|
314549c038 | ||
|
|
df059f7e63 | ||
|
|
2a620d8c6e | ||
|
|
ba0de92f84 | ||
|
|
38fd3dd372 | ||
|
|
14b5eac802 | ||
|
|
25fc2710fc | ||
|
|
54fff1e191 | ||
|
|
0f7a940e11 | ||
|
|
b53895dead | ||
|
|
d0cb1cad44 | ||
|
|
ebdd78d6b0 | ||
|
|
3f5be54938 | ||
|
|
fca8ef26c5 | ||
|
|
aa12682a42 | ||
|
|
bfa3714634 | ||
|
|
a7c3ee024b | ||
|
|
63eb64cbde | ||
|
|
fd58b5c248 | ||
|
|
126d121e34 | ||
|
|
30331275f6 | ||
|
|
8571dddd98 | ||
|
|
cf6f3945b5 | ||
|
|
f644f7b9b3 | ||
|
|
2e656e8882 | ||
|
|
a277b7c00e | ||
|
|
26d7d1620e | ||
|
|
e7b65efc7a | ||
|
|
3884eb9648 | ||
|
|
c1a02644b6 | ||
|
|
63135ded8a | ||
|
|
ded48ab821 | ||
|
|
c014837e41 | ||
|
|
c0768d7843 | ||
|
|
32a6fe1d91 | ||
|
|
6187483bc2 | ||
|
|
59143841c9 | ||
|
|
f9f22dbdf2 | ||
|
|
3da3a319af | ||
|
|
2d5a9a2b42 | ||
|
|
50d34442b7 | ||
|
|
d52a4d86e9 | ||
|
|
0b18c00db3 | ||
|
|
561c8372da | ||
|
|
db32fa51aa | ||
|
|
6a28fd9474 | ||
|
|
d52961e06e | ||
|
|
969728eb69 | ||
|
|
5001340f0d | ||
|
|
7d21f2d5db | ||
|
|
58613fe771 | ||
|
|
62ab55b1e4 | ||
|
|
6dc0c93588 | ||
|
|
84e24b9ec8 | ||
|
|
f689e9c3d3 | ||
|
|
054f1c616f | ||
|
|
8bfe5b5193 | ||
|
|
120f70d39a | ||
|
|
66f5fcd06b | ||
|
|
e934639220 | ||
|
|
52a8d6489d | ||
|
|
4176202bcc | ||
|
|
09731be502 | ||
|
|
aea496422a | ||
|
|
8970bb7db9 | ||
|
|
f4fbdaa42f | ||
|
|
07330ecabe | ||
|
|
c5a3e2a7e7 | ||
|
|
de0de15b5b | ||
|
|
b8bd70adc3 | ||
|
|
ff11f8e0cc | ||
|
|
7d118fd162 | ||
|
|
6a01e0ca16 | ||
|
|
12ec8189f3 | ||
|
|
9b62073ce6 | ||
|
|
579df88946 | ||
|
|
3f27e988ed | ||
|
|
e5a40d4f5f | ||
|
|
2e86873908 | ||
|
|
90fefe660a | ||
|
|
b0c2b60ecf | ||
|
|
0a49f55ebe | ||
|
|
fc25775754 | ||
|
|
07dc02403d | ||
|
|
2b9bba4ba0 | ||
|
|
fdec2c3d71 | ||
|
|
e8ec89f99a | ||
|
|
3899ecda5a | ||
|
|
495b97d655 | ||
|
|
c91e63ee4c | ||
|
|
466b47c623 | ||
|
|
23430d9cdf | ||
|
|
1b9d35fa12 | ||
|
|
4589399ecf | ||
|
|
73a8cd32cf | ||
|
|
f808926dbb | ||
|
|
27f20a3d78 | ||
|
|
4fdd851242 | ||
|
|
62898eae41 | ||
|
|
91b779db43 | ||
|
|
f6a934e013 | ||
|
|
42d53004eb | ||
|
|
66310ffd8a | ||
|
|
4578e6c895 | ||
|
|
8bfff2c601 | ||
|
|
ca7f91ad3b | ||
|
|
c98e90f89b | ||
|
|
78268ae2fd | ||
|
|
783c8bdcf5 | ||
|
|
d0f776fe2f | ||
|
|
aa7169f535 | ||
|
|
b98bae89c0 | ||
|
|
8681ff042e | ||
|
|
07800c3ffc | ||
|
|
d9c6df4f81 | ||
|
|
3b7631a424 | ||
|
|
df74eb3c72 | ||
|
|
be6f78f760 | ||
|
|
11a70e22bc | ||
|
|
024dd22896 | ||
|
|
564d089172 | ||
|
|
87ade43fb7 | ||
|
|
04aac0c4f9 | ||
|
|
ee90ab021f | ||
|
|
8c5cd12a30 | ||
|
|
86bc413695 | ||
|
|
e7606fd960 | ||
|
|
4791790874 | ||
|
|
afd481d354 | ||
|
|
7ce5e5d883 | ||
|
|
5b95dd1c20 | ||
|
|
94c1afe16f | ||
|
|
729ba9b26f | ||
|
|
f443ffb646 | ||
|
|
6a74768e30 | ||
|
|
e5d772879c | ||
|
|
d0676fc83c | ||
|
|
93fb2b3007 | ||
|
|
f4295c46e1 | ||
|
|
c56175e625 | ||
|
|
5eefffac4b | ||
|
|
9678869bb2 | ||
|
|
c71ebf73c6 | ||
|
|
7b74b45d4c | ||
|
|
b7e5f639b5 | ||
|
|
16bfe96bca | ||
|
|
911ab42b94 | ||
|
|
cd775bcba7 | ||
|
|
b7c75474ff | ||
|
|
8f9a89fd59 | ||
|
|
6190c1627c | ||
|
|
1eff26a19c | ||
|
|
3467dae99a | ||
|
|
8947c5f346 | ||
|
|
56d4db6df4 | ||
|
|
8461f656af | ||
|
|
abd91dae2e | ||
|
|
2d1de562d4 | ||
|
|
b48f387dac | ||
|
|
0355ef4486 | ||
|
|
553cc9f337 | ||
|
|
354d328ff8 | ||
|
|
4fc1a0efc1 | ||
|
|
9a3be58542 | ||
|
|
34f7a98035 | ||
|
|
3a24c6e665 | ||
|
|
4bde14bd6c | ||
|
|
b99a227953 | ||
|
|
09a700b36e | ||
|
|
eab577607f | ||
|
|
ea6e3e0725 | ||
|
|
187138d7b1 | ||
|
|
dba0ddd344 | ||
|
|
4b9aece222 | ||
|
|
e756b609e8 | ||
|
|
ba99a113cb | ||
|
|
8be56bc673 | ||
|
|
9bbd97a3a6 | ||
|
|
02de518971 | ||
|
|
a627f2ffd5 | ||
|
|
0448416819 | ||
|
|
3b2dfdc2f4 | ||
|
|
efa5777766 | ||
|
|
38713d1f48 | ||
|
|
1259438a3b | ||
|
|
ba494ddd57 | ||
|
|
c5b84959c0 | ||
|
|
2333084085 | ||
|
|
caa8f9964a | ||
|
|
2c756e8b46 | ||
|
|
1e1e956397 | ||
|
|
2594df9882 | ||
|
|
8da054d3fb | ||
|
|
3e7ef846ad | ||
|
|
f069c65496 | ||
|
|
cca37c0559 | ||
|
|
a9da3163eb | ||
|
|
7cff89ae26 | ||
|
|
0a3a273aac | ||
|
|
905ee32256 | ||
|
|
b0c7ce13e8 | ||
|
|
6a439840f0 | ||
|
|
35e7e08a07 | ||
|
|
2cb02f76a1 | ||
|
|
16d286ba8f | ||
|
|
94999dc95b | ||
|
|
08fcfd8ec4 | ||
|
|
4a253cab1a | ||
|
|
911a25e0f0 | ||
|
|
f110a4c2f3 | ||
|
|
ee18405f54 | ||
|
|
be7012a860 | ||
|
|
4892dcd892 | ||
|
|
8538fb2f55 | ||
|
|
2592d62ed9 | ||
|
|
f91c7ccb21 | ||
|
|
24ade2f856 | ||
|
|
756a8e4643 | ||
|
|
34533cd830 | ||
|
|
21e88b645e | ||
|
|
775715a17c | ||
|
|
a59f7caafc | ||
|
|
6903299523 | ||
|
|
1f34ebfdc1 | ||
|
|
98ee491865 | ||
|
|
d408cfca1d | ||
|
|
a8366174e9 | ||
|
|
1b97d18ef0 | ||
|
|
678b197d6a | ||
|
|
86bb4e12b3 | ||
|
|
32dd847f4f | ||
|
|
35a5093f8e | ||
|
|
6b5f5aeae3 | ||
|
|
b41f315a25 | ||
|
|
376ee2d730 | ||
|
|
79a42bf9fb | ||
|
|
2eff0dbeee | ||
|
|
da9cddb691 | ||
|
|
184e742b1b | ||
|
|
42287f8848 | ||
|
|
6495531d45 | ||
|
|
3045d6011f | ||
|
|
8a78db30c6 | ||
|
|
cbe8aede9c | ||
|
|
a0019b1019 | ||
|
|
4e6d9c4c40 | ||
|
|
54a636163c | ||
|
|
cc99eaa819 | ||
|
|
541227494f | ||
|
|
f4987ff9c3 | ||
|
|
c7ce201050 | ||
|
|
283dd90b46 | ||
|
|
5a7768f988 | ||
|
|
a84c1ce964 | ||
|
|
cd51084efe | ||
|
|
eb3fa00089 | ||
|
|
04b219236c | ||
|
|
6f3917edb8 | ||
|
|
e06954a92c | ||
|
|
9ebefc3698 | ||
|
|
c938b2ea1b | ||
|
|
9668b2cccd | ||
|
|
5b5716b51b | ||
|
|
93a7f9dc7d | ||
|
|
d7a52b569c | ||
|
|
1b4a4e626a | ||
|
|
05b26bea9c | ||
|
|
08cd8a21d4 | ||
|
|
4a7c0f55a7 | ||
|
|
64b39d6d2b | ||
|
|
cbca6a8413 | ||
|
|
9cfc20815a | ||
|
|
71f214e20d | ||
|
|
63f132c820 | ||
|
|
ffd964fe82 | ||
|
|
63dd413296 | ||
|
|
88bb3f296a | ||
|
|
f6e0e4ed08 | ||
|
|
707ab974a1 | ||
|
|
fa0f743cb4 | ||
|
|
bde434851c | ||
|
|
dc8f6f722e | ||
|
|
649e577483 | ||
|
|
4480587ae8 | ||
|
|
a4db64300c | ||
|
|
8925a6ed14 | ||
|
|
c5bd1fc735 | ||
|
|
61a35663dc | ||
|
|
2a52d7b6a1 | ||
|
|
6adec499e1 | ||
|
|
5990f126f4 | ||
|
|
e1d66b9c78 | ||
|
|
6014e10f62 | ||
|
|
457b760da5 | ||
|
|
e8ab871efb | ||
|
|
1651025969 | ||
|
|
b81d718b7e | ||
|
|
2600703625 | ||
|
|
9a613e0b85 | ||
|
|
8e63a5742a | ||
|
|
c9475e10af | ||
|
|
b10c99ae3e | ||
|
|
b93346b673 | ||
|
|
4753dbd847 | ||
|
|
44471e9d3a | ||
|
|
0f5a89aae4 | ||
|
|
d5bdaa172b | ||
|
|
714975ca95 | ||
|
|
68f364c7ff | ||
|
|
f34144e7fc | ||
|
|
e485f0fe9a | ||
|
|
115e7de7bf | ||
|
|
080b0b29a7 | ||
|
|
ebc9158a45 | ||
|
|
38a3167cbd | ||
|
|
b48a75f30d | ||
|
|
5b10a39ad5 | ||
|
|
1d7048115e | ||
|
|
1eb373da5e | ||
|
|
9cf522f7df | ||
|
|
153a620676 | ||
|
|
b1feba8314 | ||
|
|
277701bc56 | ||
|
|
39c250993e | ||
|
|
d8b82c8200 | ||
|
|
93faeaaedc | ||
|
|
35d08a0a97 | ||
|
|
921082e32f | ||
|
|
0dd119efae | ||
|
|
2019d29161 | ||
|
|
c22afb19f3 | ||
|
|
50de4b7f11 | ||
|
|
a9c119297f | ||
|
|
3c954a0840 | ||
|
|
494d95e9cd | ||
|
|
489460042d | ||
|
|
2315965eb2 | ||
|
|
0a5e6da3ae | ||
|
|
d9aedd78ba | ||
|
|
37fdeb6d18 | ||
|
|
3bd3dd2d04 | ||
|
|
af42d980d0 | ||
|
|
2070b9eee4 | ||
|
|
8651b37723 | ||
|
|
22c350aed5 | ||
|
|
80686bc279 | ||
|
|
2b3a60b950 | ||
|
|
08b52106fb | ||
|
|
1ed7c4e513 | ||
|
|
3b2b46f4ac | ||
|
|
b356ffcd68 | ||
|
|
197c2cc87b | ||
|
|
ee7e919c12 | ||
|
|
e4275003b3 | ||
|
|
0da82500f3 | ||
|
|
c14e8bfd1a | ||
|
|
d8eda3384b | ||
|
|
fe14cd184a | ||
|
|
5a367fa20a | ||
|
|
2db858a227 | ||
|
|
3cc17c0802 | ||
|
|
72d20457b1 | ||
|
|
1efa49644e | ||
|
|
e733049876 | ||
|
|
0562c9a625 | ||
|
|
40c3c2d23e | ||
|
|
366329f497 | ||
|
|
7942ae66b7 | ||
|
|
a0d65b7fc5 | ||
|
|
b4623a6d8b | ||
|
|
77a70faf04 | ||
|
|
6a44ebaa5c | ||
|
|
1d411b490b | ||
|
|
1b42514642 | ||
|
|
d371aa5111 | ||
|
|
4743c8be8f | ||
|
|
ff0a774549 | ||
|
|
94ec9e49f0 | ||
|
|
a2fe47b364 | ||
|
|
c04535d9b0 | ||
|
|
1da26a49c1 | ||
|
|
459f3f534d | ||
|
|
2b73c79c5d | ||
|
|
321100935c | ||
|
|
1af781f7a4 | ||
|
|
af4215f0fb | ||
|
|
6474ebf9c2 | ||
|
|
bb4a4407c7 | ||
|
|
e534397dc5 | ||
|
|
a0769a3bdc | ||
|
|
7586b350f3 | ||
|
|
7af1a65503 | ||
|
|
3e29ea82d0 | ||
|
|
082ad4d222 | ||
|
|
a64e6f804c | ||
|
|
82e3eb3671 | ||
|
|
baa2c5583f | ||
|
|
ef1d872c68 | ||
|
|
729e894553 | ||
|
|
1356f0bfc6 | ||
|
|
1c576b71a7 | ||
|
|
adf5055397 | ||
|
|
4011cc5f65 | ||
|
|
df38a3d138 | ||
|
|
c08cadcb89 | ||
|
|
9a6f9afa18 | ||
|
|
08201fc6d3 | ||
|
|
603b7a7298 | ||
|
|
39f5c86159 | ||
|
|
f45cafb10c | ||
|
|
45fc80342e | ||
|
|
c73c6e3bfc | ||
|
|
35caea0830 | ||
|
|
8eccef7e19 | ||
|
|
6fdfd901a0 | ||
|
|
e274786126 | ||
|
|
a03690044e | ||
|
|
dce72466bc | ||
|
|
1ffb85ea13 | ||
|
|
4d5fbe424b | ||
|
|
bd2af1568c | ||
|
|
77e2426605 | ||
|
|
fde6892783 | ||
|
|
9e2135d923 | ||
|
|
596390f813 | ||
|
|
181812d321 | ||
|
|
0d4bfc5e25 | ||
|
|
33b33aff11 | ||
|
|
506e03bbf7 | ||
|
|
1f7385da36 | ||
|
|
d0036f177f | ||
|
|
07e3571a78 | ||
|
|
3662cfb03c | ||
|
|
9ba92162a4 | ||
|
|
8ff7881971 | ||
|
|
43be83330e | ||
|
|
275f72de12 | ||
|
|
e94cfea3c2 | ||
|
|
c8d7e165a7 | ||
|
|
bc608c302e | ||
|
|
b5a4ee3b12 | ||
|
|
5b8c1249e6 | ||
|
|
e4f7f2410e | ||
|
|
d8b95a2921 | ||
|
|
513e6f4e07 | ||
|
|
8146169e18 | ||
|
|
a620ef1c32 | ||
|
|
c81c9a4e7f | ||
|
|
e7f9faf70c | ||
|
|
6294db4ba7 | ||
|
|
7523bae996 | ||
|
|
68f03b7790 | ||
|
|
03f88676a9 | ||
|
|
e6661d5f20 | ||
|
|
72b5cf8199 | ||
|
|
de987026b0 | ||
|
|
b7bfe1cb7a | ||
|
|
309c0240c0 | ||
|
|
bc89a91795 | ||
|
|
7e4c82a9ab | ||
|
|
64e2f44bf5 | ||
|
|
c1dea09711 | ||
|
|
e1ef638c5f | ||
|
|
867a453500 | ||
|
|
8032b1dc83 | ||
|
|
19053a8d15 | ||
|
|
17e1d82d84 | ||
|
|
fd866288f0 | ||
|
|
e7f8e14e8f | ||
|
|
6adce272b7 | ||
|
|
dee0655c8f | ||
|
|
628c5aead2 | ||
|
|
2f7846aeb9 | ||
|
|
ae0972963c | ||
|
|
e2ad58a090 | ||
|
|
6779db113a | ||
|
|
ec50054d52 | ||
|
|
7e65313765 | ||
|
|
e6de703443 | ||
|
|
2b7cfc6ef4 | ||
|
|
46736c6137 | ||
|
|
7a33417e45 | ||
|
|
0cd617d1ea |
22
.babelrc
22
.babelrc
@@ -1,18 +1,8 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"modules": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"@babel/plugin-transform-runtime",
|
||||
{
|
||||
"regenerator": true
|
||||
}
|
||||
]
|
||||
]
|
||||
"presets": [["env", { "modules": false }]],
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": [["env", { "targets": { "node": "current" } }]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
.cache
|
||||
.idea
|
||||
dist
|
||||
.git
|
||||
static
|
||||
integration
|
||||
demo.gif
|
||||
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: amir20
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS, Windows or Mac Os]
|
||||
- Docker version [e.g. `docker version`'s output]
|
||||
- Browser & version [e.g. chrome, safari]
|
||||
- Version [e.g. 1.26.1. Can be found at `<Dozzle-host>/version`]
|
||||
|
||||
**If applicable include logs with `--level debug` and browser logs**
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: amir20
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
0
demo.gif → .github/demo.gif
vendored
0
demo.gif → .github/demo.gif
vendored
|
Before Width: | Height: | Size: 24 MiB After Width: | Height: | Size: 24 MiB |
6
.github/golang/Dockerfile
vendored
6
.github/golang/Dockerfile
vendored
@@ -1,6 +0,0 @@
|
||||
FROM golang:1.12
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD [""]
|
||||
4
.github/golang/entrypoint.sh
vendored
4
.github/golang/entrypoint.sh
vendored
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
go test -cover ./...
|
||||
9
.github/goreleaser/Dockerfile
vendored
9
.github/goreleaser/Dockerfile
vendored
@@ -1,9 +0,0 @@
|
||||
FROM goreleaser/goreleaser:v0.106
|
||||
|
||||
RUN go get -u github.com/gobuffalo/packr/packr
|
||||
RUN apk --no-cache add nodejs-current nodejs-npm && npm i -g npm
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD [""]
|
||||
18
.github/goreleaser/entrypoint.sh
vendored
18
.github/goreleaser/entrypoint.sh
vendored
@@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -n "$DOCKER_USERNAME" ] && [ -n "$DOCKER_PASSWORD" ]; then
|
||||
echo "Login to the docker..."
|
||||
docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD $DOCKER_REGISTRY
|
||||
fi
|
||||
|
||||
# Workaround for github actions when access to different repositories is needed.
|
||||
# Github actions provides a GITHUB_TOKEN secret that can only access the current
|
||||
# repository and you cannot configure it's value.
|
||||
# Access to different repositories is needed by brew for example.
|
||||
|
||||
if [ -n "$GORELEASER_GITHUB_TOKEN" ] ; then
|
||||
export GITHUB_TOKEN=$GORELEASER_GITHUB_TOKEN
|
||||
fi
|
||||
|
||||
npm ci
|
||||
goreleaser $@
|
||||
23
.github/main.workflow
vendored
23
.github/main.workflow
vendored
@@ -1,23 +0,0 @@
|
||||
workflow "Release" {
|
||||
on = "push"
|
||||
resolves = [
|
||||
"release",
|
||||
]
|
||||
}
|
||||
|
||||
action "test" {
|
||||
uses = "./.github/golang/"
|
||||
}
|
||||
|
||||
action "is-tag" {
|
||||
uses = "actions/bin/filter@master"
|
||||
needs = ["test"]
|
||||
args = "tag"
|
||||
}
|
||||
|
||||
action "release" {
|
||||
uses = "./.github/goreleaser/"
|
||||
needs = ["is-tag"]
|
||||
args = "release"
|
||||
secrets = ["GITHUB_TOKEN", "DOCKER_USERNAME", "DOCKER_PASSWORD"]
|
||||
}
|
||||
17
.github/stale.yml
vendored
Normal file
17
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 20
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 3
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
60
.github/workflows/deploy.yml
vendored
Normal file
60
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
name: Test and Release
|
||||
jobs:
|
||||
npm-test:
|
||||
name: JavaScript Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
- name: Install depdencies
|
||||
run: yarn
|
||||
- name: Run Tests
|
||||
run: yarn test
|
||||
go-test:
|
||||
name: Go Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14.x
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Run Go Tests with Coverage
|
||||
run: go test -cover ./...
|
||||
int-test:
|
||||
needs: [go-test, npm-test]
|
||||
name: Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Build images
|
||||
run: docker-compose -f integration/docker-compose.test.yml build
|
||||
- name: Run tests
|
||||
run: docker-compose -f integration/docker-compose.test.yml run integration
|
||||
buildx:
|
||||
needs: [int-test]
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
with:
|
||||
buildx-version: latest
|
||||
qemu-version: latest
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
- name: Docker Login
|
||||
run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Run Buildx
|
||||
run: make publish
|
||||
37
.github/workflows/test.yml
vendored
Normal file
37
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
on: push
|
||||
name: Test
|
||||
jobs:
|
||||
npm-test:
|
||||
name: JavaScript Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
- name: Install depdencies
|
||||
run: yarn
|
||||
- name: Run Tests
|
||||
run: yarn test
|
||||
go-test:
|
||||
name: Go Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14.x
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Run Go Tests with Coverage
|
||||
run: go test -cover ./...
|
||||
int-test:
|
||||
name: Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Build images
|
||||
run: docker-compose -f integration/docker-compose.test.yml build
|
||||
- name: Run tests
|
||||
run: docker-compose -f integration/docker-compose.test.yml run integration
|
||||
@@ -1,38 +0,0 @@
|
||||
before:
|
||||
hooks:
|
||||
- npm run clean
|
||||
- npm run build
|
||||
- packr
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
archive:
|
||||
replacements:
|
||||
amd64: 64-bit
|
||||
386: 32-bit
|
||||
arm64: ARM_64-bit
|
||||
arm: ARM_32-bit
|
||||
linux: Linux
|
||||
darwin: Darwin
|
||||
checksum:
|
||||
name_template: "checksums.txt"
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
dockers:
|
||||
- image_templates:
|
||||
- "amir20/dozzle:{{ .Tag }}"
|
||||
- "amir20/dozzle:v{{ .Major }}.{{ .Minor }}"
|
||||
- amir20/dozzle:latest
|
||||
3
.prettierrc.js
Normal file
3
.prettierrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
printWidth: 120,
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"printWidth": 120
|
||||
}
|
||||
2
.reflex
2
.reflex
@@ -1 +1 @@
|
||||
-r '\.go$' -R '^node_modules/' -R '^static/' -R '^.cache/' -G '*_test.go' -s -- go run main.go --level debug
|
||||
-r '\.go$' -R '^node_modules/' -R '^static/' -R '^.cache/' -G '*_test.go' -s -- go run main.go routes.go --level debug
|
||||
|
||||
59
Dockerfile
59
Dockerfile
@@ -1,10 +1,57 @@
|
||||
FROM alpine:latest as certs
|
||||
RUN apk --update add ca-certificates
|
||||
# Build assets
|
||||
FROM node:current-alpine as node
|
||||
|
||||
RUN apk add --no-cache git openssh python make g++ util-linux
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Install dependencies
|
||||
COPY package*.json yarn.lock ./
|
||||
RUN yarn install --network-timeout 1000000
|
||||
|
||||
# Copy config files
|
||||
COPY .* webpack*.js ./
|
||||
|
||||
# Copy assets to build
|
||||
COPY assets ./assets
|
||||
|
||||
# Do the build
|
||||
RUN yarn build
|
||||
|
||||
FROM golang:1.14-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git ca-certificates
|
||||
RUN mkdir /dozzle
|
||||
|
||||
WORKDIR /dozzle
|
||||
|
||||
# Needed for assets
|
||||
RUN go get -u github.com/gobuffalo/packr/packr
|
||||
|
||||
# Copy go mod files
|
||||
COPY go.* ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy assets built with node
|
||||
COPY --from=node /build/static ./static
|
||||
|
||||
# Copy all other files
|
||||
COPY . .
|
||||
|
||||
# Compile static files
|
||||
RUN packr -z
|
||||
|
||||
# Args
|
||||
ARG TAG=dev
|
||||
|
||||
# Build binary
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=$TAG" -o dozzle
|
||||
|
||||
FROM scratch
|
||||
|
||||
ENV PATH=/bin
|
||||
ENV DOCKER_API_VERSION 1.38
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY dozzle /
|
||||
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=builder /dozzle/dozzle /dozzle
|
||||
|
||||
ENTRYPOINT ["/dozzle"]
|
||||
EXPOSE 8080
|
||||
6
Makefile
Normal file
6
Makefile
Normal file
@@ -0,0 +1,6 @@
|
||||
TAG := $(shell git describe --tags)
|
||||
PLATFROMS := linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
|
||||
.PHONY: publish
|
||||
publish:
|
||||
docker buildx build --build-arg TAG=$(TAG) --platform $(PLATFROMS) -t amir20/dozzle:latest -t amir20/dozzle:$(TAG) --push .
|
||||
110
README.md
110
README.md
@@ -1,18 +1,18 @@
|
||||
[](https://goreportcard.com/report/github.com/amir20/dozzle)
|
||||
[](https://travis-ci.org/amir20/dozzle)
|
||||
[](https://hub.docker.com/r/amir20/dozzle/)
|
||||
[](https://hub.docker.com/r/amir20/dozzle/)
|
||||
[](https://hub.docker.com/r/amir20/dozzle/)
|
||||

|
||||
|
||||
# dozzle
|
||||
# Dozzle - [dozzle.dev](https://dozzle.dev/)
|
||||
|
||||
Dozzle is a log viewer for Docker. It's free. It's small. And it's right in your browser. Oh, did I mention it is also real-time?
|
||||
Dozzle is a simple, lightweight application that provides you with a web based interface to monitor your Docker container logs live. It doesn’t store log information, it is for live monitoring of your container logs only.
|
||||
|
||||
While dozzle should work for most, it is not meant to be a full logging solution. For enterprise use, I recommend you look at [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana).
|
||||
While dozzle should work for most, it is not meant to be a full logging solution. For enterprise applications, products like [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana) are more suited.
|
||||
|
||||
But if you don't want to pay for those services, then you are in luck! Dozzle will be able to capture all logs from your containers and send them in real-time to your browser. Installation is also very easy.
|
||||
Dozzle doesn't cost any money. Dozzle aims to stay simple, small and free.
|
||||
|
||||

|
||||

|
||||
|
||||
## Getting dozzle
|
||||
|
||||
@@ -22,12 +22,33 @@ Dozzle is a very small Docker container (4 MB compressed). Pull the latest relea
|
||||
|
||||
## Using dozzle
|
||||
|
||||
The simplest way to use dozzle is to run the docker container. Also, mount the Docker Unix socket with `-volume` to `/var/run/docker.sock`:
|
||||
The simplest way to use dozzle is to run the docker container. Also, mount the Docker Unix socket with `--volume` to `/var/run/docker.sock`:
|
||||
|
||||
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:8080 amir20/dozzle:latest
|
||||
|
||||
dozzle will be available at [http://localhost:8888/](http://localhost:8888/). You can change `-p 8888:8080` to any port. For example, if you want to view dozzle over port 4040 then you would do `-p 4040:8080`.
|
||||
|
||||
### With Docker swarm
|
||||
|
||||
docker service create \
|
||||
--name=dozzle \
|
||||
--publish=8888:8080 \
|
||||
--constraint=node.role==manager \
|
||||
--mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
|
||||
amir20/dozzle:latest
|
||||
|
||||
### With Docker compose
|
||||
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
container_name: dozzle
|
||||
image: amir20/dozzle:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 9999:8080
|
||||
|
||||
#### Security
|
||||
|
||||
dozzle doesn't support authentication out of the box. You can control the device dozzle binds to by passing `--addr` parameter. For example,
|
||||
@@ -36,6 +57,12 @@ dozzle doesn't support authentication out of the box. You can control the device
|
||||
|
||||
will bind to `localhost` on port `1224`. You can then use a reverse proxy to control who can see dozzle.
|
||||
|
||||
If you wish to restrict the containers shown you can pass the `--filter` parameter. For example,
|
||||
|
||||
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:1224 amir20/dozzle:latest --filter name=foo
|
||||
|
||||
this would then only allow you to view containers with a name starting with "foo". You can use other filters like `status` as well, please check the official docker [command line docs](https://docs.docker.com/engine/reference/commandline/ps/#filtering) for available filters.
|
||||
|
||||
#### Changing base URL
|
||||
|
||||
dozzle by default mounts to "/". If you want to control the base path you can use the `--base` option. For example, if you want to mount at "/foobar",
|
||||
@@ -45,19 +72,72 @@ then you can override by using `--base /foobar`. See env variables below for usi
|
||||
|
||||
dozzle will be available at [http://localhost:8080/foobar/](http://localhost:8080/foobar/).
|
||||
|
||||
|
||||
#### Environment variables and configuration
|
||||
|
||||
Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can use the CLI flags or enviroment variables. The table below outlines all supported options and their respective env vars.
|
||||
|
||||
| Flag | Env Variable | Default |
|
||||
| --- | --- | --- |
|
||||
| `--addr` | `DOZZLE_ADDR` | `:8080` |
|
||||
| `--base` | `DOZZLE_BASE` | `/` |
|
||||
| `--level` | `DOZZLE_LEVEL` | `info` |
|
||||
| n/a | `DOCKER_API_VERSION` | `1.38` |
|
||||
| `--tailSize` | `DOZZLE_TAILSIZE` | `300` |
|
||||
| Flag | Env Variable | Default |
|
||||
| ------------ | -------------------- | ------- |
|
||||
| `--addr` | `DOZZLE_ADDR` | `:8080` |
|
||||
| `--base` | `DOZZLE_BASE` | `/` |
|
||||
| `--level` | `DOZZLE_LEVEL` | `info` |
|
||||
| n/a | `DOCKER_API_VERSION` | not set |
|
||||
| `--tailSize` | `DOZZLE_TAILSIZE` | `300` |
|
||||
| `--filter` | `DOZZLE_FILTER` | `""` |
|
||||
|
||||
## Troubleshooting and FAQs
|
||||
|
||||
<details>
|
||||
<summary>I installed Dozzle, but logs are slow or they never load. Help!</summary>
|
||||
|
||||
Dozzle uses Server Sent Events (SSE) which connects to a sever using a HTTP stream without closing the connection. If any proxy tries to buffer this connection, then Dozzle never receives the data and hangs forever waiting for the reverse proxy to flush the buffer. Since version `1.23.0`, Dozzle send the `X-Accel-Buffering: no` header which should stop reverse proxies buffering. However, some proxies may ignore this header. In those case, you need to explicitly disable any buffering.
|
||||
|
||||
Below is an example with nginx and using `proxy_pass` to disable buffering.
|
||||
|
||||
```
|
||||
server {
|
||||
...
|
||||
|
||||
location / {
|
||||
proxy_pass http://<dozzle.container.ip.address>:8080;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://<dozzle.container.ip.address>:8080;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>What data does Dozzle collect?</summary>
|
||||
|
||||
Dozzle does not collect any metrics or analytics. Dozzle has a [strict](https://github.com/amir20/dozzle/blob/master/routes.go#L33-L38) Content Security Policy which only allows the following policies:
|
||||
|
||||
- Allow connect to `api.github.com` to fetch most recent version.
|
||||
- Allow fonts from `fonts.gstatic.com` and styles from `fonts.googleapis.com`
|
||||
- Only allow `<script>` and `<style>` files from `self`
|
||||
|
||||
Dozzle opens all links with `rel="noopener"`.
|
||||
</details>
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
## Building
|
||||
|
||||
To Build and test locally:
|
||||
|
||||
1. Install NodeJs.
|
||||
2. Install Go.
|
||||
3. Globally install [packr utility](https://github.com/gobuffalo/packr) with `go get -u github.com/gobuffalo/packr/packr` outside of dozzle directory.
|
||||
4. Install [reflex](https://github.com/cespare/reflex) with `get -u github.com/cespare/reflex` outside of dozzle.
|
||||
5. Install node modules with `npm install`.
|
||||
6. Do `npm start`
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* snapshot: Test_createRoutes_foobar */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline' fonts.googleapis.com; img-src 'self'; manifest-src 'self'; font-src fonts.gstatic.com; connect-src 'self' api.github.com; require-trusted-types-for 'script'
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
foo page
|
||||
@@ -8,6 +9,7 @@ foo page
|
||||
/* snapshot: Test_createRoutes_index */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline' fonts.googleapis.com; img-src 'self'; manifest-src 'self'; font-src fonts.gstatic.com; connect-src 'self' api.github.com; require-trusted-types-for 'script'
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
index page
|
||||
@@ -15,6 +17,7 @@ index page
|
||||
/* snapshot: Test_createRoutes_redirect */
|
||||
HTTP/1.1 301 Moved Permanently
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline' fonts.googleapis.com; img-src 'self'; manifest-src 'self'; font-src fonts.gstatic.com; connect-src 'self' api.github.com; require-trusted-types-for 'script'
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Location: /foobar/
|
||||
|
||||
@@ -23,11 +26,10 @@ Location: /foobar/
|
||||
/* snapshot: Test_createRoutes_version */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline' fonts.googleapis.com; img-src 'self'; manifest-src 'self'; font-src fonts.gstatic.com; connect-src 'self' api.github.com; require-trusted-types-for 'script'
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
dev
|
||||
none
|
||||
unknown
|
||||
|
||||
/* snapshot: Test_handler_listContainers_happy */
|
||||
HTTP/1.1 200 OK
|
||||
@@ -41,14 +43,16 @@ HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Type: text/event-stream
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
/* snapshot: Test_handler_streamEvents_error_request */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Type: text/event-stream
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
/* snapshot: Test_handler_streamEvents_happy */
|
||||
HTTP/1.1 200 OK
|
||||
@@ -56,16 +60,26 @@ Connection: close
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
event: containers-changed
|
||||
data: start
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_error_finding_container */
|
||||
HTTP/1.1 404 Not Found
|
||||
Connection: close
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Content-Type-Options: nosniff
|
||||
|
||||
error finding container
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_error_reading */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Type: text/event-stream
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_happy */
|
||||
HTTP/1.1 200 OK
|
||||
@@ -73,5 +87,28 @@ Connection: close
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
data: INFO Testing logs...
|
||||
data: INFO Testing logs...
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_happy_container_stopped */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
event: container-stopped
|
||||
data: end of stream
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_happy_with_id */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
data: 2020-05-13T18:55:37.772853839Z INFO Testing logs...
|
||||
id: 2020-05-13T18:55:37.772853839Z
|
||||
53
assets/App.spec.js
Normal file
53
assets/App.spec.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import EventSource from "eventsourcemock";
|
||||
import { shallowMount, RouterLinkStub, createLocalVue } from "@vue/test-utils";
|
||||
import Vuex from "vuex";
|
||||
import App from "./App";
|
||||
|
||||
jest.mock("./store/config.js", () => ({ base: "" }));
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe("<App />", () => {
|
||||
const stubs = { RouterLink: RouterLinkStub, "router-view": true, icon: true };
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
global.EventSource = EventSource;
|
||||
const state = {
|
||||
settings: { menuWidth: 15 },
|
||||
};
|
||||
|
||||
const getters = {
|
||||
visibleContainers() {
|
||||
return [
|
||||
{ id: "abc", name: "Test 1" },
|
||||
{ id: "xyz", name: "Test 2" },
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
FETCH_CONTAINERS: () => Promise.resolve(),
|
||||
};
|
||||
|
||||
store = new Vuex.Store({
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
});
|
||||
});
|
||||
|
||||
test("has right title", async () => {
|
||||
const wrapper = shallowMount(App, { stubs, store, localVue });
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.vm.title).toContain("2 containers");
|
||||
});
|
||||
|
||||
test("renders correctly", async () => {
|
||||
const wrapper = shallowMount(App, { stubs, store, localVue });
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
210
assets/App.vue
210
assets/App.vue
@@ -1,114 +1,164 @@
|
||||
<template lang="html">
|
||||
<div class="columns is-marginless">
|
||||
<aside class="column menu is-2-desktop is-3-tablet">
|
||||
<a
|
||||
role="button"
|
||||
class="navbar-burger burger is-white is-hidden-tablet is-pulled-right"
|
||||
@click="showNav = !showNav"
|
||||
:class="{ 'is-active': showNav }"
|
||||
>
|
||||
<span></span> <span></span> <span></span>
|
||||
</a>
|
||||
<h1 class="title has-text-warning is-marginless">Dozzle</h1>
|
||||
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">Containers</p>
|
||||
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
|
||||
<li v-for="item in containers">
|
||||
<router-link :to="{ name: 'container', params: { id: item.id, name: item.name } }" active-class="is-active">
|
||||
<div class="hide-overflow">{{ item.name }}</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<div class="column is-offset-2-desktop is-offset-3-tablet">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</div>
|
||||
<template>
|
||||
<main>
|
||||
<mobile-menu v-if="isMobile"></mobile-menu>
|
||||
|
||||
<splitpanes @resized="onResized($event)">
|
||||
<pane min-size="10" :size="settings.menuWidth" v-if="!isMobile && !collapseNav">
|
||||
<side-menu></side-menu>
|
||||
</pane>
|
||||
<pane min-size="10">
|
||||
<splitpanes>
|
||||
<pane class="has-min-height">
|
||||
<search></search>
|
||||
<router-view></router-view>
|
||||
</pane>
|
||||
<pane v-for="other in activeContainers" :key="other.id" v-if="!isMobile">
|
||||
<scrollable-view>
|
||||
<template v-slot:header>
|
||||
<container-title :value="other.name" closable @close="removeActiveContainer(other)"></container-title>
|
||||
</template>
|
||||
<log-viewer-with-source :id="other.id"></log-viewer-with-source>
|
||||
</scrollable-view>
|
||||
</pane>
|
||||
</splitpanes>
|
||||
</pane>
|
||||
</splitpanes>
|
||||
<button
|
||||
@click="collapseNav = !collapseNav"
|
||||
class="button is-small is-rounded is-settings-control"
|
||||
:class="{ collapsed: collapseNav }"
|
||||
id="hide-nav"
|
||||
v-if="!isMobile"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :name="collapseNav ? 'chevron-right' : 'chevron-left'"></icon>
|
||||
</span>
|
||||
</button>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
let es;
|
||||
import { mapActions, mapGetters, mapState } from "vuex";
|
||||
import { Splitpanes, Pane } from "splitpanes";
|
||||
|
||||
import LogViewerWithSource from "./components/LogViewerWithSource";
|
||||
import ScrollableView from "./components/ScrollableView";
|
||||
import SideMenu from "./components/SideMenu";
|
||||
import MobileMenu from "./components/MobileMenu";
|
||||
import Search from "./components/Search";
|
||||
import ContainerTitle from "./components/ContainerTitle";
|
||||
import Icon from "./components/Icon";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
Icon,
|
||||
LogViewerWithSource,
|
||||
SideMenu,
|
||||
MobileMenu,
|
||||
ScrollableView,
|
||||
Splitpanes,
|
||||
Pane,
|
||||
Search,
|
||||
ContainerTitle,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: "Dozzle",
|
||||
containers: [],
|
||||
showNav: false
|
||||
title: "",
|
||||
collapseNav: false,
|
||||
};
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.title
|
||||
title: this.title,
|
||||
titleTemplate: "%s - Dozzle",
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
await this.fetchContainerList();
|
||||
this.title = `${this.containers.length} containers - Dozzle`;
|
||||
es = new EventSource(`${BASE_PATH}/api/events/stream`);
|
||||
es.addEventListener("containers-changed", e => setTimeout(this.fetchContainerList, 1000), false);
|
||||
this.title = `${this.visibleContainers.length} containers`;
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (es) {
|
||||
es.close();
|
||||
es = null;
|
||||
mounted() {
|
||||
if (this.hasSmallerScrollbars) {
|
||||
document.documentElement.classList.add("has-custom-scrollbars");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchContainerList() {
|
||||
this.containers = await (await fetch(`${BASE_PATH}/api/containers.json`)).json();
|
||||
if (this.hasLightTheme) {
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
}
|
||||
this.menuWidth = this.settings.menuWidth;
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
this.showNav = false;
|
||||
}
|
||||
}
|
||||
hasSmallerScrollbars(newValue, oldValue) {
|
||||
if (newValue) {
|
||||
document.documentElement.classList.add("has-custom-scrollbars");
|
||||
} else {
|
||||
document.documentElement.classList.remove("has-custom-scrollbars");
|
||||
}
|
||||
},
|
||||
hasLightTheme(newValue, oldValue) {
|
||||
if (newValue) {
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
} else {
|
||||
document.documentElement.removeAttribute("data-theme");
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(["activeContainers", "isMobile", "settings"]),
|
||||
...mapGetters(["visibleContainers"]),
|
||||
hasSmallerScrollbars() {
|
||||
return this.settings.smallerScrollbars;
|
||||
},
|
||||
hasLightTheme() {
|
||||
return this.settings.lightTheme;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
fetchContainerList: "FETCH_CONTAINERS",
|
||||
removeActiveContainer: "REMOVE_ACTIVE_CONTAINER",
|
||||
updateSetting: "UPDATE_SETTING",
|
||||
}),
|
||||
onResized(e) {
|
||||
if (e.length == 2) {
|
||||
const menuWidth = e[0].size;
|
||||
this.updateSetting({ menuWidth });
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.is-hidden-mobile.is-active {
|
||||
display: block !important;
|
||||
::v-deep .splitpanes--vertical > .splitpanes__splitter {
|
||||
min-width: 3px;
|
||||
background: var(--border-color);
|
||||
&:hover {
|
||||
background: var(--border-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-burger {
|
||||
height: 2.35rem;
|
||||
.button.has-no-border {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
aside {
|
||||
.has-min-height {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#hide-nav {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
padding: 1em;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
&.collapsed {
|
||||
left: -40px;
|
||||
width: 60px;
|
||||
padding-left: 40px;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
& {
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
&:hover {
|
||||
left: -25px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
& {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #222;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hide-overflow {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.burger.is-white {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
54
assets/__snapshots__/App.spec.js.snap
Normal file
54
assets/__snapshots__/App.spec.js.snap
Normal file
@@ -0,0 +1,54 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<App /> renders correctly 1`] = `
|
||||
<main>
|
||||
<!---->
|
||||
|
||||
<splitpanes-stub
|
||||
dblclicksplitter="true"
|
||||
pushotherpanes="true"
|
||||
>
|
||||
<pane-stub
|
||||
maxsize="100"
|
||||
minsize="10"
|
||||
size="15"
|
||||
>
|
||||
<side-menu-stub />
|
||||
</pane-stub>
|
||||
|
||||
<pane-stub
|
||||
maxsize="100"
|
||||
minsize="10"
|
||||
>
|
||||
<splitpanes-stub
|
||||
dblclicksplitter="true"
|
||||
pushotherpanes="true"
|
||||
>
|
||||
<pane-stub
|
||||
class="has-min-height"
|
||||
maxsize="100"
|
||||
minsize="0"
|
||||
>
|
||||
<search-stub />
|
||||
|
||||
<router-view-stub />
|
||||
</pane-stub>
|
||||
|
||||
</splitpanes-stub>
|
||||
</pane-stub>
|
||||
</splitpanes-stub>
|
||||
|
||||
<button
|
||||
class="button is-small is-rounded is-settings-control"
|
||||
id="hide-nav"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
>
|
||||
<icon-stub
|
||||
name="chevron-left"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</main>
|
||||
`;
|
||||
30
assets/components/ContainerTitle.vue
Normal file
30
assets/components/ContainerTitle.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="name columns is-marginless">
|
||||
<span class="column">{{ value }}</span>
|
||||
<span class="column is-narrow" v-if="closable">
|
||||
<button class="delete is-medium" @click="$emit('close')"></button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: String,
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
name: "ContainerTitle",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.name {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
32
assets/components/Icon.vue
Normal file
32
assets/components/Icon.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template functional>
|
||||
<svg class="icomoon" :class="['icon-' + props.name]">
|
||||
<use :href="'#icon-' + props.name"></use>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
name: "Icon",
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.icomoon {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
stroke-width: 0;
|
||||
stroke: currentColor;
|
||||
fill: currentColor;
|
||||
|
||||
.icon:not(.keep-size) & {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
39
assets/components/InfiniteLoader.vue
Normal file
39
assets/components/InfiniteLoader.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div ref="observer" class="control" :class="{ 'is-loading': isLoading }"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "InfiniteLoader",
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
onLoadMore: Function,
|
||||
enabled: Boolean,
|
||||
},
|
||||
mounted() {
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
async (entries) => {
|
||||
if (entries[0].intersectionRatio <= 0) return;
|
||||
if (this.onLoadMore && this.enabled) {
|
||||
const scrollingParent = this.$el.closest("[data-scrolling]") || document.documentElement;
|
||||
const previousHeight = scrollingParent.scrollHeight;
|
||||
this.isLoading = true;
|
||||
await this.onLoadMore();
|
||||
this.isLoading = false;
|
||||
this.$nextTick(() => (scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight));
|
||||
}
|
||||
},
|
||||
{ threshholds: 1 }
|
||||
);
|
||||
|
||||
intersectionObserver.observe(this.$refs.observer);
|
||||
|
||||
this.$once("hook:beforeDestroy", () => intersectionObserver.disconnect());
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss"></style>
|
||||
200
assets/components/LogEventSource.spec.js
Normal file
200
assets/components/LogEventSource.spec.js
Normal file
@@ -0,0 +1,200 @@
|
||||
import debounce from "lodash.debounce";
|
||||
import EventSource from "eventsourcemock";
|
||||
import { sources } from "eventsourcemock";
|
||||
import { shallowMount, mount, createLocalVue } from "@vue/test-utils";
|
||||
import Vuex from "vuex";
|
||||
import LogEventSource from "./LogEventSource.vue";
|
||||
import LogViewer from "./LogViewer.vue";
|
||||
|
||||
jest.mock("lodash.debounce", () =>
|
||||
jest.fn((fn) => {
|
||||
return fn;
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock("../store/config.js", () => ({ base: "" }));
|
||||
|
||||
describe("<LogEventSource />", () => {
|
||||
beforeEach(() => {
|
||||
global.EventSource = EventSource;
|
||||
window.scrollTo = jest.fn();
|
||||
const observe = jest.fn();
|
||||
const disconnect = jest.fn();
|
||||
global.IntersectionObserver = jest.fn(() => ({
|
||||
observe,
|
||||
disconnect,
|
||||
}));
|
||||
debounce.mockClear();
|
||||
});
|
||||
|
||||
function createLogEventSource(searchFilter = null) {
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
localVue.component("log-viewer", LogViewer);
|
||||
|
||||
const state = { searchFilter, settings: { size: "medium", showTimestamp: true } };
|
||||
|
||||
const store = new Vuex.Store({
|
||||
state,
|
||||
});
|
||||
|
||||
return mount(LogEventSource, {
|
||||
localVue,
|
||||
store,
|
||||
scopedSlots: {
|
||||
default: `
|
||||
<log-viewer :messages="props.messages"></log-viewer>
|
||||
`,
|
||||
},
|
||||
propsData: { id: "abc" },
|
||||
});
|
||||
}
|
||||
|
||||
test("renders correctly", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should connect to EventSource", async () => {
|
||||
shallowMount(LogEventSource);
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
expect(sources["/api/logs/stream?id=abc"].readyState).toBe(1);
|
||||
});
|
||||
|
||||
test("should close EventSource", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
wrapper.destroy();
|
||||
expect(sources["/api/logs/stream?id=abc"].readyState).toBe(2);
|
||||
});
|
||||
|
||||
test("should parse messages", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({ data: `2019-06-12T10:55:42.459034602Z "This is a message."` });
|
||||
|
||||
const [message, _] = wrapper.vm.messages;
|
||||
const { key, ...messageWithoutKey } = message;
|
||||
|
||||
expect(key).toBe("2019-06-12T10:55:42.459034602Z");
|
||||
expect(messageWithoutKey).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"date": 2019-06-12T10:55:42.459Z,
|
||||
"message": "\\"This is a message.\\"",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test("should parse messages with loki's timestamp format", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({ data: `2020-04-27T12:35:43.272974324+02:00 xxxxx` });
|
||||
|
||||
const [message, _] = wrapper.vm.messages;
|
||||
const { key, ...messageWithoutKey } = message;
|
||||
|
||||
expect(key).toBe("2020-04-27T12:35:43.272974324+02:00");
|
||||
expect(messageWithoutKey).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"date": 2020-04-27T10:35:43.272Z,
|
||||
"message": "xxxxx",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test("should pass messages to slot", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({ data: `2019-06-12T10:55:42.459034602Z "This is a message."` });
|
||||
const [message, _] = wrapper.findComponent(LogViewer).vm.messages;
|
||||
|
||||
const { key, ...messageWithoutKey } = message;
|
||||
|
||||
expect(key).toBe("2019-06-12T10:55:42.459034602Z");
|
||||
|
||||
expect(messageWithoutKey).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"date": 2019-06-12T10:55:42.459Z,
|
||||
"message": "\\"This is a message.\\"",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
describe("render html correctly", () => {
|
||||
const RealDate = Date;
|
||||
beforeAll(() => {
|
||||
global.Date = class extends RealDate {
|
||||
constructor(arg) {
|
||||
if (arg) {
|
||||
return new RealDate(arg);
|
||||
} else {
|
||||
return new RealDate(1560336936000);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
afterAll(() => (global.Date = RealDate));
|
||||
|
||||
test("should render messages", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({ data: `2019-06-12T10:55:42.459034602Z "This is a message."` });
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
|
||||
<ul class="events medium">
|
||||
<li class=""><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55 AM</time></span> <span class="text">"This is a message."</span></li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
test("should render messages with color", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({
|
||||
data: `2019-06-12T10:55:42.459034602Z \x1b[30mblack\x1b[37mwhite`,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
|
||||
<ul class="events medium">
|
||||
<li class=""><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55 AM</time></span> <span class="text"><span style="color:#000">black<span style="color:#AAA">white</span></span></span></li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
test("should render messages with html entities", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({
|
||||
data: `2019-06-12T10:55:42.459034602Z <test>foo bar</test>`,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
|
||||
<ul class="events medium">
|
||||
<li class=""><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55 AM</time></span> <span class="text"><test>foo bar</test></span></li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
test("should render messages with filter", async () => {
|
||||
const wrapper = createLogEventSource("test");
|
||||
sources["/api/logs/stream?id=abc"].emitOpen();
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({
|
||||
data: `2019-06-11T10:55:42.459034602Z Foo bar`,
|
||||
});
|
||||
sources["/api/logs/stream?id=abc"].emitMessage({
|
||||
data: `2019-06-12T10:55:42.459034602Z This is a test <hi></hi>`,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
|
||||
<ul class="events medium">
|
||||
<li class=""><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55 AM</time></span> <span class="text">This is a <mark>test</mark> <hi></hi></span></li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
94
assets/components/LogEventSource.vue
Normal file
94
assets/components/LogEventSource.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div>
|
||||
<infinite-loader :onLoadMore="loadOlderLogs" :enabled="messages.length > 100"></infinite-loader>
|
||||
<slot :messages="messages"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import debounce from "lodash.debounce";
|
||||
import InfiniteLoader from "./InfiniteLoader";
|
||||
import config from "../store/config";
|
||||
|
||||
export default {
|
||||
props: ["id"],
|
||||
name: "LogEventSource",
|
||||
components: {
|
||||
InfiniteLoader,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
messages: [],
|
||||
buffer: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.es = null;
|
||||
this.loadLogs(this.id);
|
||||
},
|
||||
methods: {
|
||||
loadLogs(id) {
|
||||
if (this.es) {
|
||||
this.es.close();
|
||||
this.messages = [];
|
||||
this.buffer = [];
|
||||
this.es = null;
|
||||
}
|
||||
this.es = new EventSource(`${config.base}/api/logs/stream?id=${this.id}`);
|
||||
|
||||
this.es.addEventListener("container-stopped", (e) => {
|
||||
this.es.close();
|
||||
this.buffer.push({ event: "container-stopped", message: "Container stopped", date: new Date() });
|
||||
flushNow();
|
||||
});
|
||||
this.es.addEventListener("error", (e) => console.log("EventSource failed: " + JSON.stringify(e)));
|
||||
|
||||
const flushBuffer = debounce(() => flushNow(), 250, { maxWait: 1000 });
|
||||
const flushNow = () => {
|
||||
this.messages.push(...this.buffer);
|
||||
this.buffer = [];
|
||||
};
|
||||
this.es.onmessage = (e) => {
|
||||
this.buffer.push(this.parseMessage(e.data));
|
||||
flushBuffer();
|
||||
};
|
||||
this.$once("hook:beforeDestroy", () => this.es.close());
|
||||
},
|
||||
async loadOlderLogs() {
|
||||
if (this.messages.length < 300) return;
|
||||
|
||||
const to = this.messages[0].date;
|
||||
const last = this.messages[299].date;
|
||||
const delta = to - last;
|
||||
const from = new Date(to.getTime() + delta);
|
||||
const logs = await (
|
||||
await fetch(`/api/logs?id=${this.id}&from=${from.toISOString()}&to=${to.toISOString()}`)
|
||||
).text();
|
||||
if (logs) {
|
||||
const newMessages = logs
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => this.parseMessage(line));
|
||||
this.messages.unshift(...newMessages);
|
||||
}
|
||||
},
|
||||
parseMessage(data) {
|
||||
let i = data.indexOf(" ");
|
||||
if (i == -1) {
|
||||
i = data.length;
|
||||
}
|
||||
const key = data.substring(0, i);
|
||||
const date = new Date(key);
|
||||
const message = data.substring(i).trim();
|
||||
return { key, date, message };
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
id(newValue, oldValue) {
|
||||
if (oldValue !== newValue) {
|
||||
this.loadLogs(newValue);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
126
assets/components/LogViewer.vue
Normal file
126
assets/components/LogViewer.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<ul class="events" :class="settings.size">
|
||||
<li v-for="item in filtered" :key="item.key" :class="{ event: !!item.event }">
|
||||
<span class="date" v-if="settings.showTimestamp"><relative-time :date="item.date"></relative-time></span>
|
||||
<span class="text" v-html="colorize(item.message)"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<script>
|
||||
import { mapActions, mapGetters, mapState } from "vuex";
|
||||
import AnsiConvertor from "ansi-to-html";
|
||||
import DOMPurify from "dompurify";
|
||||
import RelativeTime from "./RelativeTime";
|
||||
|
||||
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
|
||||
|
||||
if (window.trustedTypes && trustedTypes.createPolicy) {
|
||||
trustedTypes.createPolicy("default", {
|
||||
createHTML: (string, sink) => DOMPurify.sanitize(string, { RETURN_TRUSTED_TYPE: true }),
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
props: ["messages"],
|
||||
name: "LogViewer",
|
||||
components: { RelativeTime },
|
||||
data() {
|
||||
return {
|
||||
showSearch: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
colorize: function (value) {
|
||||
return ansiConvertor.toHtml(value).replace("<mark>", "<mark>").replace("</mark>", "</mark>");
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(["searchFilter", "settings"]),
|
||||
filtered() {
|
||||
const { searchFilter, messages } = this;
|
||||
if (searchFilter) {
|
||||
const isSmartCase = searchFilter === searchFilter.toLowerCase();
|
||||
try {
|
||||
const regex = isSmartCase ? new RegExp(searchFilter, "i") : new RegExp(searchFilter);
|
||||
return messages
|
||||
.filter((d) => d.message.match(regex))
|
||||
.map((d) => ({
|
||||
...d,
|
||||
message: d.message.replace(regex, "<mark>$&</mark>"),
|
||||
}));
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
console.info(`Ignoring SytaxError from search.`, e);
|
||||
return messages;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.events {
|
||||
padding: 10px;
|
||||
font-family: "Roboto Mono", monaco, monospace;
|
||||
|
||||
& > li {
|
||||
word-wrap: break-word;
|
||||
line-height: 130%;
|
||||
&:last-child {
|
||||
scroll-snap-align: end;
|
||||
scroll-margin-block-end: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
font-size: 60%;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: 120%;
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
background-color: #262626;
|
||||
color: #258ccd;
|
||||
|
||||
[data-theme="light"] & {
|
||||
background-color: #f0f0f0;
|
||||
color: #009900;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
li.event {
|
||||
color: #f14668;
|
||||
}
|
||||
|
||||
::v-deep mark {
|
||||
border-radius: 2px;
|
||||
background-color: var(--secondary-color);
|
||||
animation: pops 200ms ease-out;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes pops {
|
||||
0% {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
19
assets/components/LogViewerWithSource.vue
Normal file
19
assets/components/LogViewerWithSource.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<log-event-source :id="id" v-slot="eventSource">
|
||||
<log-viewer :messages="eventSource.messages"></log-viewer>
|
||||
</log-event-source>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LogEventSource from "./LogEventSource";
|
||||
import LogViewer from "./LogViewer";
|
||||
|
||||
export default {
|
||||
props: ["id"],
|
||||
name: "LogViewerWithSource",
|
||||
components: {
|
||||
LogEventSource,
|
||||
LogViewer,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
87
assets/components/MobileMenu.vue
Normal file
87
assets/components/MobileMenu.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<aside>
|
||||
<a
|
||||
role="button"
|
||||
class="navbar-burger burger is-hidden-tablet is-pulled-right"
|
||||
@click="showNav = !showNav"
|
||||
:class="{ 'is-active': showNav }"
|
||||
>
|
||||
<span></span> <span></span> <span></span>
|
||||
</a>
|
||||
<h1 class="title has-text-warning is-marginless">Dozzle</h1>
|
||||
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">Containers</p>
|
||||
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
|
||||
<li v-for="item in visibleContainers" :key="item.id">
|
||||
<router-link
|
||||
:to="{ name: 'container', params: { id: item.id, name: item.name } }"
|
||||
active-class="is-active"
|
||||
:title="item.name"
|
||||
>
|
||||
<div class="hide-overflow">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
export default {
|
||||
props: [],
|
||||
name: "MobileMenu",
|
||||
data() {
|
||||
return {
|
||||
showNav: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["activeContainersById", "visibleContainers"]),
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
this.showNav = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
aside {
|
||||
padding: 1em;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--scheme-main-ter);
|
||||
z-index: 2;
|
||||
max-height: 100vh;
|
||||
overflow: auto;
|
||||
|
||||
.menu-label {
|
||||
margin-top: 1em;
|
||||
}
|
||||
.hide-overflow {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.burger {
|
||||
color: var(--body-color);
|
||||
}
|
||||
|
||||
.is-hidden-mobile.is-active {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.navbar-burger {
|
||||
height: 2.35rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
assets/components/RelativeTime.vue
Normal file
34
assets/components/RelativeTime.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<time :datetime="date.toISOString()">{{ date | relativeTime }}</time>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatRelative } from "date-fns";
|
||||
import { enGB, enUS } from "date-fns/locale";
|
||||
|
||||
const use24Hr =
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
hour: "numeric",
|
||||
})
|
||||
.formatToParts(new Date(2020, 0, 1, 13))
|
||||
.find((part) => part.type === "hour").value.length === 2;
|
||||
|
||||
const locale = use24Hr ? enGB : enUS;
|
||||
|
||||
export default {
|
||||
props: {
|
||||
date: {
|
||||
required: true,
|
||||
type: Date,
|
||||
},
|
||||
},
|
||||
name: "RelativeTime",
|
||||
components: {},
|
||||
|
||||
filters: {
|
||||
relativeTime(date) {
|
||||
return formatRelative(date, new Date(), { locale });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
96
assets/components/ScrollProgress.vue
Normal file
96
assets/components/ScrollProgress.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="scroll-progress">
|
||||
<svg width="100" height="100" viewBox="0 0 100 100">
|
||||
<circle r="44" cx="50" cy="50" :style="{ '--progress': scrollProgress }" />
|
||||
</svg>
|
||||
<div class="percent columns is-vcentered is-centered has-text-weight-light">
|
||||
<span class="column is-narrow is-paddingless is-size-2">
|
||||
{{ Math.ceil(scrollProgress * 100) }}
|
||||
</span>
|
||||
<span class="column is-narrow is-paddingless">
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import throttle from "lodash.throttle";
|
||||
|
||||
export default {
|
||||
name: "ScrollProgress",
|
||||
data() {
|
||||
return {
|
||||
scrollProgress: 0,
|
||||
animation: { cancel: () => {} },
|
||||
parentElement: document,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.onScrollThrottled = throttle(this.onScroll, 150);
|
||||
},
|
||||
mounted() {
|
||||
this.attachEvents();
|
||||
this.$once("hook:beforeDestroy", this.detachEvents);
|
||||
},
|
||||
watch: {
|
||||
activeContainers() {
|
||||
this.detachEvents();
|
||||
this.attachEvents();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(["activeContainers"]),
|
||||
},
|
||||
methods: {
|
||||
attachEvents() {
|
||||
this.parentElement = this.$el.closest("[data-scrolling]") || document;
|
||||
this.parentElement.addEventListener("scroll", this.onScrollThrottled);
|
||||
},
|
||||
detachEvents() {
|
||||
this.parentElement.removeEventListener("scroll", this.onScrollThrottled);
|
||||
},
|
||||
onScroll() {
|
||||
const p = this.parentElement == document ? document.documentElement : this.parentElement;
|
||||
this.scrollProgress = p.scrollTop / (p.scrollHeight - p.clientHeight);
|
||||
this.animation.cancel();
|
||||
this.animation = this.$el.animate(
|
||||
{ opacity: [1, 0] },
|
||||
{
|
||||
duration: 500,
|
||||
delay: 2000,
|
||||
fill: "both",
|
||||
easing: "ease-out",
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.scroll-progress {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
circle {
|
||||
fill: var(--scheme-main-ter);
|
||||
fill-opacity: 0.8;
|
||||
transition: stroke-dashoffset 250ms ease-out;
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: 50% 50%;
|
||||
stroke: var(--primary-color);
|
||||
stroke-dashoffset: calc(276.32px - var(--progress) * 276.32px);
|
||||
stroke-dasharray: 276.32px 276.32px;
|
||||
stroke-width: 3;
|
||||
will-change: stroke-dashoffset;
|
||||
}
|
||||
|
||||
.percent {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
145
assets/components/ScrollableView.vue
Normal file
145
assets/components/ScrollableView.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<section :class="{ 'is-full-height-scrollable': scrollable }">
|
||||
<header v-if="$slots.header">
|
||||
<slot name="header"></slot>
|
||||
</header>
|
||||
<main ref="content" :data-scrolling="scrollable">
|
||||
<div class="scrollbar-progress is-hidden-mobile">
|
||||
<scroll-progress v-show="paused"></scroll-progress>
|
||||
</div>
|
||||
<slot></slot>
|
||||
<div ref="scrollObserver"></div>
|
||||
</main>
|
||||
|
||||
<div class="scrollbar-notification">
|
||||
<transition name="fade">
|
||||
<button class="button" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom('instant')" v-show="paused">
|
||||
<icon name="download"></icon>
|
||||
</button>
|
||||
</transition>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icon from "./Icon";
|
||||
import ScrollProgress from "./ScrollProgress";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
ScrollProgress,
|
||||
},
|
||||
name: "ScrollableView",
|
||||
data() {
|
||||
return {
|
||||
paused: false,
|
||||
hasMore: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const { content } = this.$refs;
|
||||
const mutationObserver = new MutationObserver((e) => {
|
||||
if (!this.paused) {
|
||||
this.scrollToBottom("instant");
|
||||
} else {
|
||||
this.hasMore = true;
|
||||
}
|
||||
});
|
||||
mutationObserver.observe(content, { childList: true, subtree: true });
|
||||
this.$once("hook:beforeDestroy", () => mutationObserver.disconnect());
|
||||
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
(entries) => (this.paused = entries[0].intersectionRatio == 0),
|
||||
{ threshholds: [0, 1], rootMargin: "80px 0px" }
|
||||
);
|
||||
intersectionObserver.observe(this.$refs.scrollObserver);
|
||||
this.$once("hook:beforeDestroy", () => intersectionObserver.disconnect());
|
||||
},
|
||||
|
||||
methods: {
|
||||
scrollToBottom(behavior = "instant") {
|
||||
this.$refs.scrollObserver.scrollIntoView({ behavior });
|
||||
this.hasMore = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.is-full-height-scrollable {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
scroll-snap-type: y proximity;
|
||||
}
|
||||
|
||||
.scrollbar-progress {
|
||||
text-align: right;
|
||||
margin-right: 110px;
|
||||
.scroll-progress {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollbar-notification {
|
||||
text-align: right;
|
||||
margin-right: 65px;
|
||||
button {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
background-color: var(--secondary-color);
|
||||
transition: background-color 1s ease-out;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
border: none !important;
|
||||
|
||||
&.has-more {
|
||||
background-color: var(--primary-color);
|
||||
animation-name: bounce;
|
||||
animation-duration: 1000ms;
|
||||
animation-fill-mode: both;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
20%,
|
||||
50%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.15s ease-in;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,71 +0,0 @@
|
||||
<template lang="html">
|
||||
<transition name="fade">
|
||||
<button
|
||||
class="button scroll-notification"
|
||||
:class="hasNew ? 'is-warning' : 'is-primary'"
|
||||
@click="scrollToBottom"
|
||||
v-show="visible"
|
||||
>
|
||||
<span class="icon large"> <i class="fas fa-chevron-down"></i> </span>
|
||||
</button>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["messages"],
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
hasNew: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener("scroll", this.onScroll, { passive: true });
|
||||
setTimeout(() => this.scrollToBottom(), 500);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener("scroll", this.onScroll);
|
||||
},
|
||||
methods: {
|
||||
scrollToBottom() {
|
||||
this.visible = false;
|
||||
window.scrollTo(0, document.documentElement.scrollHeight || document.body.scrollHeight);
|
||||
},
|
||||
onScroll() {
|
||||
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const scrollBottom =
|
||||
(document.documentElement.scrollHeight || document.body.scrollHeight) - document.documentElement.clientHeight;
|
||||
const diff = Math.abs(scrollTop - scrollBottom);
|
||||
this.visible = diff > 50;
|
||||
if (!this.visible) {
|
||||
this.hasNew = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
messages() {
|
||||
if (this.visible) {
|
||||
this.hasNew = true;
|
||||
} else {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.scroll-notification {
|
||||
position: fixed;
|
||||
right: 40px;
|
||||
bottom: 30px;
|
||||
}
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.15s ease-in;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
100
assets/components/Search.vue
Normal file
100
assets/components/Search.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="search columns is-gapless is-vcentered" v-show="showSearch" v-if="settings.search">
|
||||
<div class="column">
|
||||
<p class="control has-icons-left">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="Find / RegEx"
|
||||
ref="filter"
|
||||
v-model="filter"
|
||||
@keyup.esc="resetSearch()"
|
||||
/>
|
||||
<span class="icon is-left">
|
||||
<icon name="search"></icon>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-1 has-text-centered">
|
||||
<button class="delete is-medium" @click="resetSearch()"></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from "vuex";
|
||||
import hotkeys from "hotkeys-js";
|
||||
import Icon from "./Icon";
|
||||
|
||||
export default {
|
||||
props: [],
|
||||
name: "Search",
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showSearch: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
hotkeys("command+f, ctrl+f", (event, handler) => {
|
||||
this.showSearch = true;
|
||||
this.$nextTick(() => this.$refs.filter.focus() || this.$refs.filter.select());
|
||||
event.preventDefault();
|
||||
});
|
||||
hotkeys("esc", (event, handler) => {
|
||||
this.resetSearch();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
updateSearchFilter: "SET_SEARCH",
|
||||
}),
|
||||
resetSearch() {
|
||||
this.showSearch = false;
|
||||
this.filter = "";
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(["searchFilter", "settings"]),
|
||||
filter: {
|
||||
get() {
|
||||
return this.searchFilter;
|
||||
},
|
||||
set(value) {
|
||||
this.updateSearchFilter(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search {
|
||||
width: 350px;
|
||||
position: fixed;
|
||||
padding: 10px;
|
||||
background: var(--scheme-main-ter);
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-radius: 0 0 0 5px;
|
||||
z-index: 10;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
|
||||
.delete {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 10px 3px;
|
||||
}
|
||||
|
||||
.input {
|
||||
color: var(--body-color);
|
||||
&::placeholder {
|
||||
color: var(--border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
106
assets/components/SideMenu.vue
Normal file
106
assets/components/SideMenu.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<aside>
|
||||
<div class="columns is-marginless">
|
||||
<div class="column">
|
||||
<h1 class="title has-text-warning is-marginless">Dozzle</h1>
|
||||
</div>
|
||||
<div class="column is-narrow has-text-right x">
|
||||
<router-link
|
||||
:to="{ name: 'settings' }"
|
||||
active-class="is-active"
|
||||
class="button is-small is-rounded is-settings-control"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon name="cog"></icon>
|
||||
</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<p class="menu-label is-hidden-mobile">Containers</p>
|
||||
<ul class="menu-list is-hidden-mobile">
|
||||
<li v-for="item in visibleContainers" :key="item.id" :class="item.state">
|
||||
<router-link
|
||||
:to="{ name: 'container', params: { id: item.id, name: item.name } }"
|
||||
active-class="is-active"
|
||||
:title="item.name"
|
||||
>
|
||||
<div class="hide-overflow">
|
||||
<span
|
||||
@click.stop.prevent="appendActiveContainer(item)"
|
||||
class="icon is-small will-append-container"
|
||||
:class="{ 'is-active': activeContainersById[item.id] }"
|
||||
>
|
||||
<icon name="pin"></icon>
|
||||
</span>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters, mapState } from "vuex";
|
||||
|
||||
import Icon from "./Icon";
|
||||
|
||||
export default {
|
||||
props: [],
|
||||
name: "SideMenu",
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["activeContainers"]),
|
||||
...mapGetters(["activeContainersById", "visibleContainers"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
appendActiveContainer: "APPEND_ACTIVE_CONTAINER",
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
aside {
|
||||
padding: 1em;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
width: inherit;
|
||||
|
||||
.hide-overflow {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.is-hidden-mobile.is-active {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
h1.title {
|
||||
font-family: "Gafata", sans-serif;
|
||||
text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
li.exited a {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.will-append-container.icon {
|
||||
transition: transform 0.2s ease-out;
|
||||
&.is-active {
|
||||
pointer-events: none;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.router-link-exact-active & {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
13
assets/components/__snapshots__/LogEventSource.spec.js.snap
Normal file
13
assets/components/__snapshots__/LogEventSource.spec.js.snap
Normal file
@@ -0,0 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<LogEventSource /> renders correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="control"
|
||||
/>
|
||||
|
||||
<ul
|
||||
class="events medium"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
5
assets/favicon.svg
Normal file
5
assets/favicon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="128" height="128" rx="6" fill="#222222"/>
|
||||
<path d="M82.3248 94.3863H123V104.093H67.8025V95.3506L106.164 44.3736H68.3808V34.5382H121.072V42.9594L82.3248 94.3863Z" fill="#FFDD57"/>
|
||||
<path d="M8 107.107L17.5656 14L43.8372 16.7013C51.9339 17.5338 58.9091 20.0604 64.7629 24.2812C70.6166 28.5019 74.8873 34.0893 77.5749 41.0432C80.3052 48.0016 81.2514 55.7674 80.4137 64.3407L79.8027 70.2877C78.9005 79.0698 76.4053 86.5894 72.3173 92.8468C68.2719 99.1084 62.914 103.684 56.2436 106.574C49.6158 109.468 42.1213 110.529 33.7602 109.755L8 107.107ZM28.8005 25.3655L21.3043 98.3288L34.2164 99.6565C43.6767 100.629 51.3299 98.4435 57.1758 93.0993C63.0644 87.7595 66.5671 79.6542 67.684 68.7832L68.2424 63.3477C69.3286 52.7752 67.6788 44.3123 63.293 37.9592C58.9542 31.5678 52.2295 27.8607 43.1188 26.8377L28.8005 25.3655Z" fill="#FFDD57"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 949 B |
74
assets/index.ejs
Normal file
74
assets/index.ejs
Normal file
@@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Dozzle</title>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Gafata&family=Roboto:wght@300;400&family=Roboto+Mono&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script type="application/json" id="config__json">
|
||||
{
|
||||
"base": "{{ .Base }}",
|
||||
"version": "{{ .Version }}"
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="is-hidden"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<defs>
|
||||
<symbol id="icon-check" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M19.293 5.293l-10.293 10.293-4.293-4.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414l5 5c0.391 0.391 1.024 0.391 1.414 0l11-11c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0z"
|
||||
></path>
|
||||
</symbol>
|
||||
<symbol id="icon-chevron-down" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M5.293 9.707l6 6c0.391 0.391 1.024 0.391 1.414 0l6-6c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-5.293 5.293-5.293-5.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"
|
||||
></path>
|
||||
</symbol>
|
||||
<symbol id="icon-chevron-left" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M15.707 17.293l-5.293-5.293 5.293-5.293c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-6 6c-0.391 0.391-0.391 1.024 0 1.414l6 6c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414z"
|
||||
></path>
|
||||
</symbol>
|
||||
<symbol id="icon-chevron-right" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M9.707 18.707l6-6c0.391-0.391 0.391-1.024 0-1.414l-6-6c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414l5.293 5.293-5.293 5.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0z"
|
||||
></path>
|
||||
</symbol>
|
||||
<symbol id="icon-chevrons-down" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M6.293 13.707l5 5c0.391 0.391 1.024 0.391 1.414 0l5-5c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-4.293 4.293-4.293-4.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414zM6.293 6.707l5 5c0.391 0.391 1.024 0.391 1.414 0l5-5c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-4.293 4.293-4.293-4.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"
|
||||
></path>
|
||||
</symbol>
|
||||
<symbol id="icon-download" viewBox="0 0 24 24">
|
||||
<path d="M15.6 9.6v-7.2h-7.2v7.2h-6l9.6 9.6 9.6-9.6h-6zM0 21.6h24v2.4h-24v-2.4z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-pin" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M13.2 14.4h7.2v-1.2l-3.6-1.2v-9.6l3.6-1.2v-1.2h-16.8v1.2l3.6 1.2v9.6l-3.6 1.2v1.2h7.2v8.4l1.2 1.2 1.2-1.2v-8.4z"
|
||||
></path>
|
||||
</symbol>
|
||||
<symbol id="icon-search" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M15.48 17.184c-1.608 1.259-3.66 2.019-5.889 2.019-5.302 0-9.6-4.298-9.6-9.6s4.298-9.6 9.6-9.6c5.302 0 9.6 4.298 9.6 9.6 0 2.229-0.76 4.281-2.035 5.91l0.016-0.021 6.42 6.396-1.704 1.704-6.396-6.408zM9.6 16.8c3.976 0 7.2-3.224 7.2-7.2s-3.224-7.2-7.2-7.2v0c-3.976 0-7.2 3.224-7.2 7.2s3.224 7.2 7.2 7.2v0z"
|
||||
></path>
|
||||
</symbol>
|
||||
<symbol id="icon-cog" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M21.886 14.303c-1.259-2.181-0.502-4.976 1.691-6.246l-2.358-4.085c-0.674 0.395-1.457 0.622-2.293 0.622-2.52 0-4.563-2.057-4.563-4.594h-4.717c0.006 0.783-0.189 1.577-0.608 2.303-1.259 2.181-4.058 2.923-6.255 1.658l-2.358 4.085c0.679 0.386 1.267 0.951 1.685 1.675 1.257 2.178 0.504 4.967-1.681 6.24l2.358 4.085c0.671-0.391 1.451-0.615 2.283-0.615 2.512 0 4.55 2.044 4.563 4.569h4.717c-0.002-0.775 0.194-1.56 0.609-2.279 1.257-2.177 4.049-2.92 6.244-1.664l2.358-4.085c-0.675-0.386-1.258-0.949-1.674-1.669zM12 16.859c-2.684 0-4.859-2.176-4.859-4.859s2.176-4.859 4.859-4.859c2.684 0 4.859 2.176 4.859 4.859s-2.176 4.859-4.859 4.859z"
|
||||
></path>
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Dozzle</title>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto|Roboto+Mono|Gafata" rel="stylesheet" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
<link href="styles.scss" rel="stylesheet" />
|
||||
<script>
|
||||
window["BASE_PATH"] = "{{ .Base }}";
|
||||
</script>
|
||||
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="is-dark">
|
||||
<div id="app"></div>
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,34 +1,53 @@
|
||||
import Vue from "vue";
|
||||
import VueRouter from "vue-router";
|
||||
import Meta from "vue-meta";
|
||||
import Dropdown from "buefy/dist/esm/dropdown";
|
||||
import Switch from "buefy/dist/esm/switch";
|
||||
import store from "./store";
|
||||
import config from "./store/config";
|
||||
import App from "./App.vue";
|
||||
import Container from "./pages/Container.vue";
|
||||
import Settings from "./pages/Settings.vue";
|
||||
import Index from "./pages/Index.vue";
|
||||
import Show from "./pages/Show.vue";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
Vue.use(Meta);
|
||||
Vue.use(Dropdown);
|
||||
Vue.use(Switch);
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
component: Index,
|
||||
name: "default"
|
||||
name: "default",
|
||||
},
|
||||
{
|
||||
path: "/container/:id",
|
||||
component: Container,
|
||||
name: "container",
|
||||
props: true
|
||||
}
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
component: Settings,
|
||||
name: "settings",
|
||||
},
|
||||
{
|
||||
path: "/show",
|
||||
component: Show,
|
||||
name: "show",
|
||||
},
|
||||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: "history",
|
||||
base: BASE_PATH + "/",
|
||||
routes
|
||||
base: config.base + "/",
|
||||
routes,
|
||||
});
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
render: h => h(App)
|
||||
store,
|
||||
render: (h) => h(App),
|
||||
}).$mount("#app");
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "Dozzle Log Viewer",
|
||||
"short_name": "Dozzle",
|
||||
"theme_color": "#111111",
|
||||
"background_color": "#111111",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"start_url": "/"
|
||||
}
|
||||
@@ -1,183 +1,53 @@
|
||||
<template lang="html">
|
||||
<div class="is-fullheight">
|
||||
<div class="search columns is-gapless is-vcentered" v-show="showSearch">
|
||||
<div class="column">
|
||||
<p class="control has-icons-left">
|
||||
<input class="input" type="text" placeholder="Filter" ref="filter" v-model="filter" />
|
||||
<span class="icon is-small is-left"><i class="fas fa-search"></i></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-1 has-text-centered">
|
||||
<button class="delete is-medium" @click="resetSearch()"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="events">
|
||||
<li v-for="item in filtered" class="event" :key="item.key">
|
||||
<span class="date">{{ item.dateRelative }}</span> <span class="text" v-html="item.message"></span>
|
||||
</li>
|
||||
</ul>
|
||||
<scrollbar-notification :messages="messages"></scrollbar-notification>
|
||||
</div>
|
||||
<template>
|
||||
<scrollable-view :scrollable="activeContainers.length > 0">
|
||||
<template v-slot:header v-if="activeContainers.length > 0">
|
||||
<container-title :value="allContainersById[id].name"></container-title>
|
||||
</template>
|
||||
<log-viewer-with-source :id="id"></log-viewer-with-source>
|
||||
</scrollable-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatRelative } from "date-fns";
|
||||
import ScrollbarNotification from "../components/ScrollbarNotification";
|
||||
import { mapActions, mapGetters, mapState } from "vuex";
|
||||
|
||||
let es = null;
|
||||
let nextId = 0;
|
||||
const parseMessage = data => {
|
||||
const date = new Date(data.substring(0, 30));
|
||||
const dateRelative = formatRelative(date, new Date());
|
||||
const message = data.substring(30);
|
||||
const key = nextId++;
|
||||
return {
|
||||
key,
|
||||
date,
|
||||
dateRelative,
|
||||
message
|
||||
};
|
||||
};
|
||||
import LogViewerWithSource from "../components/LogViewerWithSource";
|
||||
import ScrollableView from "../components/ScrollableView";
|
||||
import ContainerTitle from "../components/ContainerTitle";
|
||||
|
||||
export default {
|
||||
props: ["id", "name"],
|
||||
name: "Container",
|
||||
components: {
|
||||
ScrollbarNotification
|
||||
LogViewerWithSource,
|
||||
ScrollableView,
|
||||
ContainerTitle,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
messages: [],
|
||||
showSearch: false,
|
||||
title: "",
|
||||
filter: ""
|
||||
title: "loading",
|
||||
};
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.title,
|
||||
titleTemplate: "%s - Dozzle"
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
created() {
|
||||
this.loadLogs(this.id);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (es) {
|
||||
es.close();
|
||||
es = null;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
id(newValue, oldValue) {
|
||||
if (oldValue !== newValue) {
|
||||
this.loadLogs(newValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadLogs(id) {
|
||||
if (es) {
|
||||
es.close();
|
||||
es = null;
|
||||
this.messages = [];
|
||||
}
|
||||
es = new EventSource(`${BASE_PATH}/api/logs/stream?id=${id}`);
|
||||
es.onmessage = e => this.messages.push(parseMessage(e.data));
|
||||
this.title = `${this.name} - Dozzle`;
|
||||
},
|
||||
onKeyDown(e) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "f") {
|
||||
this.showSearch = true;
|
||||
this.$nextTick(() => this.$refs.filter.focus());
|
||||
e.preventDefault();
|
||||
} else if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
||||
this.messages = [];
|
||||
} else if (e.key === "Escape") {
|
||||
this.resetSearch();
|
||||
}
|
||||
},
|
||||
resetSearch() {
|
||||
this.showSearch = false;
|
||||
this.filter = "";
|
||||
if (this.allContainersById[this.id]) {
|
||||
this.title = this.allContainersById[this.id].name;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filtered() {
|
||||
const { filter } = this;
|
||||
if (filter) {
|
||||
const isSmartCase = filter === filter.toLowerCase();
|
||||
const regex = isSmartCase ? new RegExp(filter, "i") : new RegExp(filter);
|
||||
return this.messages
|
||||
.filter(d => d.message.match(regex))
|
||||
.map(d => ({
|
||||
...d,
|
||||
message: d.message.replace(regex, "<mark>$&</mark>")
|
||||
}));
|
||||
}
|
||||
return this.messages;
|
||||
}
|
||||
}
|
||||
...mapState(["activeContainers"]),
|
||||
...mapGetters(["allContainersById"]),
|
||||
},
|
||||
watch: {
|
||||
id() {
|
||||
this.title = this.allContainersById[this.id].name;
|
||||
},
|
||||
allContainersById() {
|
||||
this.title = this.allContainersById[this.id].name;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.events {
|
||||
padding: 10px;
|
||||
font-family: "Roboto Mono", monaco, monospace;
|
||||
}
|
||||
|
||||
.event {
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.date {
|
||||
background-color: #262626;
|
||||
color: #258ccd;
|
||||
}
|
||||
|
||||
.is-fullheight {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.search {
|
||||
width: 350px;
|
||||
position: fixed;
|
||||
padding: 10px;
|
||||
background: rgba(50, 50, 50, 0.9);
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-radius: 0 0 0 5px;
|
||||
}
|
||||
.delete {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
/deep/ mark {
|
||||
border-radius: 2px;
|
||||
background-color: #ffdd57;
|
||||
animation: pops 0.2s ease-out;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes pops {
|
||||
0% {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
9
assets/pages/Index.spec.js
Normal file
9
assets/pages/Index.spec.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import Index from "./Index";
|
||||
|
||||
describe("<Index />", () => {
|
||||
test("renders correctly", () => {
|
||||
const wrapper = shallowMount(Index);
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
<template lang="html">
|
||||
<div class="hero is-fullheight is-dark">
|
||||
<template>
|
||||
<div class="hero is-fullheight">
|
||||
<div class="hero-body">
|
||||
<div class="container has-text-centered">
|
||||
<h1 class="title">Please choose a container from the list to view the logs</h1>
|
||||
@@ -11,12 +11,6 @@
|
||||
<script>
|
||||
export default {
|
||||
props: [],
|
||||
name: "Default"
|
||||
name: "Default",
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.hero.is-dark {
|
||||
color: #ddd;
|
||||
background-color: #111;
|
||||
}
|
||||
</style>
|
||||
|
||||
170
assets/pages/Settings.vue
Normal file
170
assets/pages/Settings.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="section">
|
||||
<div class="has-underline">
|
||||
<h2 class="title is-4">About</h2>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
You are using Dozzle <i>{{ currentVersion }}</i
|
||||
>.
|
||||
<span v-if="hasUpdate">
|
||||
New version is available! Update to
|
||||
<a :href="nextRelease.html_url" class="next-release" target="_blank" rel="noreferrer noopener">{{
|
||||
nextRelease.name
|
||||
}}</a
|
||||
>.
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section">
|
||||
<div class="has-underline">
|
||||
<h2 class="title is-4">Display</h2>
|
||||
</div>
|
||||
<div class="item">
|
||||
<b-switch v-model="search">
|
||||
Enable searching with Dozzle using <code>command+f</code> or <code>ctrl+f</code>
|
||||
</b-switch>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<b-switch v-model="smallerScrollbars">
|
||||
Use smaller scrollbars
|
||||
</b-switch>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<b-switch v-model="showTimestamp">
|
||||
Show timestamps
|
||||
</b-switch>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<b-switch v-model="showAllContainers">
|
||||
Show stopped containers
|
||||
</b-switch>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<b-switch v-model="lightTheme">
|
||||
Use light theme
|
||||
</b-switch>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<h2 class="title is-6 is-marginless">Font size</h2>
|
||||
Modify the font size when viewing logs.
|
||||
|
||||
<b-dropdown v-model="size" aria-role="list" style="margin: -8px 10px 0;">
|
||||
<button class="button is-primary" type="button" slot="trigger">
|
||||
<span class="is-capitalized">{{ size }}</span>
|
||||
<span class="icon"><icon name="chevron-down"></icon></span>
|
||||
</button>
|
||||
<b-dropdown-item
|
||||
:value="value"
|
||||
aria-role="listitem"
|
||||
v-for="value in ['small', 'medium', 'large']"
|
||||
:key="value"
|
||||
>
|
||||
<div class="media">
|
||||
<span class="icon keep-size">
|
||||
<icon name="check" v-if="value == size"></icon>
|
||||
</span>
|
||||
<div class="media-content">
|
||||
<h3 class="is-capitalized">{{ value }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gt from "semver/functions/gt";
|
||||
import valid from "semver/functions/valid";
|
||||
import { mapActions, mapState } from "vuex";
|
||||
import Icon from "../components/Icon";
|
||||
import config from "../store/config";
|
||||
|
||||
export default {
|
||||
props: [],
|
||||
name: "Settings",
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentVersion: config.version,
|
||||
nextRelease: null,
|
||||
hasUpdate: false,
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
const releases = await (await fetch("https://api.github.com/repos/amir20/dozzle/releases")).json();
|
||||
this.hasUpdate = gt(releases[0].tag_name, this.currentVersion);
|
||||
this.nextRelease = releases[0];
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: "Settings",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
updateSetting: "UPDATE_SETTING",
|
||||
}),
|
||||
},
|
||||
computed: {
|
||||
...mapState(["settings"]),
|
||||
...["search", "size", "smallerScrollbars", "showTimestamp", "showAllContainers", "lightTheme"].reduce(
|
||||
(map, name) => {
|
||||
map[name] = {
|
||||
get() {
|
||||
return this.settings[name];
|
||||
},
|
||||
set(value) {
|
||||
this.updateSetting({ [name]: value });
|
||||
},
|
||||
};
|
||||
return map;
|
||||
},
|
||||
{}
|
||||
),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
a.next-release {
|
||||
text-decoration: underline;
|
||||
color: #00d1b2;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.has-underline {
|
||||
border-bottom: 1px solid var(--title-color);
|
||||
padding: 1em 0px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
code {
|
||||
border-radius: 4px;
|
||||
background-color: #444;
|
||||
}
|
||||
</style>
|
||||
29
assets/pages/Show.vue
Normal file
29
assets/pages/Show.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template> </template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters, mapState } from "vuex";
|
||||
export default {
|
||||
props: [],
|
||||
name: "Show",
|
||||
computed: mapGetters(["visibleContainers"]),
|
||||
watch: {
|
||||
visibleContainers(newValue) {
|
||||
if (newValue) {
|
||||
if (this.$route.query.name) {
|
||||
const [container, _] = this.visibleContainers.filter((c) => c.name == this.$route.query.name);
|
||||
if (container) {
|
||||
this.$router.push({ name: "container", params: { id: container.id } });
|
||||
} else {
|
||||
console.error(`No containers found matching name=${this.$route.query.name}. Redirecting to /`);
|
||||
this.$router.push({ name: "default" });
|
||||
}
|
||||
} else {
|
||||
console.error(`Expection query parameter name to be set. Redirecting to /`);
|
||||
this.$router.push({ name: "default" });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped></style>
|
||||
21
assets/pages/__snapshots__/Index.spec.js.snap
Normal file
21
assets/pages/__snapshots__/Index.spec.js.snap
Normal file
@@ -0,0 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Index /> renders correctly 1`] = `
|
||||
<div
|
||||
class="hero is-fullheight"
|
||||
>
|
||||
<div
|
||||
class="hero-body"
|
||||
>
|
||||
<div
|
||||
class="container has-text-centered"
|
||||
>
|
||||
<h1
|
||||
class="title"
|
||||
>
|
||||
Please choose a container from the list to view the logs
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
2
assets/store/config.js
Normal file
2
assets/store/config.js
Normal file
@@ -0,0 +1,2 @@
|
||||
const config = JSON.parse(document.querySelector("script#config__json").textContent);
|
||||
export default config;
|
||||
91
assets/store/index.js
Normal file
91
assets/store/index.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
import storage from "store/dist/store.modern";
|
||||
import { DEFAULT_SETTINGS, DOZZLE_SETTINGS_KEY } from "./settings";
|
||||
import config from "./config";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const mql = window.matchMedia("(max-width: 770px)");
|
||||
|
||||
storage.set(DOZZLE_SETTINGS_KEY, { ...DEFAULT_SETTINGS, ...storage.get(DOZZLE_SETTINGS_KEY) });
|
||||
|
||||
const state = {
|
||||
containers: [],
|
||||
activeContainers: [],
|
||||
searchFilter: null,
|
||||
isMobile: mql.matches,
|
||||
settings: storage.get(DOZZLE_SETTINGS_KEY),
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
SET_CONTAINERS(state, containers) {
|
||||
state.containers = containers;
|
||||
},
|
||||
ADD_ACTIVE_CONTAINERS(state, container) {
|
||||
state.activeContainers.push(container);
|
||||
},
|
||||
REMOVE_ACTIVE_CONTAINER(state, container) {
|
||||
state.activeContainers.splice(state.activeContainers.indexOf(container), 1);
|
||||
},
|
||||
SET_SEARCH(state, filter) {
|
||||
state.searchFilter = filter;
|
||||
},
|
||||
SET_MOBILE_WIDTH(state, value) {
|
||||
state.isMobile = value;
|
||||
},
|
||||
UPDATE_SETTINGS(state, newValues) {
|
||||
state.settings = { ...state.settings, ...newValues };
|
||||
storage.set(DOZZLE_SETTINGS_KEY, state.settings);
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
APPEND_ACTIVE_CONTAINER({ commit }, container) {
|
||||
commit("ADD_ACTIVE_CONTAINERS", container);
|
||||
},
|
||||
REMOVE_ACTIVE_CONTAINER({ commit }, container) {
|
||||
commit("REMOVE_ACTIVE_CONTAINER", container);
|
||||
},
|
||||
SET_SEARCH({ commit }, filter) {
|
||||
commit("SET_SEARCH", filter);
|
||||
},
|
||||
async FETCH_CONTAINERS({ commit }) {
|
||||
const containers = await (await fetch(`${config.base}/api/containers.json`)).json();
|
||||
commit("SET_CONTAINERS", containers);
|
||||
},
|
||||
UPDATE_SETTING({ commit }, setting) {
|
||||
commit("UPDATE_SETTINGS", setting);
|
||||
},
|
||||
};
|
||||
const getters = {
|
||||
activeContainersById({ activeContainers }) {
|
||||
return activeContainers.reduce((map, obj) => {
|
||||
map[obj.id] = obj;
|
||||
return map;
|
||||
}, {});
|
||||
},
|
||||
allContainersById({ containers }) {
|
||||
return containers.reduce((map, obj) => {
|
||||
map[obj.id] = obj;
|
||||
return map;
|
||||
}, {});
|
||||
},
|
||||
visibleContainers({ containers, settings: { showAllContainers } }) {
|
||||
const filter = showAllContainers ? () => true : (c) => c.state === "running";
|
||||
return containers.filter(filter);
|
||||
},
|
||||
};
|
||||
|
||||
const es = new EventSource(`${config.base}/api/events/stream`);
|
||||
es.addEventListener("containers-changed", (e) => setTimeout(() => store.dispatch("FETCH_CONTAINERS"), 1000), false);
|
||||
mql.addListener((e) => store.commit("SET_MOBILE_WIDTH", e.matches));
|
||||
|
||||
const store = new Vuex.Store({
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
});
|
||||
|
||||
export default store;
|
||||
10
assets/store/settings.js
Normal file
10
assets/store/settings.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
|
||||
export const DEFAULT_SETTINGS = {
|
||||
search: true,
|
||||
size: "medium",
|
||||
menuWidth: 15,
|
||||
smallerScrollbars: false,
|
||||
showTimestamp: true,
|
||||
showAllContainers: false,
|
||||
lightTheme: false
|
||||
};
|
||||
@@ -1,19 +1,119 @@
|
||||
@charset "utf-8";
|
||||
@import "~bulma/sass/utilities/initial-variables.sass";
|
||||
|
||||
$menu-item-active-background-color: hsl(171, 100%, 41%);
|
||||
$menu-item-color: hsl(0, 6%, 87%);
|
||||
$body-family: "Roboto", sans-serif;
|
||||
$body-background-color: var(--body-background-color);
|
||||
$body-color: var(--body-color);
|
||||
|
||||
@import "../node_modules/bulma/bulma.sass";
|
||||
$scheme-main: var(--scheme-main);
|
||||
$scheme-main-bis: var(--scheme-main-bis);
|
||||
$scheme-main-ter: var(--scheme-main-ter);
|
||||
|
||||
.is-dark {
|
||||
color: #ddd;
|
||||
background-color: #111;
|
||||
$border: var(--border-color);
|
||||
$border-hover: var(--border-hover-color);
|
||||
|
||||
$menu-item-active-background-color: var(--menu-item-active-background-color);
|
||||
$menu-item-color: var(--menu-item-color);
|
||||
$menu-item-hover-background-color: var(--menu-item-hover-background-color);
|
||||
|
||||
$title-color: var(--title-color);
|
||||
|
||||
@import "~bulma";
|
||||
@import "../node_modules/splitpanes/dist/splitpanes.css";
|
||||
@import "~buefy/src/scss/utils/_all";
|
||||
@import "~buefy/src/scss/components/_dropdown";
|
||||
@import "~buefy/src/scss/components/_switch";
|
||||
|
||||
html {
|
||||
--scheme-main: #{$black};
|
||||
--scheme-main-bis: #{$black-bis};
|
||||
--scheme-main-ter: #{$black-ter};
|
||||
|
||||
--border-color: #{$grey-darker};
|
||||
--border-hover-color: var(--secondary-color);
|
||||
|
||||
--primary-color: #{$turquoise};
|
||||
--secondary-color: #{$yellow};
|
||||
|
||||
--body-background-color: #{$black-bis};
|
||||
--body-color: #{$grey-lighter};
|
||||
|
||||
--menu-item-active-background-color: var(--primary-color);
|
||||
--menu-item-color: hsl(0, 6%, 87%);
|
||||
--menu-item-hover-background-color: #{$white-ter};
|
||||
|
||||
--title-color: #{$grey-lightest};
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Roboto", sans-serif;
|
||||
[data-theme="light"] {
|
||||
--scheme-main: #{$white};
|
||||
--scheme-main-bis: #{$white-bis};
|
||||
--scheme-main-ter: #{$white-ter};
|
||||
|
||||
--border-color: #{$grey-lighter};
|
||||
--border-hover-color: var(--secondary-color);
|
||||
|
||||
--primary-color: #{$turquoise};
|
||||
--secondary-color: #d8f0ca;
|
||||
|
||||
--body-background-color: #{$white-bis};
|
||||
--body-color: #{$grey-darker};
|
||||
|
||||
--menu-item-color: #{$grey-dark};
|
||||
--menu-item-hover-background-color: #eee8e7;
|
||||
|
||||
--title-color: #{$grey-dark};
|
||||
}
|
||||
|
||||
h1.title {
|
||||
font-family: "Gafata", sans-serif;
|
||||
html {
|
||||
overflow-x: unset;
|
||||
overflow-y: unset;
|
||||
scroll-snap-type: y proximity;
|
||||
}
|
||||
|
||||
html.has-custom-scrollbars {
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
display: content;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(128, 128, 128, 0.33);
|
||||
outline: 1px solid slategrey;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background-color: #777;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track:hover {
|
||||
background-color: rgba(64, 64, 64, 0.33);
|
||||
}
|
||||
|
||||
section main {
|
||||
scrollbar-color: #353535 transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
}
|
||||
|
||||
.is-settings-control {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
&:hover {
|
||||
border-color: rgb(255, 221, 87) !important;
|
||||
background: rgba(0, 0, 0, 0.8) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none !important;
|
||||
color: unset;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
160
docker/client.go
160
docker/client.go
@@ -1,47 +1,96 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"io"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type dockerClient struct {
|
||||
cli dockerProxy
|
||||
cli dockerProxy
|
||||
filters filters.Args
|
||||
}
|
||||
|
||||
type dockerProxy interface {
|
||||
ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error)
|
||||
ContainerLogs(context.Context, string, types.ContainerLogsOptions) (io.ReadCloser, error)
|
||||
Events(context.Context, types.EventsOptions) (<-chan events.Message, <-chan error)
|
||||
ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error)
|
||||
}
|
||||
|
||||
// Client is a proxy around the docker client
|
||||
type Client interface {
|
||||
ListContainers() ([]Container, error)
|
||||
ContainerLogs(context.Context, string, int) (<-chan string, <-chan error)
|
||||
FindContainer(string) (Container, error)
|
||||
ContainerLogs(context.Context, string, int, string) (<-chan string, <-chan error)
|
||||
Events(context.Context) (<-chan events.Message, <-chan error)
|
||||
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time) ([]string, error)
|
||||
}
|
||||
|
||||
// NewClient creates a new instance of Client
|
||||
func NewClient() Client {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
return NewClientWithFilters(map[string]string{})
|
||||
}
|
||||
|
||||
// NewClientWithFilters creates a new instance of Client with docker filters
|
||||
func NewClientWithFilters(f map[string]string) Client {
|
||||
filterArgs := filters.NewArgs()
|
||||
for k, v := range f {
|
||||
filterArgs.Add(k, v)
|
||||
}
|
||||
|
||||
log.Debugf("filterArgs = %v", filterArgs)
|
||||
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return &dockerClient{cli}
|
||||
|
||||
return &dockerClient{cli, filterArgs}
|
||||
}
|
||||
|
||||
func (d *dockerClient) FindContainer(id string) (Container, error) {
|
||||
var container Container
|
||||
containers, err := d.ListContainers()
|
||||
if err != nil {
|
||||
return container, err
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, c := range containers {
|
||||
if c.ID == id {
|
||||
container = c
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == false {
|
||||
return container, fmt.Errorf("Unable to find container with id: %s", id)
|
||||
}
|
||||
|
||||
return container, nil
|
||||
}
|
||||
|
||||
func (d *dockerClient) ListContainers() ([]Container, error) {
|
||||
list, err := d.cli.ContainerList(context.Background(), types.ContainerListOptions{})
|
||||
containerListOptions := types.ContainerListOptions{
|
||||
Filters: d.filters,
|
||||
All: true,
|
||||
}
|
||||
list, err := d.cli.ContainerList(context.Background(), containerListOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -64,7 +113,7 @@ func (d *dockerClient) ListContainers() ([]Container, error) {
|
||||
}
|
||||
|
||||
sort.Slice(containers, func(i, j int) bool {
|
||||
return containers[i].Name < containers[j].Name
|
||||
return strings.ToLower(containers[i].Name) < strings.ToLower(containers[j].Name)
|
||||
})
|
||||
|
||||
if containers == nil {
|
||||
@@ -74,8 +123,45 @@ func (d *dockerClient) ListContainers() ([]Container, error) {
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize int) (<-chan string, <-chan error) {
|
||||
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: strconv.Itoa(tailSize), Timestamps: true}
|
||||
func logReader(reader io.ReadCloser, tty bool) func() (string, error) {
|
||||
if tty {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
return func() (string, error) {
|
||||
if scanner.Scan() {
|
||||
return scanner.Text(), nil
|
||||
}
|
||||
|
||||
return "", io.EOF
|
||||
}
|
||||
}
|
||||
hdr := make([]byte, 8)
|
||||
var buffer bytes.Buffer
|
||||
return func() (string, error) {
|
||||
buffer.Reset()
|
||||
_, err := reader.Read(hdr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
count := binary.BigEndian.Uint32(hdr[4:])
|
||||
_, err = io.CopyN(&buffer, reader, int64(count))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(buffer.String()), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize int, since string) (<-chan string, <-chan error) {
|
||||
log.WithField("id", id).WithField("since", since).Debug("Streaming logs for container")
|
||||
|
||||
options := types.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Follow: true,
|
||||
Tail: strconv.Itoa(tailSize),
|
||||
Timestamps: true,
|
||||
Since: since,
|
||||
}
|
||||
reader, err := d.cli.ContainerLogs(ctx, id, options)
|
||||
errChannel := make(chan error, 1)
|
||||
|
||||
@@ -91,30 +177,23 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize in
|
||||
reader.Close()
|
||||
}()
|
||||
|
||||
containerJSON, _ := d.cli.ContainerInspect(ctx, id)
|
||||
|
||||
go func() {
|
||||
defer close(messages)
|
||||
defer close(errChannel)
|
||||
defer reader.Close()
|
||||
|
||||
hdr := make([]byte, 8)
|
||||
var buffer bytes.Buffer
|
||||
nextEntry := logReader(reader, containerJSON.Config.Tty)
|
||||
for {
|
||||
_, err := reader.Read(hdr)
|
||||
if err != nil {
|
||||
errChannel <- err
|
||||
break
|
||||
}
|
||||
count := binary.BigEndian.Uint32(hdr[4:])
|
||||
_, err = io.CopyN(&buffer, reader, int64(count))
|
||||
line, err := nextEntry()
|
||||
if err != nil {
|
||||
errChannel <- err
|
||||
break
|
||||
}
|
||||
select {
|
||||
case messages <- buffer.String():
|
||||
case messages <- line:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
buffer.Reset()
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -124,3 +203,34 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize in
|
||||
func (d *dockerClient) Events(ctx context.Context) (<-chan events.Message, <-chan error) {
|
||||
return d.cli.Events(ctx, types.EventsOptions{})
|
||||
}
|
||||
|
||||
func (d *dockerClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time) ([]string, error) {
|
||||
options := types.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Timestamps: true,
|
||||
Since: strconv.FormatInt(from.Unix(), 10),
|
||||
Until: strconv.FormatInt(to.Unix(), 10),
|
||||
}
|
||||
reader, _ := d.cli.ContainerLogs(ctx, id, options)
|
||||
defer reader.Close()
|
||||
|
||||
containerJSON, _ := d.cli.ContainerInspect(ctx, id)
|
||||
|
||||
nextEntry := logReader(reader, containerJSON.Config.Tty)
|
||||
|
||||
var messages []string
|
||||
for {
|
||||
line, err := nextEntry()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
messages = append(messages, line)
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
@@ -5,13 +5,16 @@ import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockedProxy struct {
|
||||
@@ -37,11 +40,20 @@ func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options type
|
||||
}
|
||||
return reader, args.Error(1)
|
||||
}
|
||||
func (m *mockedProxy) ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error) {
|
||||
args := m.Called(ctx, containerID)
|
||||
json, ok := args.Get(0).(types.ContainerJSON)
|
||||
if !ok && args.Get(0) != nil {
|
||||
panic("proxies return value is not of type types.ContainerJSON")
|
||||
}
|
||||
|
||||
return json, args.Error(1)
|
||||
}
|
||||
|
||||
func Test_dockerClient_ListContainers_null(t *testing.T) {
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
|
||||
client := &dockerClient{proxy}
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
|
||||
list, err := client.ListContainers()
|
||||
assert.Empty(t, list, "list should be empty")
|
||||
@@ -53,7 +65,7 @@ func Test_dockerClient_ListContainers_null(t *testing.T) {
|
||||
func Test_dockerClient_ListContainers_error(t *testing.T) {
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test"))
|
||||
client := &dockerClient{proxy}
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
|
||||
list, err := client.ListContainers()
|
||||
assert.Nil(t, list, "list should be nil")
|
||||
@@ -76,7 +88,7 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
client := &dockerClient{proxy}
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
|
||||
list, err := client.ListContainers()
|
||||
require.NoError(t, err, "error should not return an error.")
|
||||
@@ -107,13 +119,39 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
|
||||
binary.BigEndian.PutUint32(b[4:], uint32(len(expected)))
|
||||
b = append(b, []byte(expected)...)
|
||||
|
||||
var reader io.ReadCloser
|
||||
reader = ioutil.NopCloser(bytes.NewReader(b))
|
||||
reader := ioutil.NopCloser(bytes.NewReader(b))
|
||||
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true, Since: "since"}
|
||||
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
|
||||
|
||||
json := types.ContainerJSON{Config: &container.Config{Tty: false}}
|
||||
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
|
||||
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
messages, _ := client.ContainerLogs(context.Background(), id, 300, "since")
|
||||
|
||||
actual, _ := <-messages
|
||||
assert.Equal(t, expected, actual, "message doesn't match expected")
|
||||
|
||||
_, ok := <-messages
|
||||
assert.False(t, ok, "channel should have been closed")
|
||||
proxy.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_dockerClient_ContainerLogs_happy_with_tty(t *testing.T) {
|
||||
id := "123456"
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
expected := "INFO Testing logs..."
|
||||
|
||||
reader := ioutil.NopCloser(bytes.NewReader([]byte(expected)))
|
||||
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true}
|
||||
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
|
||||
|
||||
client := &dockerClient{proxy}
|
||||
messages, _ := client.ContainerLogs(context.Background(), id, 300)
|
||||
json := types.ContainerJSON{Config: &container.Config{Tty: true}}
|
||||
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
|
||||
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
messages, _ := client.ContainerLogs(context.Background(), id, 300, "")
|
||||
|
||||
actual, _ := <-messages
|
||||
assert.Equal(t, expected, actual, "message doesn't match expected")
|
||||
@@ -129,9 +167,9 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
|
||||
|
||||
proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test"))
|
||||
|
||||
client := &dockerClient{proxy}
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
|
||||
messages, err := client.ContainerLogs(context.Background(), id, 300)
|
||||
messages, err := client.ContainerLogs(context.Background(), id, 300, "")
|
||||
|
||||
assert.Nil(t, messages, "messages should be nil")
|
||||
|
||||
@@ -141,3 +179,52 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
|
||||
assert.False(t, ok, "error channel should have been closed")
|
||||
proxy.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_dockerClient_FindContainer_happy(t *testing.T) {
|
||||
containers := []types.Container{
|
||||
{
|
||||
ID: "abcdefghijklmnopqrst",
|
||||
Names: []string{"/z_test_container"},
|
||||
},
|
||||
{
|
||||
ID: "1234567890_abcxyzdef",
|
||||
Names: []string{"/a_test_container"},
|
||||
},
|
||||
}
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
|
||||
container, err := client.FindContainer("abcdefghijkl")
|
||||
require.NoError(t, err, "error should not be thrown")
|
||||
|
||||
assert.Equal(t, container, Container{
|
||||
ID: "abcdefghijkl",
|
||||
Name: "z_test_container",
|
||||
Names: []string{"/z_test_container"},
|
||||
})
|
||||
|
||||
proxy.AssertExpectations(t)
|
||||
}
|
||||
func Test_dockerClient_FindContainer_error(t *testing.T) {
|
||||
containers := []types.Container{
|
||||
{
|
||||
ID: "abcdefghijklmnopqrst",
|
||||
Names: []string{"/z_test_container"},
|
||||
},
|
||||
{
|
||||
ID: "1234567890_abcxyzdef",
|
||||
Names: []string{"/a_test_container"},
|
||||
},
|
||||
}
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
client := &dockerClient{proxy, filters.NewArgs()}
|
||||
|
||||
_, err := client.FindContainer("not_valid")
|
||||
require.Error(t, err, "error should be thrown")
|
||||
|
||||
proxy.AssertExpectations(t)
|
||||
}
|
||||
|
||||
58
go.mod
58
go.mod
@@ -1,44 +1,54 @@
|
||||
module github.com/amir20/dozzle
|
||||
|
||||
replace github.com/docker/docker v0.0.0-20170601211448-f5ec1e2936dc => github.com/docker/engine v0.0.0-20180718150940-a3ef7e9a9bda
|
||||
replace github.com/docker/docker v0.0.0-20190827232753-32688a47f341 => github.com/docker/engine v0.0.0-20190827232753-32688a47f341
|
||||
|
||||
// github.com/docker/engine v18.06.1-ce
|
||||
replace github.com/docker/docker => github.com/docker/engine v0.0.0-20180816081446-320063a2ad06
|
||||
// github.com/docker/engine v19.06.1-ce
|
||||
replace github.com/docker/docker => github.com/docker/engine v0.0.0-20190827232753-32688a47f341
|
||||
|
||||
// github.com/docker/distribution master
|
||||
// a proper tagged release is expected in early fall(September 2018)
|
||||
// see; https://github.com/docker/distribution/issues/2693
|
||||
replace github.com/docker/distribution => github.com/docker/distribution v2.6.0-rc.1.0.20180820212402-02bf4a2887a4+incompatible
|
||||
replace github.com/docker/distribution => github.com/docker/distribution v0.0.0-20190711223531-1fb7fffdb266
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.12 // indirect
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/beme/abide v0.0.0-20181227202223-4c487ef9d895
|
||||
github.com/Microsoft/go-winio v0.4.14 // indirect
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760
|
||||
github.com/containerd/containerd v1.3.3 // indirect
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/docker v0.0.0-20170601211448-f5ec1e2936dc
|
||||
github.com/docker/docker v0.0.0-20190827232753-32688a47f341
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/gobuffalo/packd v0.1.0 // indirect
|
||||
github.com/gobuffalo/packr v1.25.0
|
||||
github.com/gogo/protobuf v1.2.1 // indirect
|
||||
github.com/google/go-cmp v0.3.0 // indirect
|
||||
github.com/gorilla/mux v1.7.1
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/gobuffalo/envy v1.9.0 // indirect
|
||||
github.com/gobuffalo/packd v1.0.0 // indirect
|
||||
github.com/gobuffalo/packr v1.30.1
|
||||
github.com/gogo/protobuf v1.3.1 // indirect
|
||||
github.com/golang/protobuf v1.3.4 // indirect
|
||||
github.com/google/go-cmp v0.3.1 // indirect
|
||||
github.com/gorilla/mux v1.7.4
|
||||
github.com/magiconair/properties v1.8.1
|
||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/pelletier/go-toml v1.6.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.5.2 // indirect
|
||||
github.com/sergi/go-diff v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.4.1
|
||||
github.com/spf13/pflag v1.0.3
|
||||
github.com/spf13/viper v1.3.2
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.7.0
|
||||
github.com/stretchr/objx v0.2.0 // indirect
|
||||
github.com/stretchr/testify v1.3.0
|
||||
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 // indirect
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
|
||||
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 // indirect
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
|
||||
google.golang.org/grpc v1.20.1 // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b // indirect
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae // indirect
|
||||
google.golang.org/genproto v0.0.0-20200226201735-46b91f19d98c // indirect
|
||||
google.golang.org/grpc v1.27.1 // indirect
|
||||
gopkg.in/ini.v1 v1.52.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||
gotest.tools v2.2.0+incompatible // indirect
|
||||
)
|
||||
|
||||
go 1.14
|
||||
|
||||
433
go.sum
433
go.sum
@@ -1,118 +1,310 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiUOryc=
|
||||
github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/beme/abide v0.0.0-20181227202223-4c487ef9d895 h1:gKYojZRR5Nko2XJrcAEiQpBQbir/wzsNqGqtOjKJU6g=
|
||||
github.com/beme/abide v0.0.0-20181227202223-4c487ef9d895/go.mod h1:6+8gCKsZnxzhGTmKRh4BSkLos9CbWRJNcrp55We4SqQ=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760 h1:FvTM5NSN5HYvfKpgL+8x73U5v063vHsd7AX05eV1DnM=
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760/go.mod h1:6+8gCKsZnxzhGTmKRh4BSkLos9CbWRJNcrp55We4SqQ=
|
||||
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
|
||||
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
|
||||
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
|
||||
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/containerd/containerd v1.3.3 h1:LoIzb5y9x5l8VKAlyrbusNPXqBY0+kviRloxFUMFwKc=
|
||||
github.com/containerd/containerd v1.3.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/distribution v2.6.0-rc.1.0.20180820212402-02bf4a2887a4+incompatible h1:x3ZXVm6ovZmIA+s9MEdSXjdyd5Zbd5VPBcda2KrSuWk=
|
||||
github.com/docker/distribution v2.6.0-rc.1.0.20180820212402-02bf4a2887a4+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/engine v0.0.0-20180816081446-320063a2ad06 h1:CcxlLWAS/9b46iqHDTBlALJZF9atXVNjeymdCNrUfnY=
|
||||
github.com/docker/engine v0.0.0-20180816081446-320063a2ad06/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
|
||||
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
|
||||
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/docker/distribution v0.0.0-20190711223531-1fb7fffdb266 h1:6BCth6L0iZKTU3F0OxqlkECwdmUDLbHdD9qz6HXlpb4=
|
||||
github.com/docker/distribution v0.0.0-20190711223531-1fb7fffdb266/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
|
||||
github.com/docker/engine v0.0.0-20190827232753-32688a47f341 h1:EZsx4y4IdfCZofMwt/ICb/8P5TgSR69Zrnw21vOHKc0=
|
||||
github.com/docker/engine v0.0.0-20190827232753-32688a47f341/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
|
||||
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
|
||||
github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
|
||||
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU=
|
||||
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
|
||||
github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=
|
||||
github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
|
||||
github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
|
||||
github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=
|
||||
github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=
|
||||
github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=
|
||||
github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=
|
||||
github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
|
||||
github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
|
||||
github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0 h1:P6naWPiHm/7R3eYx/ub3VhaW9G+1xAMJ6vzACePaGPI=
|
||||
github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
|
||||
github.com/gobuffalo/packd v0.1.0 h1:4sGKOD8yaYJ+dek1FDkwcxCHA40M4kfKgFHx8N2kwbU=
|
||||
github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
|
||||
github.com/gobuffalo/packr v1.25.0 h1:NtPK45yOKFdTKHTvRGKL+UIKAKmJVWIVJOZBDI/qEdY=
|
||||
github.com/gobuffalo/packr v1.25.0/go.mod h1:NqsGg8CSB2ZD+6RBIRs18G7aZqdYDlYNNvsSqP6T4/U=
|
||||
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
|
||||
github.com/gobuffalo/packr/v2 v2.1.0/go.mod h1:n90ZuXIc2KN2vFAOQascnPItp9A2g9QYSvYvS3AjQEM=
|
||||
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754 h1:tpom+2CJmpzAWj5/VEHync2rJGi+epHNIeRSWjzGA+4=
|
||||
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
||||
github.com/gobuffalo/envy v1.9.0 h1:eZR0DuEgVLfeIb1zIKt3bT4YovIMf9O9LXQeCZLXpqE=
|
||||
github.com/gobuffalo/envy v1.9.0/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
|
||||
github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
|
||||
github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
|
||||
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
|
||||
github.com/gobuffalo/packd v1.0.0 h1:6ERZvJHfe24rfFmA9OaoKBdC7+c9sydrytMg8SdFGBM=
|
||||
github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI=
|
||||
github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
|
||||
github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk=
|
||||
github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/gorilla/mux v1.7.1 h1:Dw4jY2nghMMRsh1ol8dv1axHkDwMQK2DHerMNJsIpJU=
|
||||
github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
||||
github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
|
||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
|
||||
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.2 h1:XU784Pr0wdahMY2bYcyK6N1KuaRAdLtqD4qd8D18Bfs=
|
||||
github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.5.2 h1:qLvObTrvO/XRCqmkKxUlOBc48bI3efyDuAZe25QiF0w=
|
||||
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
|
||||
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
|
||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs=
|
||||
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
|
||||
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
@@ -120,48 +312,191 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
|
||||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
|
||||
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
|
||||
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak=
|
||||
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug=
|
||||
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438 h1:khxRGsvPk4n2y8I/mLLjp7e5dMTJmH75wvqS6nMwUtY=
|
||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190404132500-923d25813098/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200226201735-46b91f19d98c h1:xFOdgVPpeowWAH0MJ5i0XMp+3yWiWamMtN/kx9xThIQ=
|
||||
google.golang.org/genproto v0.0.0-20200226201735-46b91f19d98c/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.52.0 h1:j+Lt/M1oPPejkniCg1TkWE2J3Eh1oZTsHSXzMTzUXn4=
|
||||
gopkg.in/ini.v1 v1.52.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
|
||||
1
integration/.dockerignore
Normal file
1
integration/.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
1
integration/.gitignore
vendored
Normal file
1
integration/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
__diff_output__
|
||||
8
integration/Dockerfile
Normal file
8
integration/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM amir20/docker-alpine-puppeteer:edge
|
||||
|
||||
COPY --chown=pptruser:pptruser package*.json yarn.lock /app/
|
||||
RUN yarn
|
||||
|
||||
COPY --chown=pptruser:pptruser . /app/
|
||||
|
||||
CMD [ "yarn", "test"]
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
18
integration/docker-compose.test.yml
Normal file
18
integration/docker-compose.test.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
dozzle:
|
||||
container_name: dozzle
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- DOZZLE_FILTER=name=dozzle
|
||||
build:
|
||||
context: ..
|
||||
integration:
|
||||
build:
|
||||
context: .
|
||||
command: yarn test
|
||||
environment:
|
||||
- BASE=http://dozzle:8080/
|
||||
depends_on:
|
||||
- dozzle
|
||||
9
integration/jest-puppeteer.config.js
Normal file
9
integration/jest-puppeteer.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
launch: {
|
||||
headless: process.env.HEADLESS !== "false",
|
||||
defaultViewport: { width: 1920, height: 1200 },
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||
executablePath: process.env.CHROME_EXE_PATH || "",
|
||||
},
|
||||
browserContext: "incognito",
|
||||
};
|
||||
5
integration/jest-setup.js
Normal file
5
integration/jest-setup.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const { toMatchImageSnapshot } = require("jest-image-snapshot");
|
||||
|
||||
expect.extend({ toMatchImageSnapshot });
|
||||
|
||||
jest.setTimeout(5000);
|
||||
24
integration/package.json
Normal file
24
integration/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "test",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"test": "jest"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"jest": "^26.0.1",
|
||||
"jest-image-snapshot": "^4.0.0",
|
||||
"puppeteer": "^4.0.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-puppeteer",
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/jest-setup.js"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest-puppeteer": "^4.4.0"
|
||||
}
|
||||
}
|
||||
72
integration/test.js
Normal file
72
integration/test.js
Normal file
@@ -0,0 +1,72 @@
|
||||
const puppeteer = require("puppeteer");
|
||||
const iPhoneX = puppeteer.devices["iPhone X"];
|
||||
const iPadLandscape = puppeteer.devices["iPad landscape"];
|
||||
|
||||
const { BASE } = process.env;
|
||||
|
||||
describe("home page", () => {
|
||||
beforeEach(async () => {
|
||||
await page.goto(BASE, { waitUntil: "networkidle2" });
|
||||
});
|
||||
|
||||
it("renders full page on desktop", async () => {
|
||||
const image = await page.screenshot({ fullPage: true });
|
||||
|
||||
expect(image).toMatchImageSnapshot();
|
||||
});
|
||||
|
||||
it("renders ipad viewport", async () => {
|
||||
await page.emulate(iPadLandscape);
|
||||
const image = await page.screenshot();
|
||||
|
||||
expect(image).toMatchImageSnapshot();
|
||||
});
|
||||
|
||||
it("renders iphone viewport", async () => {
|
||||
await page.emulate(iPhoneX);
|
||||
const image = await page.screenshot();
|
||||
|
||||
expect(image).toMatchImageSnapshot();
|
||||
});
|
||||
|
||||
it("displays iphone menu", async () => {
|
||||
await page.emulate(iPhoneX);
|
||||
await page.click("a.navbar-burger");
|
||||
|
||||
const menuText = await page.$eval("aside ul.menu-list.is-hidden-mobile li a", (e) => e.textContent);
|
||||
expect(menuText.trim()).toEqual("dozzle");
|
||||
});
|
||||
|
||||
describe("has menu visible", () => {
|
||||
beforeAll(async () => {
|
||||
await jestPuppeteer.resetBrowser();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await page.goto(BASE, { waitUntil: "networkidle2" });
|
||||
});
|
||||
|
||||
it("and shows one container with correct title", async () => {
|
||||
const menuTitle = await page.$eval("aside ul.menu-list li a", (e) => e.title);
|
||||
|
||||
expect(menuTitle).toEqual("dozzle");
|
||||
});
|
||||
|
||||
it("and menu is clickable", async () => {
|
||||
await page.click("aside ul.menu-list li a");
|
||||
|
||||
const className = await page.$eval("aside ul.menu-list li a", (e) => e.className);
|
||||
|
||||
expect(className).toContain("router-link-exact-active");
|
||||
});
|
||||
|
||||
it("and when clicked shows logs", async () => {
|
||||
await page.click("aside ul.menu-list li a");
|
||||
|
||||
await page.waitForSelector("ul.events li span.text");
|
||||
const text = await page.$eval("ul.events li:nth-child(2) span.text", (e) => e.textContent);
|
||||
|
||||
expect(text).toContain("Dozzle version dev");
|
||||
});
|
||||
});
|
||||
});
|
||||
4129
integration/yarn.lock
Normal file
4129
integration/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
13
jest.config.js
Normal file
13
jest.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ["js", "json", "vue"],
|
||||
coveragePathIgnorePatterns: ["node_modules"],
|
||||
testPathIgnorePatterns: ["node_modules", "<rootDir>/integration/"],
|
||||
transformIgnorePatterns: ["node_modules"],
|
||||
watchPathIgnorePatterns: ["<rootDir>/node_modules/"],
|
||||
snapshotSerializers: ["jest-serializer-vue"],
|
||||
transform: {
|
||||
".*\\.vue$": "vue-jest",
|
||||
"^.+\\.js$": "babel-jest",
|
||||
},
|
||||
};
|
||||
196
main.go
196
main.go
@@ -2,31 +2,28 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/gobuffalo/packr"
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
addr = ""
|
||||
base = ""
|
||||
level = ""
|
||||
addr = ""
|
||||
base = ""
|
||||
level = ""
|
||||
showAll = false
|
||||
tailSize = 300
|
||||
version = "dev"
|
||||
commit = "none"
|
||||
date = "unknown"
|
||||
filters map[string]string
|
||||
version = "dev"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
@@ -39,6 +36,7 @@ func init() {
|
||||
pflag.String("base", "/", "base address of the application to mount")
|
||||
pflag.String("level", "info", "logging level")
|
||||
pflag.Int("tailSize", 300, "Tail size to use for initial container logs")
|
||||
pflag.StringToStringVar(&filters, "filter", map[string]string{}, "Container filters to use for showing logs")
|
||||
pflag.Parse()
|
||||
|
||||
viper.AutomaticEnv()
|
||||
@@ -50,6 +48,20 @@ func init() {
|
||||
level = viper.GetString("level")
|
||||
tailSize = viper.GetInt("tailSize")
|
||||
|
||||
// Until https://github.com/spf13/viper/issues/911 is fixed. We have to use this hacky way.
|
||||
// filters = viper.GetStringMapString("filter")
|
||||
if value, ok := os.LookupEnv("DOZZLE_FILTER"); ok {
|
||||
log.Infof("Parsing %s", value)
|
||||
urlValues, err := url.ParseQuery(strings.ReplaceAll(value, ",", "&"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
filters = map[string]string{}
|
||||
for k, v := range urlValues {
|
||||
filters[k] = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
l, _ := log.ParseLevel(level)
|
||||
log.SetLevel(l)
|
||||
|
||||
@@ -59,25 +71,9 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
func createRoutes(base string, h *handler) *mux.Router {
|
||||
r := mux.NewRouter()
|
||||
if base != "/" {
|
||||
r.HandleFunc(base, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, base+"/", http.StatusMovedPermanently)
|
||||
}))
|
||||
}
|
||||
s := r.PathPrefix(base).Subrouter()
|
||||
s.HandleFunc("/api/containers.json", h.listContainers)
|
||||
s.HandleFunc("/api/logs/stream", h.streamLogs)
|
||||
s.HandleFunc("/api/events/stream", h.streamEvents)
|
||||
s.HandleFunc("/version", h.version)
|
||||
s.PathPrefix("/").Handler(http.StripPrefix(base, http.HandlerFunc(h.index)))
|
||||
return r
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.Infof("Dozzle version %s", version)
|
||||
dockerClient := docker.NewClient()
|
||||
dockerClient := docker.NewClientWithFilters(filters)
|
||||
_, err := dockerClient.ListContainers()
|
||||
|
||||
if err != nil {
|
||||
@@ -85,7 +81,10 @@ func main() {
|
||||
}
|
||||
|
||||
box := packr.NewBox("./static")
|
||||
r := createRoutes(base, &handler{dockerClient, box})
|
||||
r := createRoutes(base, &handler{
|
||||
client: dockerClient,
|
||||
box: box,
|
||||
})
|
||||
srv := &http.Server{Addr: addr, Handler: r}
|
||||
|
||||
go func() {
|
||||
@@ -105,132 +104,3 @@ func main() {
|
||||
srv.Shutdown(ctx)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func (h *handler) index(w http.ResponseWriter, req *http.Request) {
|
||||
fileServer := http.FileServer(h.box)
|
||||
if h.box.Has(req.URL.Path) && req.URL.Path != "" && req.URL.Path != "/" {
|
||||
fileServer.ServeHTTP(w, req)
|
||||
} else {
|
||||
text, _ := h.box.FindString("index.html")
|
||||
text = strings.Replace(text, "__BASE__", "{{ .Base }}", -1)
|
||||
tmpl, err := template.New("index.html").Parse(text)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
path := ""
|
||||
if base != "/" {
|
||||
path = base
|
||||
}
|
||||
|
||||
data := struct{ Base string }{path}
|
||||
err = tmpl.Execute(w, data)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) listContainers(w http.ResponseWriter, r *http.Request) {
|
||||
containers, err := h.client.ListContainers()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(containers)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
if id == "" {
|
||||
http.Error(w, "id is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
f, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
messages, err := h.client.ContainerLogs(r.Context(), id, tailSize)
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("Transfer-Encoding", "chunked")
|
||||
|
||||
log.Debugf("Starting to stream logs for %s", id)
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case message, ok := <-messages:
|
||||
if !ok {
|
||||
break Loop
|
||||
}
|
||||
_, e := fmt.Fprintf(w, "data: %s\n\n", message)
|
||||
if e != nil {
|
||||
log.Debugf("Error while writing to log stream: %v", e)
|
||||
break Loop
|
||||
}
|
||||
f.Flush()
|
||||
case e := <-err:
|
||||
log.Debugf("Error while reading from log stream: %v", e)
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
|
||||
log.WithField("NumGoroutine", runtime.NumGoroutine()).Debug("runtime stats")
|
||||
}
|
||||
|
||||
func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
f, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("Transfer-Encoding", "chunked")
|
||||
|
||||
ctx := r.Context()
|
||||
messages, err := h.client.Events(ctx)
|
||||
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case message, ok := <-messages:
|
||||
if !ok {
|
||||
break Loop
|
||||
}
|
||||
switch message.Action {
|
||||
case "connect", "disconnect", "create", "destroy", "start", "stop":
|
||||
log.Debugf("Triggering docker event: %v", message.Action)
|
||||
_, err := fmt.Fprintf(w, "event: containers-changed\ndata: %s\n\n", message.Action)
|
||||
|
||||
if err != nil {
|
||||
log.Debugf("Error while writing to event stream: %v", err)
|
||||
break
|
||||
}
|
||||
f.Flush()
|
||||
default:
|
||||
log.Debugf("Ignoring docker event: %v", message.Action)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
break Loop
|
||||
case <-err:
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) version(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, version)
|
||||
fmt.Fprintln(w, commit)
|
||||
fmt.Fprintln(w, date)
|
||||
}
|
||||
|
||||
107
main_test.go
107
main_test.go
@@ -3,12 +3,14 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/magiconair/properties/assert"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/magiconair/properties/assert"
|
||||
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/beme/abide"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
@@ -22,6 +24,15 @@ type MockedClient struct {
|
||||
docker.Client
|
||||
}
|
||||
|
||||
func (m *MockedClient) FindContainer(id string) (docker.Container, error) {
|
||||
args := m.Called(id)
|
||||
container, ok := args.Get(0).(docker.Container)
|
||||
if !ok {
|
||||
panic("containers is not of type docker.Container")
|
||||
}
|
||||
return container, args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClient) ListContainers() ([]docker.Container, error) {
|
||||
args := m.Called()
|
||||
containers, ok := args.Get(0).([]docker.Container)
|
||||
@@ -31,7 +42,7 @@ func (m *MockedClient) ListContainers() ([]docker.Container, error) {
|
||||
return containers, args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockedClient) ContainerLogs(ctx context.Context, id string, tailSize int) (<-chan string, <-chan error) {
|
||||
func (m *MockedClient) ContainerLogs(ctx context.Context, id string, tailSize int, since string) (<-chan string, <-chan error) {
|
||||
args := m.Called(ctx, id, tailSize)
|
||||
channel, ok := args.Get(0).(chan string)
|
||||
if !ok {
|
||||
@@ -95,13 +106,12 @@ func Test_handler_streamLogs_happy(t *testing.T) {
|
||||
req.URL.RawQuery = q.Encode()
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
mockedClient := new(MockedClient)
|
||||
|
||||
messages := make(chan string)
|
||||
errChannel := make(chan error)
|
||||
mockedClient.On("ContainerLogs", mock.Anything, id, 300).Return(messages, errChannel)
|
||||
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
|
||||
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, 300).Return(messages, errChannel)
|
||||
go func() {
|
||||
messages <- "INFO Testing logs..."
|
||||
close(messages)
|
||||
@@ -109,6 +119,80 @@ func Test_handler_streamLogs_happy(t *testing.T) {
|
||||
|
||||
h := handler{client: mockedClient}
|
||||
handler := http.HandlerFunc(h.streamLogs)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_handler_streamLogs_happy_with_id(t *testing.T) {
|
||||
id := "123456"
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
|
||||
q := req.URL.Query()
|
||||
q.Add("id", id)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
mockedClient := new(MockedClient)
|
||||
|
||||
messages := make(chan string)
|
||||
errChannel := make(chan error)
|
||||
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
|
||||
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, 300).Return(messages, errChannel)
|
||||
go func() {
|
||||
messages <- "2020-05-13T18:55:37.772853839Z INFO Testing logs..."
|
||||
close(messages)
|
||||
}()
|
||||
|
||||
h := handler{client: mockedClient}
|
||||
handler := http.HandlerFunc(h.streamLogs)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_handler_streamLogs_happy_container_stopped(t *testing.T) {
|
||||
id := "123456"
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
|
||||
q := req.URL.Query()
|
||||
q.Add("id", id)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
mockedClient := new(MockedClient)
|
||||
messages := make(chan string)
|
||||
errChannel := make(chan error)
|
||||
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
|
||||
mockedClient.On("ContainerLogs", mock.Anything, id, 300).Return(messages, errChannel)
|
||||
|
||||
go func() {
|
||||
errChannel <- io.EOF
|
||||
close(messages)
|
||||
}()
|
||||
|
||||
h := handler{client: mockedClient}
|
||||
handler := http.HandlerFunc(h.streamLogs)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_handler_streamLogs_error_finding_container(t *testing.T) {
|
||||
id := "123456"
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
|
||||
q := req.URL.Query()
|
||||
q.Add("id", id)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
mockedClient := new(MockedClient)
|
||||
mockedClient.On("FindContainer", id).Return(docker.Container{}, errors.New("error finding container"))
|
||||
|
||||
h := handler{client: mockedClient}
|
||||
handler := http.HandlerFunc(h.streamLogs)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
@@ -118,22 +202,24 @@ func Test_handler_streamLogs_error_reading(t *testing.T) {
|
||||
id := "123456"
|
||||
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
|
||||
q := req.URL.Query()
|
||||
q.Add("id", "123456")
|
||||
q.Add("id", id)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
mockedClient := new(MockedClient)
|
||||
messages := make(chan string)
|
||||
errChannel := make(chan error)
|
||||
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
|
||||
mockedClient.On("ContainerLogs", mock.Anything, id, 300).Return(messages, errChannel)
|
||||
|
||||
go func() {
|
||||
errChannel <- errors.New("test error")
|
||||
close(messages)
|
||||
}()
|
||||
|
||||
h := handler{client: mockedClient}
|
||||
handler := http.HandlerFunc(h.streamLogs)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
@@ -142,7 +228,6 @@ func Test_handler_streamLogs_error_reading(t *testing.T) {
|
||||
func Test_handler_streamEvents_happy(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/api/events/stream", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
rr := httptest.NewRecorder()
|
||||
mockedClient := new(MockedClient)
|
||||
messages := make(chan events.Message)
|
||||
errChannel := make(chan error)
|
||||
@@ -160,6 +245,7 @@ func Test_handler_streamEvents_happy(t *testing.T) {
|
||||
|
||||
h := handler{client: mockedClient}
|
||||
handler := http.HandlerFunc(h.streamEvents)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
@@ -168,7 +254,6 @@ func Test_handler_streamEvents_happy(t *testing.T) {
|
||||
func Test_handler_streamEvents_error(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/api/events/stream", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
rr := httptest.NewRecorder()
|
||||
mockedClient := new(MockedClient)
|
||||
messages := make(chan events.Message)
|
||||
errChannel := make(chan error)
|
||||
@@ -181,6 +266,7 @@ func Test_handler_streamEvents_error(t *testing.T) {
|
||||
|
||||
h := handler{client: mockedClient}
|
||||
handler := http.HandlerFunc(h.streamEvents)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
@@ -190,8 +276,6 @@ func Test_handler_streamEvents_error_request(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/api/events/stream", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
mockedClient := new(MockedClient)
|
||||
|
||||
messages := make(chan events.Message)
|
||||
@@ -207,6 +291,7 @@ func Test_handler_streamEvents_error_request(t *testing.T) {
|
||||
|
||||
h := handler{client: mockedClient}
|
||||
handler := http.HandlerFunc(h.streamEvents)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
mockedClient.AssertExpectations(t)
|
||||
|
||||
8679
package-lock.json
generated
8679
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
97
package.json
97
package.json
@@ -1,16 +1,18 @@
|
||||
{
|
||||
"name": "dozzle",
|
||||
"version": "1.9.0",
|
||||
"version": "2.0.0",
|
||||
"description": "Realtime log viewer for docker containers. ",
|
||||
"scripts": {
|
||||
"prestart": "npm run clean",
|
||||
"start": "DOCKER_API_VERSION=1.38 concurrently 'npm run watch-server' 'npm run watch-assets'",
|
||||
"watch-assets": "npx parcel watch --public-url '__BASE__' assets/index.html -d static",
|
||||
"watch-server": "reflex -c .reflex",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "npx parcel build --no-source-maps --public-url '__BASE__' assets/index.html -d static",
|
||||
"prestart": "yarn clean",
|
||||
"start": "npm-run-all -p watch:*",
|
||||
"watch:assets": "webpack --mode=development --watch",
|
||||
"watch:server": "reflex -c .reflex",
|
||||
"prebuild": "yarn clean",
|
||||
"build": "yarn webpack --mode=production",
|
||||
"clean": "rm -rf static/ a_main-packr.go",
|
||||
"release": "goreleaser --rm-dist"
|
||||
"release": "release-it",
|
||||
"test": "TZ=UTC jest",
|
||||
"integration": "docker-compose -f integration/docker-compose.test.yml up --build --force-recreate integration"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -23,24 +25,55 @@
|
||||
},
|
||||
"homepage": "https://github.com/amir20/dozzle#readme",
|
||||
"dependencies": {
|
||||
"bulma": "^0.7.4",
|
||||
"date-fns": "^2.0.0-alpha.25",
|
||||
"vue": "^2.6.10",
|
||||
"vue-meta": "^1.6.0",
|
||||
"vue-router": "^3.0.6"
|
||||
"ansi-to-html": "^0.6.14",
|
||||
"buefy": "^0.8.20",
|
||||
"bulma": "^0.9.0",
|
||||
"date-fns": "^2.14.0",
|
||||
"dompurify": "^2.0.12",
|
||||
"hotkeys-js": "^3.8.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"semver": "^7.3.2",
|
||||
"splitpanes": "^2.2.1",
|
||||
"store": "^2.0.12",
|
||||
"vue": "^2.6.11",
|
||||
"vue-meta": "^2.4.0",
|
||||
"vue-router": "^3.3.4",
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.4.4",
|
||||
"@babel/plugin-transform-runtime": "^7.4.4",
|
||||
"@vue/component-compiler-utils": "^3.0.0",
|
||||
"concurrently": "^4.1.0",
|
||||
"husky": "^2.2.0",
|
||||
"lint-staged": "^8.1.6",
|
||||
"parcel-bundler": "^1.12.3",
|
||||
"prettier": "^1.17.0",
|
||||
"sass": "^1.20.1",
|
||||
"vue-hot-reload-api": "^2.3.3",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
"@babel/core": "^7.10.3",
|
||||
"@babel/plugin-transform-runtime": "^7.10.3",
|
||||
"@vue/component-compiler-utils": "^3.1.2",
|
||||
"@vue/test-utils": "^1.0.3",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^26.1.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"caniuse-lite": "^1.0.30001090",
|
||||
"css-loader": "^3.6.0",
|
||||
"eventsourcemock": "^2.0.0",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"husky": "^4.2.5",
|
||||
"jest": "^26.1.0",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
"lint-staged": "^10.2.11",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss-cssnext": "^3.1.0",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"prettier": "^2.0.5",
|
||||
"release-it": "^13.6.3",
|
||||
"sass": "^1.26.9",
|
||||
"sass-loader": "^8.0.2",
|
||||
"vue-hot-reload-api": "^2.3.4",
|
||||
"vue-jest": "^3.0.5",
|
||||
"vue-loader": "^15.9.3",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-pwa-manifest": "^4.2.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
@@ -49,15 +82,15 @@
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,vue,css}": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"browserslist": [
|
||||
">5%",
|
||||
"not ie <= 8"
|
||||
],
|
||||
"alias": {
|
||||
"vue": "./node_modules/vue/dist/vue.esm.js"
|
||||
"release-it": {
|
||||
"github": {
|
||||
"release": true
|
||||
},
|
||||
"npm": {
|
||||
"publish": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
200
routes.go
Normal file
200
routes.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func createRoutes(base string, h *handler) *mux.Router {
|
||||
r := mux.NewRouter()
|
||||
r.Use(setCSPHeaders)
|
||||
if base != "/" {
|
||||
r.HandleFunc(base, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, base+"/", http.StatusMovedPermanently)
|
||||
}))
|
||||
}
|
||||
s := r.PathPrefix(base).Subrouter()
|
||||
s.HandleFunc("/api/containers.json", h.listContainers)
|
||||
s.HandleFunc("/api/logs/stream", h.streamLogs)
|
||||
s.HandleFunc("/api/logs", h.fetchLogsBetweenDates)
|
||||
s.HandleFunc("/api/events/stream", h.streamEvents)
|
||||
s.HandleFunc("/version", h.version)
|
||||
s.PathPrefix("/").Handler(http.StripPrefix(base, http.HandlerFunc(h.index)))
|
||||
return r
|
||||
}
|
||||
|
||||
func setCSPHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline' fonts.googleapis.com; img-src 'self'; manifest-src 'self'; font-src fonts.gstatic.com; connect-src 'self' api.github.com; require-trusted-types-for 'script'")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handler) index(w http.ResponseWriter, req *http.Request) {
|
||||
fileServer := http.FileServer(h.box)
|
||||
if h.box.Has(req.URL.Path) && req.URL.Path != "" && req.URL.Path != "/" {
|
||||
fileServer.ServeHTTP(w, req)
|
||||
} else {
|
||||
text, err := h.box.FindString("index.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tmpl, err := template.New("index.html").Parse(text)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
path := ""
|
||||
if base != "/" {
|
||||
path = base
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Base string
|
||||
Version string
|
||||
}{path, version}
|
||||
err = tmpl.Execute(w, data)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) listContainers(w http.ResponseWriter, r *http.Request) {
|
||||
containers, err := h.client.ListContainers()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(containers)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
|
||||
|
||||
from, _ := time.Parse(time.RFC3339, r.URL.Query().Get("from"))
|
||||
to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to"))
|
||||
id := r.URL.Query().Get("id")
|
||||
|
||||
messages, _ := h.client.ContainerLogsBetweenDates(r.Context(), id, from, to)
|
||||
|
||||
for _, m := range messages {
|
||||
fmt.Fprintln(w, m)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
if id == "" {
|
||||
http.Error(w, "id is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
f, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
container, e := h.client.FindContainer(id)
|
||||
if e != nil {
|
||||
http.Error(w, e.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
messages, err := h.client.ContainerLogs(r.Context(), container.ID, tailSize, r.Header.Get("Last-Event-ID"))
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case message, ok := <-messages:
|
||||
if !ok {
|
||||
break Loop
|
||||
}
|
||||
fmt.Fprintf(w, "data: %s\n", message)
|
||||
if index := strings.IndexAny(message, " "); index != -1 {
|
||||
id := message[:index]
|
||||
if _, err := time.Parse(time.RFC3339Nano, id); err == nil {
|
||||
fmt.Fprintf(w, "id: %s\n", id)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(w, "\n")
|
||||
f.Flush()
|
||||
case e := <-err:
|
||||
if e == io.EOF {
|
||||
log.Debugf("Container stopped: %v", container.ID)
|
||||
fmt.Fprintf(w, "event: container-stopped\ndata: end of stream\n\n")
|
||||
f.Flush()
|
||||
} else {
|
||||
log.Debugf("Error while reading from log stream: %v", e)
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.WithField("NumGoroutine", runtime.NumGoroutine()).Debug("runtime stats")
|
||||
}
|
||||
|
||||
func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
f, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
ctx := r.Context()
|
||||
messages, err := h.client.Events(ctx)
|
||||
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case message, ok := <-messages:
|
||||
if !ok {
|
||||
break Loop
|
||||
}
|
||||
switch message.Action {
|
||||
case "connect", "disconnect", "create", "destroy", "start", "stop":
|
||||
log.Debugf("Triggering docker event: %v", message.Action)
|
||||
_, err := fmt.Fprintf(w, "event: containers-changed\ndata: %s\n\n", message.Action)
|
||||
|
||||
if err != nil {
|
||||
log.Debugf("Error while writing to event stream: %v", err)
|
||||
break
|
||||
}
|
||||
f.Flush()
|
||||
default:
|
||||
log.Debugf("Ignoring docker event: %v", message.Action)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
break Loop
|
||||
case <-err:
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) version(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, version)
|
||||
}
|
||||
78
webpack.config.js
Normal file
78
webpack.config.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const path = require("path");
|
||||
const { VueLoaderPlugin } = require("vue-loader");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const WebpackPwaManifest = require("webpack-pwa-manifest");
|
||||
|
||||
module.exports = (env, argv) => ({
|
||||
stats: { children: false, entrypoints: false, modules: false },
|
||||
performance: {
|
||||
maxAssetSize: 350000,
|
||||
maxEntrypointSize: 600000,
|
||||
},
|
||||
devtool: argv.mode === "development" ? "inline-cheap-source-map" : false,
|
||||
entry: ["./assets/main.js", "./assets/styles.scss"],
|
||||
output: {
|
||||
path: path.resolve(__dirname, "./static"),
|
||||
filename: "[name].js",
|
||||
publicPath: "{{ .Base }}",
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: "vue-loader",
|
||||
},
|
||||
{
|
||||
test: /\.(sass|scss|css)$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
{
|
||||
loader: "css-loader",
|
||||
query: {
|
||||
importLoaders: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: "postcss-loader",
|
||||
options: {
|
||||
ident: "postcss",
|
||||
plugins: (loader) => [
|
||||
require("postcss-import")(),
|
||||
require("postcss-cssnext")({
|
||||
features: {
|
||||
customProperties: { warnings: false },
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
"sass-loader",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new VueLoaderPlugin(),
|
||||
new MiniCssExtractPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
hash: true,
|
||||
template: "assets/index.ejs",
|
||||
scriptLoading: "defer",
|
||||
favicon: "assets/favicon.svg",
|
||||
}),
|
||||
new WebpackPwaManifest({
|
||||
name: "Dozzle Log Viewer",
|
||||
short_name: "Dozzle",
|
||||
theme_color: "#222",
|
||||
background_color: "#222",
|
||||
display: "standalone",
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
vue$: "vue/dist/vue.runtime.esm.js",
|
||||
},
|
||||
extensions: ["*", ".js", ".vue", ".json"],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user