mirror of
https://github.com/moghtech/komodo.git
synced 2026-03-12 10:33:16 -05:00
Compare commits
2430 Commits
v0.2.7
...
v2.0.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15eaeab68d | ||
|
|
89ea5ad5a4 | ||
|
|
c05dde3678 | ||
|
|
a17a1005c7 | ||
|
|
d13314eb98 | ||
|
|
f41a61116a | ||
|
|
13fbcae105 | ||
|
|
71c437962f | ||
|
|
e4147ccdaf | ||
|
|
5089375211 | ||
|
|
e489812af4 | ||
|
|
7373ff8d0e | ||
|
|
4e1cd32f3f | ||
|
|
57211746f1 | ||
|
|
5e153fb02b | ||
|
|
b28a413005 | ||
|
|
691671abbd | ||
|
|
504e81c2f7 | ||
|
|
0a479a0f4a | ||
|
|
6a05779ceb | ||
|
|
acd27ba058 | ||
|
|
96c4ae9fc5 | ||
|
|
5c99958cba | ||
|
|
7288f067c5 | ||
|
|
5d0f7de9fb | ||
|
|
4129abcfd0 | ||
|
|
4275e903eb | ||
|
|
c57c321cbb | ||
|
|
9a7d49b35e | ||
|
|
d6aaef24cd | ||
|
|
3dce84c1f5 | ||
|
|
8b16f97caf | ||
|
|
c98a809db4 | ||
|
|
c066b4fe16 | ||
|
|
b54fa235bc | ||
|
|
05fd1aba2c | ||
|
|
1a5290e124 | ||
|
|
538e11ef3b | ||
|
|
9208e9bf86 | ||
|
|
31640e3df8 | ||
|
|
42751d1b90 | ||
|
|
673ab5c17d | ||
|
|
ad00f38758 | ||
|
|
b2459b5a64 | ||
|
|
5923dd0e22 | ||
|
|
51294edd73 | ||
|
|
baf77bf701 | ||
|
|
ddd99dc85d | ||
|
|
a4c902282d | ||
|
|
d7a2d4aadc | ||
|
|
c930468811 | ||
|
|
27efc81451 | ||
|
|
e44bd324f7 | ||
|
|
b64c7e7289 | ||
|
|
5c1a17fc91 | ||
|
|
50c3d77aed | ||
|
|
bba1e3ae0c | ||
|
|
fc26fede66 | ||
|
|
9cee723a1a | ||
|
|
b922e86d29 | ||
|
|
85dd3c0fe9 | ||
|
|
4e3ecf7e6c | ||
|
|
18ffe635c8 | ||
|
|
8758c9ff8b | ||
|
|
5da48ece82 | ||
|
|
9a48edc43f | ||
|
|
93430f6dc3 | ||
|
|
d893ae9241 | ||
|
|
24ed3cfcb1 | ||
|
|
36ee7ed72e | ||
|
|
5895fb843e | ||
|
|
98f53020fb | ||
|
|
1d10873e77 | ||
|
|
f5ee42029f | ||
|
|
525402f6f2 | ||
|
|
9f877cc188 | ||
|
|
69d6b8e6ec | ||
|
|
526bcc1907 | ||
|
|
f7badf5579 | ||
|
|
86fd7a155d | ||
|
|
628afc3baa | ||
|
|
04f944fcc2 | ||
|
|
f39447b513 | ||
|
|
612acd02ad | ||
|
|
90557ae514 | ||
|
|
0c292f7157 | ||
|
|
673ee05fe9 | ||
|
|
94b5bd6660 | ||
|
|
be40110004 | ||
|
|
2d4a34b820 | ||
|
|
2feb08725b | ||
|
|
161393805f | ||
|
|
f4b692080d | ||
|
|
e1c6c866a2 | ||
|
|
7106328390 | ||
|
|
338acba5c6 | ||
|
|
dcf6f2431e | ||
|
|
58b520e651 | ||
|
|
8be0846733 | ||
|
|
5b651f2527 | ||
|
|
026ba4fca8 | ||
|
|
4bca07d4ba | ||
|
|
9091f14711 | ||
|
|
8b78bb838e | ||
|
|
d384f7ca78 | ||
|
|
48475b55d5 | ||
|
|
64d7959c75 | ||
|
|
d1856c2325 | ||
|
|
61b7fcdb0a | ||
|
|
2693f13829 | ||
|
|
7fea06861b | ||
|
|
abd155193a | ||
|
|
7a4de0343d | ||
|
|
c638e3876e | ||
|
|
fc4ea40b9c | ||
|
|
62b84328d8 | ||
|
|
a5ee0de394 | ||
|
|
bf0ab26d22 | ||
|
|
0dd2ade574 | ||
|
|
f0172c2721 | ||
|
|
363d330868 | ||
|
|
2843711796 | ||
|
|
4f044ccde8 | ||
|
|
9be21b54bd | ||
|
|
057ecbcfef | ||
|
|
e91d63f262 | ||
|
|
3b799edb30 | ||
|
|
41c7faf48d | ||
|
|
e1d745aee0 | ||
|
|
01df3add41 | ||
|
|
577973841b | ||
|
|
3e5311f413 | ||
|
|
46e99783f2 | ||
|
|
a6590804a7 | ||
|
|
bd752b7c72 | ||
|
|
de83b3a5a0 | ||
|
|
c91963fdbb | ||
|
|
973aae82fd | ||
|
|
c3626e0949 | ||
|
|
5ba96a58e4 | ||
|
|
c6a1cf2867 | ||
|
|
bd39aec1fd | ||
|
|
079895a64d | ||
|
|
5af1c646a5 | ||
|
|
747e03b5c4 | ||
|
|
78c6761eca | ||
|
|
a949891d26 | ||
|
|
9d256e420a | ||
|
|
b460504b5a | ||
|
|
e60316f2c1 | ||
|
|
c45e3eab16 | ||
|
|
ec12f63272 | ||
|
|
f2f77b4eb6 | ||
|
|
677a65f8b0 | ||
|
|
a5a1090177 | ||
|
|
ccb18fc88a | ||
|
|
0ca583fe5e | ||
|
|
4013cd3f1e | ||
|
|
5854a7de99 | ||
|
|
8c52232852 | ||
|
|
fb9e5b1860 | ||
|
|
a1c00a8d32 | ||
|
|
775f5bf703 | ||
|
|
effe737ffe | ||
|
|
436959cbcd | ||
|
|
9957287443 | ||
|
|
c77de8683c | ||
|
|
087bcb7044 | ||
|
|
0dfcaaf06a | ||
|
|
6624a821a5 | ||
|
|
c32af84e02 | ||
|
|
9d9add7b34 | ||
|
|
db099dbe1e | ||
|
|
413a8abc84 | ||
|
|
36243e83c1 | ||
|
|
076113a1de | ||
|
|
c998edaa73 | ||
|
|
4de32b08e5 | ||
|
|
262999c58f | ||
|
|
be57f52e6e | ||
|
|
3591789316 | ||
|
|
83f2bd65fe | ||
|
|
5e5cdb81f8 | ||
|
|
721038a1df | ||
|
|
7de98ad519 | ||
|
|
8c62f2b5c5 | ||
|
|
85787781ee | ||
|
|
25e2d4e926 | ||
|
|
b9b8d45cbc | ||
|
|
2f2e863dbf | ||
|
|
a3a01f1625 | ||
|
|
aec8fa2bf1 | ||
|
|
7ff2dba82f | ||
|
|
9c86b2d239 | ||
|
|
b1fec7c663 | ||
|
|
8341c6b802 | ||
|
|
73285c4374 | ||
|
|
32d48cdb02 | ||
|
|
8e081fd09c | ||
|
|
04531f1dea | ||
|
|
80f439d472 | ||
|
|
d5e03d6d16 | ||
|
|
a9f55bb8e6 | ||
|
|
9e765f93f5 | ||
|
|
b3aa8e906f | ||
|
|
03fe442aa0 | ||
|
|
d268009a6a | ||
|
|
f0697e812a | ||
|
|
78766463d6 | ||
|
|
0fa1edba2c | ||
|
|
bbd968cac3 | ||
|
|
5f24fc1be3 | ||
|
|
7ecd2b0b0b | ||
|
|
7bf44d2e04 | ||
|
|
24e0672384 | ||
|
|
04f081631f | ||
|
|
b1af956b63 | ||
|
|
370712b29f | ||
|
|
2b6c552964 | ||
|
|
434a1d8ea9 | ||
|
|
0b7f28360f | ||
|
|
3c8ef0ab29 | ||
|
|
930b2423c3 | ||
|
|
546747b5f2 | ||
|
|
c6df866755 | ||
|
|
ea5e684915 | ||
|
|
64db8933de | ||
|
|
7a5580de57 | ||
|
|
b1656bb174 | ||
|
|
559ce103da | ||
|
|
75e278a57b | ||
|
|
430f3ddc34 | ||
|
|
6c30c202e9 | ||
|
|
c5401de1c5 | ||
|
|
7a3d9e0ef6 | ||
|
|
595e3ece42 | ||
|
|
a3bc895755 | ||
|
|
3e3def03ec | ||
|
|
bc672d9649 | ||
|
|
ea6dee4d51 | ||
|
|
b985f18c74 | ||
|
|
45909b2f04 | ||
|
|
2b5a54ce89 | ||
|
|
a18f33b95e | ||
|
|
f35b00ea95 | ||
|
|
70fab08520 | ||
|
|
0331780a5f | ||
|
|
06cdfd2bbc | ||
|
|
1555202569 | ||
|
|
5139622aad | ||
|
|
61ce2ee3db | ||
|
|
3171c14f2b | ||
|
|
521db748d8 | ||
|
|
35bf224080 | ||
|
|
e0b31cfe51 | ||
|
|
0a890078b0 | ||
|
|
df97ced7a4 | ||
|
|
d4e5e2e6d8 | ||
|
|
19aa60dcb5 | ||
|
|
fc19c53e6f | ||
|
|
4f0af960db | ||
|
|
e2ec5258fb | ||
|
|
49b6545a02 | ||
|
|
0aabaa9e62 | ||
|
|
dc65986eab | ||
|
|
1d8f28437d | ||
|
|
c1502e89c2 | ||
|
|
0bd15fc442 | ||
|
|
5a3621b02e | ||
|
|
38192e2dac | ||
|
|
5d271d5547 | ||
|
|
11fb67a35b | ||
|
|
a80499dcc4 | ||
|
|
8c76b8487f | ||
|
|
2b32d9042a | ||
|
|
dc48f1f2ca | ||
|
|
8e7b7bdcf1 | ||
|
|
f11d64f72e | ||
|
|
2ffae85180 | ||
|
|
bd79d0f1e0 | ||
|
|
e890b1f675 | ||
|
|
3b7de25c30 | ||
|
|
793bb99f31 | ||
|
|
d465c9f273 | ||
|
|
ce641a8974 | ||
|
|
1b89ceb122 | ||
|
|
2dbc011d26 | ||
|
|
246da88ae1 | ||
|
|
a8c16f64b1 | ||
|
|
a5b711a348 | ||
|
|
9666e9ad83 | ||
|
|
7479640c73 | ||
|
|
4823825035 | ||
|
|
23897a7acf | ||
|
|
20d5588b5c | ||
|
|
f7e15ccde5 | ||
|
|
cf7623b1fc | ||
|
|
d3c464c05d | ||
|
|
5c9d416aa4 | ||
|
|
aabcd88312 | ||
|
|
9d2624c6bc | ||
|
|
ee11fb0b6c | ||
|
|
45adfbddd0 | ||
|
|
d26d035dc6 | ||
|
|
e673ba0adf | ||
|
|
f876facfa7 | ||
|
|
3a47d57478 | ||
|
|
a707028277 | ||
|
|
0c6276c677 | ||
|
|
fc9c6706f1 | ||
|
|
7674269ce9 | ||
|
|
3b511c5adc | ||
|
|
87221a10e9 | ||
|
|
450cb6a148 | ||
|
|
f252cefb21 | ||
|
|
7855e9d688 | ||
|
|
feb263c15f | ||
|
|
4f8d1c22cc | ||
|
|
60bd47834e | ||
|
|
4d632a6b61 | ||
|
|
381dd76723 | ||
|
|
077e28a5fe | ||
|
|
6b02aaed7d | ||
|
|
e466944c05 | ||
|
|
8ff94b7465 | ||
|
|
b17df5ed7b | ||
|
|
207dc30206 | ||
|
|
c3eb386bdb | ||
|
|
4279e46892 | ||
|
|
8d3d2fee12 | ||
|
|
1df36c4266 | ||
|
|
36f7ad33c7 | ||
|
|
ec34b2c139 | ||
|
|
d14c28d1f2 | ||
|
|
68f7a0e9ce | ||
|
|
50f0376f0a | ||
|
|
bbd53747ad | ||
|
|
6a2adf1f83 | ||
|
|
128b15b94f | ||
|
|
8d74b377b7 | ||
|
|
d7e972e5c6 | ||
|
|
e5cb4aac5a | ||
|
|
d0f62f8326 | ||
|
|
47c4091a4b | ||
|
|
973480e2b3 | ||
|
|
b9e1cc87d2 | ||
|
|
05d20c8603 | ||
|
|
fe2d68a001 | ||
|
|
26fd5b2a6d | ||
|
|
76457bcb61 | ||
|
|
ebd2c2238d | ||
|
|
b7fc1bef7b | ||
|
|
50b9f2e1bf | ||
|
|
41ce86f6ab | ||
|
|
7a21c01e52 | ||
|
|
e63e282510 | ||
|
|
5456b36c18 | ||
|
|
fcfb58a7e9 | ||
|
|
2203004a74 | ||
|
|
996fb49823 | ||
|
|
35d22c77a2 | ||
|
|
44ab89600f | ||
|
|
0900e48cb8 | ||
|
|
c530a46a27 | ||
|
|
f69c8db3ea | ||
|
|
48f2f651e1 | ||
|
|
bdb5b4185e | ||
|
|
42a7b8c19b | ||
|
|
ded17e4840 | ||
|
|
80fb1e6889 | ||
|
|
1dc861f538 | ||
|
|
3da63395fd | ||
|
|
c40cbc4d77 | ||
|
|
05e352e88c | ||
|
|
5884c09fb8 | ||
|
|
f8add38043 | ||
|
|
501f734e8b | ||
|
|
de62732ac8 | ||
|
|
bfa61058cd | ||
|
|
72ca6d9910 | ||
|
|
4d1ac32ad3 | ||
|
|
927e5959fa | ||
|
|
37ccc6e1ef | ||
|
|
deaa8754f3 | ||
|
|
dd8ac67c72 | ||
|
|
be4457c9cf | ||
|
|
1868421815 | ||
|
|
366f7a12b4 | ||
|
|
75119370df | ||
|
|
9e85b9d4c8 | ||
|
|
8afbbf23dc | ||
|
|
770a1116a1 | ||
|
|
0b4aebbc24 | ||
|
|
f1696e26e4 | ||
|
|
1a7b682301 | ||
|
|
b0110b05aa | ||
|
|
561b490f26 | ||
|
|
cac1f0b42e | ||
|
|
28886fb304 | ||
|
|
fb84d4cf7d | ||
|
|
31e9624556 | ||
|
|
3864bb7115 | ||
|
|
cea8601246 | ||
|
|
a546364bf3 | ||
|
|
c8c62ea562 | ||
|
|
845e8780c7 | ||
|
|
db60347566 | ||
|
|
c3ea0239d6 | ||
|
|
e9d13449bf | ||
|
|
2daa92a639 | ||
|
|
6473080078 | ||
|
|
d3957f65dc | ||
|
|
cb34969f1e | ||
|
|
55a0a8cd05 | ||
|
|
89f08372c6 | ||
|
|
6a3ce2d426 | ||
|
|
4928378d46 | ||
|
|
eea222cfba | ||
|
|
6e9cc2dc77 | ||
|
|
55d45084d0 | ||
|
|
9657a44049 | ||
|
|
51fa9ae3c2 | ||
|
|
5fd256444e | ||
|
|
059716f178 | ||
|
|
0bee1fe2c5 | ||
|
|
1e58c1a958 | ||
|
|
ed1431db0a | ||
|
|
dc769ff159 | ||
|
|
098f23ac4c | ||
|
|
03f577d22f | ||
|
|
95ca217362 | ||
|
|
6d61045764 | ||
|
|
34e075eaf3 | ||
|
|
232dc0bb4e | ||
|
|
0cc0ee2aab | ||
|
|
edebe925ff | ||
|
|
5fd45bbc7b | ||
|
|
0a490dadb2 | ||
|
|
23847c15bc | ||
|
|
0d238aee4f | ||
|
|
98ad6cf5fa | ||
|
|
e35b81630b | ||
|
|
1215852fe4 | ||
|
|
4164b76ff5 | ||
|
|
26a9daffeb | ||
|
|
8bb9f16e9b | ||
|
|
b6eaf76497 | ||
|
|
073893da0e | ||
|
|
e71547f1c2 | ||
|
|
1991627990 | ||
|
|
3434d827a3 | ||
|
|
1ef8b9878a | ||
|
|
07ddaa8377 | ||
|
|
142c08cde4 | ||
|
|
1aa1422faa | ||
|
|
1394e8a6b1 | ||
|
|
420ee10211 | ||
|
|
e918461dc5 | ||
|
|
4dc9ca27be | ||
|
|
f49b186f2f | ||
|
|
6e039b41f1 | ||
|
|
e7cd77b022 | ||
|
|
556cbd04c7 | ||
|
|
4e3d181466 | ||
|
|
5d4326f46f | ||
|
|
4bb486ad0a | ||
|
|
d29c5112d8 | ||
|
|
d41315b8a4 | ||
|
|
847404388c | ||
|
|
eef8ec59b8 | ||
|
|
9eb32f9ff5 | ||
|
|
859bfe67ef | ||
|
|
21ea469cd4 | ||
|
|
7fb902b892 | ||
|
|
c9c4ac47ee | ||
|
|
f228cd31f3 | ||
|
|
4feecb4b97 | ||
|
|
e2680d0942 | ||
|
|
7422c0730d | ||
|
|
37ac0dc7e3 | ||
|
|
dccaca1df4 | ||
|
|
886aea4c36 | ||
|
|
cbca070bae | ||
|
|
b4bdd401f6 | ||
|
|
e546166240 | ||
|
|
21689ce0ad | ||
|
|
941787db64 | ||
|
|
d4b1aacac3 | ||
|
|
30f89461bf | ||
|
|
a42d1397e9 | ||
|
|
b29313c28f | ||
|
|
08a246a90c | ||
|
|
1a08df28d0 | ||
|
|
a226ffc256 | ||
|
|
b385ee5ec3 | ||
|
|
c78c34357d | ||
|
|
4b7c692f00 | ||
|
|
1ac98a096e | ||
|
|
281a2dc1ce | ||
|
|
0fe91378a6 | ||
|
|
11e76d1cf2 | ||
|
|
a3bcd71105 | ||
|
|
3ecc56dd76 | ||
|
|
7239cbb19b | ||
|
|
a0540f7011 | ||
|
|
37aea7605e | ||
|
|
78be913541 | ||
|
|
c34f5ebf49 | ||
|
|
e5822cefb8 | ||
|
|
4baab194cf | ||
|
|
a896583da6 | ||
|
|
7b2674c38b | ||
|
|
d1e32989e3 | ||
|
|
e802bb3882 | ||
|
|
27a38b1bf5 | ||
|
|
2bc8a754be | ||
|
|
7a2a54bec1 | ||
|
|
6a15150d59 | ||
|
|
1b1dca76da | ||
|
|
a032f0f4ff | ||
|
|
2749d49435 | ||
|
|
d88e42ef2d | ||
|
|
a370e7d121 | ||
|
|
d139ad2b3d | ||
|
|
8d2d180398 | ||
|
|
37ca4ca986 | ||
|
|
33e73b8543 | ||
|
|
cf6e36e90c | ||
|
|
9eb8b32f4a | ||
|
|
b400add6f1 | ||
|
|
24adb89d25 | ||
|
|
4674b2badb | ||
|
|
65d1a69cb9 | ||
|
|
0da5718991 | ||
|
|
6b26cd120c | ||
|
|
28e1bb19a4 | ||
|
|
166107ac07 | ||
|
|
d77201880f | ||
|
|
1d7629e9b2 | ||
|
|
198f690ca5 | ||
|
|
531c79a144 | ||
|
|
d685862713 | ||
|
|
af0f245b5b | ||
|
|
cba36861b7 | ||
|
|
2c2c1d47b4 | ||
|
|
3a6b997241 | ||
|
|
7122f79b9d | ||
|
|
9bcee8122b | ||
|
|
a49c98946e | ||
|
|
7d222a7241 | ||
|
|
33501dac3e | ||
|
|
4675dfa736 | ||
|
|
0be51dc784 | ||
|
|
52453d1320 | ||
|
|
25da97ac1a | ||
|
|
02db5a11d3 | ||
|
|
89a5272246 | ||
|
|
ae51ea1ad6 | ||
|
|
3bdb4bea16 | ||
|
|
677bb14b5d | ||
|
|
6700700a80 | ||
|
|
996d4aa129 | ||
|
|
75894a7282 | ||
|
|
2a065edcf1 | ||
|
|
6f3703acfb | ||
|
|
59e989ecdf | ||
|
|
951ff34a9e | ||
|
|
2d83105500 | ||
|
|
3d455f5142 | ||
|
|
01de8c4a9b | ||
|
|
d5de338561 | ||
|
|
58c1afb8ef | ||
|
|
230f357b5a | ||
|
|
991c95fff0 | ||
|
|
f6243fe6b1 | ||
|
|
9feeccba6e | ||
|
|
673c7f3a6b | ||
|
|
39f900d651 | ||
|
|
8a06a0d6ce | ||
|
|
7789ee4f4a | ||
|
|
0472b6a7f7 | ||
|
|
d1d2227d36 | ||
|
|
cea7c5fc5e | ||
|
|
34a9f8eb9e | ||
|
|
494d01aeed | ||
|
|
084e2fec23 | ||
|
|
98d72fc908 | ||
|
|
20ac04fae5 | ||
|
|
a65fd4dca7 | ||
|
|
0873104b5a | ||
|
|
9a7b6ebd51 | ||
|
|
a4153fa28b | ||
|
|
e732da3b05 | ||
|
|
75ffbd559b | ||
|
|
cae80b43e5 | ||
|
|
d924a8ace4 | ||
|
|
dcfad5dc4e | ||
|
|
134d1697e9 | ||
|
|
3094d0036a | ||
|
|
ee5fd55cdb | ||
|
|
0ca126ff23 | ||
|
|
2fa9d9ecce | ||
|
|
118ae9b92c | ||
|
|
2205a81e79 | ||
|
|
e2280f38df | ||
|
|
545196d7eb | ||
|
|
23f8ecc1d9 | ||
|
|
4d401d7f20 | ||
|
|
4165e25332 | ||
|
|
4cc0817b0f | ||
|
|
51cf1e2b05 | ||
|
|
5309c70929 | ||
|
|
1278c62859 | ||
|
|
6d6acdbc0b | ||
|
|
d22000331e | ||
|
|
31034e5b34 | ||
|
|
a43e1f3f52 | ||
|
|
7a3b2b542d | ||
|
|
8d516d6d5f | ||
|
|
3e0d1befbd | ||
|
|
5dc609b206 | ||
|
|
f1127007c3 | ||
|
|
765e5a0df1 | ||
|
|
76f2f61be5 | ||
|
|
b43e2918da | ||
|
|
f45205011e | ||
|
|
5b211fb8f0 | ||
|
|
3f04614303 | ||
|
|
4c3210f70a | ||
|
|
2c37ac26a5 | ||
|
|
db1cf786ac | ||
|
|
9c841e5bdc | ||
|
|
e385c6e722 | ||
|
|
9ef25e7575 | ||
|
|
f945a3014a | ||
|
|
fdad04d6cb | ||
|
|
c914f23aa8 | ||
|
|
82b2e68cd3 | ||
|
|
e274d6f7c8 | ||
|
|
ab8777460d | ||
|
|
7e030e702f | ||
|
|
a869a74002 | ||
|
|
1d31110f8c | ||
|
|
bb63892e10 | ||
|
|
4e554eb2a7 | ||
|
|
00968b6ea1 | ||
|
|
a8050db5f6 | ||
|
|
bf0a972ec2 | ||
|
|
23c1a08c87 | ||
|
|
2b6b8a21ec | ||
|
|
02974b9adb | ||
|
|
64d13666a9 | ||
|
|
2b2f354a3c | ||
|
|
aea5441466 | ||
|
|
97ced3b2cb | ||
|
|
1f79987c58 | ||
|
|
e859a919c5 | ||
|
|
2a1270dd74 | ||
|
|
f5a59b0333 | ||
|
|
cacea235f9 | ||
|
|
54ba31dca9 | ||
|
|
17d7ecb419 | ||
|
|
38f3448790 | ||
|
|
ec88a6fa5a | ||
|
|
3820cd0ca2 | ||
|
|
419aa87bbb | ||
|
|
7a9ad42203 | ||
|
|
3f1cfa9064 | ||
|
|
d05c81864e | ||
|
|
f1a09f34ab | ||
|
|
23c6e6306d | ||
|
|
800da90561 | ||
|
|
b24bf6ed89 | ||
|
|
d66a781a13 | ||
|
|
f9b2994d44 | ||
|
|
c0d6d96b64 | ||
|
|
34496b948a | ||
|
|
90c6adf923 | ||
|
|
3b72dc65cc | ||
|
|
05f38d02be | ||
|
|
ea5506c202 | ||
|
|
64b0a5c9d2 | ||
|
|
93cc6a3a6e | ||
|
|
7ae69cf33b | ||
|
|
404e00cc64 | ||
|
|
6fe5bc7420 | ||
|
|
82324b00ee | ||
|
|
5daba3a557 | ||
|
|
020cdc06fd | ||
|
|
cb270f4dff | ||
|
|
21666cf9b3 | ||
|
|
a417926690 | ||
|
|
293b36fae4 | ||
|
|
dca37e9ba8 | ||
|
|
1cc302fcbf | ||
|
|
febcf739d0 | ||
|
|
cb79e00794 | ||
|
|
869b397596 | ||
|
|
41d1ff9760 | ||
|
|
dfafadf57b | ||
|
|
538a79b8b5 | ||
|
|
5088dc5c3c | ||
|
|
581d7e0b2c | ||
|
|
657298041f | ||
|
|
d71e9dca11 | ||
|
|
165131bdf8 | ||
|
|
0a81d2a0d0 | ||
|
|
44ab5eb804 | ||
|
|
e3d8e603ec | ||
|
|
8b5c179473 | ||
|
|
8582bc92da | ||
|
|
8ee270d045 | ||
|
|
2cfae525e9 | ||
|
|
80e5d2a972 | ||
|
|
6f22c011a6 | ||
|
|
401cccee79 | ||
|
|
654b923f98 | ||
|
|
61261be70f | ||
|
|
46418125e3 | ||
|
|
e029e94f0d | ||
|
|
3be2b5163b | ||
|
|
6a145f58ff | ||
|
|
f1cede2ebd | ||
|
|
a5cfa1d412 | ||
|
|
a0674654c1 | ||
|
|
3faa1c58c1 | ||
|
|
7e296f34af | ||
|
|
9f8ced190c | ||
|
|
c194bb16d8 | ||
|
|
39fec9b55e | ||
|
|
e97ed9888d | ||
|
|
559102ffe3 | ||
|
|
6bf80ddcc7 | ||
|
|
89dbe1b4d9 | ||
|
|
334e16d646 | ||
|
|
a7bbe519f4 | ||
|
|
5827486c5a | ||
|
|
8ca8f7eddd | ||
|
|
0600276b43 | ||
|
|
a77a1495c7 | ||
|
|
021ed5d15f | ||
|
|
7d4376f426 | ||
|
|
7e9b406a34 | ||
|
|
dcf78b05b3 | ||
|
|
3236302d05 | ||
|
|
fc41258d6c | ||
|
|
ae8df90361 | ||
|
|
7d05b2677f | ||
|
|
2f55468a4c | ||
|
|
a20bd2c23f | ||
|
|
b3aa0ffa78 | ||
|
|
8e58a283cd | ||
|
|
9b2d9932ef | ||
|
|
7cb093ade1 | ||
|
|
e2f73d8474 | ||
|
|
12abd5a5bd | ||
|
|
f349cdf50d | ||
|
|
796bcac952 | ||
|
|
fed05684aa | ||
|
|
80a91584a8 | ||
|
|
12d05e9a25 | ||
|
|
f4d06c91ff | ||
|
|
5d7449529f | ||
|
|
a0021d1785 | ||
|
|
bbd23e3f5f | ||
|
|
71841a8e41 | ||
|
|
5228ffd9b8 | ||
|
|
a06f506e54 | ||
|
|
71d6a55e50 | ||
|
|
d16c03dd2a | ||
|
|
6abd9a6554 | ||
|
|
5f04e881a5 | ||
|
|
5fc0a87dea | ||
|
|
2463ed3879 | ||
|
|
a2758ce6f4 | ||
|
|
3f1788dbbb | ||
|
|
33a0560af6 | ||
|
|
610a10c488 | ||
|
|
39b217687d | ||
|
|
2f73461979 | ||
|
|
aae9bb9e51 | ||
|
|
7d011d93fa | ||
|
|
bffdea4357 | ||
|
|
790566bf79 | ||
|
|
b17db93f13 | ||
|
|
daa2ea9361 | ||
|
|
176fb04707 | ||
|
|
5ba1254cdb | ||
|
|
43593162b0 | ||
|
|
418f359492 | ||
|
|
3cded60166 | ||
|
|
6f70f9acb0 | ||
|
|
6e1064e58e | ||
|
|
d96e5b4c46 | ||
|
|
5a8822c7d2 | ||
|
|
1f2d236228 | ||
|
|
a89bd4a36d | ||
|
|
0b40dff72b | ||
|
|
59874f0a92 | ||
|
|
14e459b32e | ||
|
|
f6c55b7be1 | ||
|
|
460819a145 | ||
|
|
91f4df8ac2 | ||
|
|
6a19e18539 | ||
|
|
30c5fa3569 | ||
|
|
4b6aa1d73d | ||
|
|
5dfd007580 | ||
|
|
955670d979 | ||
|
|
f70e359f14 | ||
|
|
a2b0981f76 | ||
|
|
49a8e581bf | ||
|
|
2d0c1724db | ||
|
|
20ae1c22d7 | ||
|
|
e8d75b2a3d | ||
|
|
e23d68f86a | ||
|
|
2111976450 | ||
|
|
8a0109522b | ||
|
|
8d75fa3f2f | ||
|
|
197e938346 | ||
|
|
6ba0184551 | ||
|
|
c456b67018 | ||
|
|
02e152af4d | ||
|
|
392e691f92 | ||
|
|
495e208ccd | ||
|
|
14474adb90 | ||
|
|
896784e2e3 | ||
|
|
2e690bce24 | ||
|
|
7172d24512 | ||
|
|
b754c89118 | ||
|
|
31a23dfe2d | ||
|
|
b0f80cafc3 | ||
|
|
85a16f6c6f | ||
|
|
29a7e4c27b | ||
|
|
a73b572725 | ||
|
|
aa44bf04e8 | ||
|
|
93348621c5 | ||
|
|
4b2139ede2 | ||
|
|
3251216be7 | ||
|
|
1f980a45e8 | ||
|
|
94da1dce99 | ||
|
|
d4fc015494 | ||
|
|
5800fc91d2 | ||
|
|
91785e1e8f | ||
|
|
41fccdb16e | ||
|
|
78cf93da8a | ||
|
|
ea36549dbe | ||
|
|
a319095869 | ||
|
|
a6d7a80cbc | ||
|
|
20f051c890 | ||
|
|
2fef954ad5 | ||
|
|
e1b9367ee3 | ||
|
|
c7717fbfdf | ||
|
|
bf918042c3 | ||
|
|
46ac16100d | ||
|
|
eca0378c56 | ||
|
|
bfd5c5390d | ||
|
|
db41878278 | ||
|
|
26468ed8ea | ||
|
|
707751708d | ||
|
|
d28d3422a3 | ||
|
|
9e2b1ede93 | ||
|
|
37e37deb04 | ||
|
|
e73a6ca72c | ||
|
|
6082b7b1bd | ||
|
|
678767c24b | ||
|
|
59cb86d599 | ||
|
|
5f0a9ad652 | ||
|
|
fc758121da | ||
|
|
95ccf1af0b | ||
|
|
627f7ab585 | ||
|
|
4238abf61a | ||
|
|
66bfe69983 | ||
|
|
42b493ae10 | ||
|
|
f4d6c50b67 | ||
|
|
17176a7d56 | ||
|
|
140b95b70c | ||
|
|
3a2cb73088 | ||
|
|
4585533bc5 | ||
|
|
83099f03a1 | ||
|
|
9e619c0250 | ||
|
|
edf49dc685 | ||
|
|
beffc8c159 | ||
|
|
d99cf87da0 | ||
|
|
8e19eb7b0f | ||
|
|
78a0b56c73 | ||
|
|
bf5dc52237 | ||
|
|
482ea59d4c | ||
|
|
7740d36f49 | ||
|
|
820754deda | ||
|
|
4219884198 | ||
|
|
d9e24cc35a | ||
|
|
8d2ce884d9 | ||
|
|
313b000e64 | ||
|
|
c2f9e29605 | ||
|
|
8c6f38cafb | ||
|
|
4a03eba99a | ||
|
|
79fe078e3b | ||
|
|
6be032fcd4 | ||
|
|
d0c94278ec | ||
|
|
03ae7268fd | ||
|
|
f443294818 | ||
|
|
2202835d86 | ||
|
|
98fbc7a506 | ||
|
|
8ee89296e1 | ||
|
|
989c3d2d01 | ||
|
|
dc72883b90 | ||
|
|
e99364430f | ||
|
|
e106e38cd9 | ||
|
|
e4d0c56e49 | ||
|
|
7427a158f4 | ||
|
|
b926f89954 | ||
|
|
e666a22f08 | ||
|
|
4107f779a5 | ||
|
|
828d6cdfed | ||
|
|
fe82400a99 | ||
|
|
e37fc6adde | ||
|
|
c21c8f99ae | ||
|
|
78a63f92bb | ||
|
|
ce67655021 | ||
|
|
2ccecf38f2 | ||
|
|
1ddae31aad | ||
|
|
097fbefa63 | ||
|
|
b51442a661 | ||
|
|
a21d49d224 | ||
|
|
c99a33880e | ||
|
|
6ee55262ba | ||
|
|
878b9b55bb | ||
|
|
af6193f83a | ||
|
|
b8fefddd8b | ||
|
|
7f490f5bf2 | ||
|
|
efa7c13286 | ||
|
|
f913be7a0b | ||
|
|
35901ef7ea | ||
|
|
5b938490fc | ||
|
|
a7326a0116 | ||
|
|
877bda91d7 | ||
|
|
439a091e50 | ||
|
|
b0e89f4963 | ||
|
|
b1e4b55ba1 | ||
|
|
d4a1891c70 | ||
|
|
9db7592d7e | ||
|
|
84fb603951 | ||
|
|
55bac0dd13 | ||
|
|
b143f42363 | ||
|
|
007efd136a | ||
|
|
b329767f9e | ||
|
|
b4231957d5 | ||
|
|
b4dc446f95 | ||
|
|
c92515cecc | ||
|
|
f3712feea2 | ||
|
|
0e81d17860 | ||
|
|
c3f1557b83 | ||
|
|
5f88e4b436 | ||
|
|
473c6b3867 | ||
|
|
c10edaa5d1 | ||
|
|
9418a6d963 | ||
|
|
57646b750f | ||
|
|
0d57f9411c | ||
|
|
7d396dd539 | ||
|
|
bfe762b71a | ||
|
|
16ede84bac | ||
|
|
4524db94db | ||
|
|
580dab4acd | ||
|
|
645382856a | ||
|
|
5c4e6a6dbb | ||
|
|
66810e1efb | ||
|
|
69a84882f0 | ||
|
|
41648436a5 | ||
|
|
083a88aa7b | ||
|
|
750f95c90d | ||
|
|
129f3ecd82 | ||
|
|
1b754f80ab | ||
|
|
968a882012 | ||
|
|
696ebdb26f | ||
|
|
8fee04607d | ||
|
|
6fe250244b | ||
|
|
b530af0eec | ||
|
|
21e9361079 | ||
|
|
524d2d956b | ||
|
|
aca9633941 | ||
|
|
32e1bd2dda | ||
|
|
cb363d1559 | ||
|
|
63eb74b9c8 | ||
|
|
bbcc27704f | ||
|
|
0aa9513dd0 | ||
|
|
26b216b478 | ||
|
|
166299bb57 | ||
|
|
03c47eb3dc | ||
|
|
1fcb4ad085 | ||
|
|
f51af8fbe1 | ||
|
|
4a975e1b92 | ||
|
|
ba556e3284 | ||
|
|
299a326942 | ||
|
|
a5d4b9aefb | ||
|
|
40b820ae42 | ||
|
|
7028bf2996 | ||
|
|
75ebd0e6c0 | ||
|
|
426153df66 | ||
|
|
5bd423a6a6 | ||
|
|
c24131d383 | ||
|
|
9f54b6c26a | ||
|
|
ab8ae51ece | ||
|
|
ef2a83ff16 | ||
|
|
7872771aee | ||
|
|
b12cf858d8 | ||
|
|
38dba91c3a | ||
|
|
ea8136aa57 | ||
|
|
f956e12e28 | ||
|
|
207ea52b95 | ||
|
|
caf28d3a26 | ||
|
|
8fff45649d | ||
|
|
de5df70e11 | ||
|
|
3df010ac2a | ||
|
|
2d3beb708e | ||
|
|
1dc22d01c4 | ||
|
|
eb029d0408 | ||
|
|
f926932181 | ||
|
|
cc96d80c6a | ||
|
|
144b49495c | ||
|
|
de9354bdc7 | ||
|
|
38bfee84d7 | ||
|
|
ec33d9fb9e | ||
|
|
0a66937b1d | ||
|
|
43cc0c3bc1 | ||
|
|
c14b395c70 | ||
|
|
7b8529a7c6 | ||
|
|
547c089581 | ||
|
|
4fe5e461b3 | ||
|
|
edfb873f7c | ||
|
|
5ef5294c44 | ||
|
|
5d3c50e04f | ||
|
|
f10efbb5ba | ||
|
|
39ce98161b | ||
|
|
cff6e79eee | ||
|
|
dedf22ede8 | ||
|
|
6955b92a99 | ||
|
|
5c63eeab02 | ||
|
|
4c14a4ae20 | ||
|
|
29fd856a2d | ||
|
|
195bdbd94a | ||
|
|
298ccd945c | ||
|
|
436e4e79e9 | ||
|
|
8b8c89d976 | ||
|
|
25c8d25636 | ||
|
|
ea242de2e4 | ||
|
|
be03547407 | ||
|
|
9c0d28b311 | ||
|
|
f269deb99c | ||
|
|
3df8163131 | ||
|
|
33a16a9bd2 | ||
|
|
215e7d1bdc | ||
|
|
25e0905c0c | ||
|
|
1c07ccea85 | ||
|
|
405ec1b8cc | ||
|
|
4f212bd06f | ||
|
|
074f4ea2db | ||
|
|
c9abccaf02 | ||
|
|
6428fa6de2 | ||
|
|
883f54431d | ||
|
|
28dc030e2b | ||
|
|
145d933e63 | ||
|
|
9772ca1a1c | ||
|
|
4059b69201 | ||
|
|
8e175ea5a1 | ||
|
|
d931b8b4e7 | ||
|
|
0982800ad2 | ||
|
|
4382ad0b3b | ||
|
|
e7891f7870 | ||
|
|
6bada46841 | ||
|
|
eae6cbd228 | ||
|
|
a0ee6180b2 | ||
|
|
3ce3de8768 | ||
|
|
6c46993b61 | ||
|
|
fbd9d14aaa | ||
|
|
1011ec60ab | ||
|
|
48e17a7c87 | ||
|
|
a94baded55 | ||
|
|
e97c0873cf | ||
|
|
43a0b76811 | ||
|
|
2d2577e5ee | ||
|
|
202ac77de3 | ||
|
|
568c963419 | ||
|
|
5c3294241d | ||
|
|
648a04be88 | ||
|
|
1b5822f649 | ||
|
|
c41a008603 | ||
|
|
603243b0eb | ||
|
|
d09ab36696 | ||
|
|
ad168c87f7 | ||
|
|
914f4c6197 | ||
|
|
c73d918e18 | ||
|
|
9d116f56cb | ||
|
|
8a8dede5db | ||
|
|
d2cecf316c | ||
|
|
cad1ee123e | ||
|
|
6aa801b705 | ||
|
|
078ba59002 | ||
|
|
5eacb7191b | ||
|
|
45eafd10b9 | ||
|
|
42c486807c | ||
|
|
8c31fcff02 | ||
|
|
49f1d40ce8 | ||
|
|
bf85e886bd | ||
|
|
eda0b233ca | ||
|
|
5efb227851 | ||
|
|
1a45fffe75 | ||
|
|
fa72f2e5ef | ||
|
|
c9152db300 | ||
|
|
25fcca7246 | ||
|
|
ac449e38d5 | ||
|
|
d6c66948ba | ||
|
|
b6af790aef | ||
|
|
36a49210a0 | ||
|
|
d2b2aa0550 | ||
|
|
7f4c883416 | ||
|
|
676fb3c732 | ||
|
|
17da4bd2fa | ||
|
|
b44e57bbf6 | ||
|
|
6aa5b5faae | ||
|
|
9565855477 | ||
|
|
3504c083b4 | ||
|
|
5fdaa9a808 | ||
|
|
ec35b14077 | ||
|
|
158f3ad89b | ||
|
|
7257ecbaed | ||
|
|
a2a94f23ee | ||
|
|
03cad5b23b | ||
|
|
2460b5edf7 | ||
|
|
83fdb180aa | ||
|
|
9b1d32ebdf | ||
|
|
ea4ae7651c | ||
|
|
5f6fabd925 | ||
|
|
38d9495ab1 | ||
|
|
46ad5b3953 | ||
|
|
e60b817208 | ||
|
|
0ce5248292 | ||
|
|
050c29f4a3 | ||
|
|
8580728933 | ||
|
|
3c5868d111 | ||
|
|
40e1b1ff88 | ||
|
|
99641b2e39 | ||
|
|
f0e7757eb4 | ||
|
|
f7283b1fc1 | ||
|
|
771af21eae | ||
|
|
0dda791ec7 | ||
|
|
bc76b1c07e | ||
|
|
8b537924fb | ||
|
|
f5ce3570e4 | ||
|
|
f1e51d275c | ||
|
|
eaa10d96b5 | ||
|
|
037364068d | ||
|
|
2441bc8cbf | ||
|
|
92ac003910 | ||
|
|
693f24763f | ||
|
|
d9d44ceee1 | ||
|
|
30ab8ed17b | ||
|
|
2bf2be54cc | ||
|
|
b7ea680958 | ||
|
|
2a56d09f89 | ||
|
|
2612f742b2 | ||
|
|
29bdf5c71d | ||
|
|
873d9ea433 | ||
|
|
717f3afa89 | ||
|
|
ec31d1af01 | ||
|
|
9e5c52b9a4 | ||
|
|
762873d5be | ||
|
|
67fa512975 | ||
|
|
502dd3a4a8 | ||
|
|
8c22bdd473 | ||
|
|
ba6801da11 | ||
|
|
309802093c | ||
|
|
3d1e3009b3 | ||
|
|
fdc23c2650 | ||
|
|
072ee6834e | ||
|
|
bedbf76349 | ||
|
|
e26d1211cc | ||
|
|
0342ee4dd9 | ||
|
|
669d5c81b4 | ||
|
|
defbab5955 | ||
|
|
9405295e4a | ||
|
|
28c077ed4c | ||
|
|
61406c1b00 | ||
|
|
64638730b9 | ||
|
|
c0942c6d1d | ||
|
|
ff964cd0fe | ||
|
|
d56f632a11 | ||
|
|
a7f22b6cfb | ||
|
|
6053fc1d99 | ||
|
|
573ff1863c | ||
|
|
dd4a9b0cb5 | ||
|
|
d243cf2da7 | ||
|
|
4e06e788ae | ||
|
|
a0f71f8af5 | ||
|
|
fcbb75d0c0 | ||
|
|
0a8419bb13 | ||
|
|
40fe76cf27 | ||
|
|
5594d3c1d9 | ||
|
|
b12aeb259f | ||
|
|
b121b0ac07 | ||
|
|
a9f1d91b1b | ||
|
|
abf48d0243 | ||
|
|
447690d8bf | ||
|
|
a70c0a2697 | ||
|
|
0758e6ff81 | ||
|
|
ea0e059ee1 | ||
|
|
c9e0524794 | ||
|
|
81ceaf1eae | ||
|
|
37c07ff748 | ||
|
|
62e8943ebe | ||
|
|
99ccffbc38 | ||
|
|
84dc29b77f | ||
|
|
81bab4aa50 | ||
|
|
9fa2fd0f58 | ||
|
|
3745967690 | ||
|
|
e8cfc13342 | ||
|
|
ec47bb11ee | ||
|
|
d008c95853 | ||
|
|
4986d70506 | ||
|
|
1372a5fb39 | ||
|
|
f54224650f | ||
|
|
2eee1459e7 | ||
|
|
5a3fd891c4 | ||
|
|
ba3f288c2d | ||
|
|
6d5fd7dc5d | ||
|
|
df3fd7c4e9 | ||
|
|
395f032ee2 | ||
|
|
de2bd800c4 | ||
|
|
75352a91ff | ||
|
|
9b12270d04 | ||
|
|
7fc378798f | ||
|
|
3db2c93303 | ||
|
|
150d6562bf | ||
|
|
c3b549b051 | ||
|
|
931f2bd92d | ||
|
|
6b6324d79c | ||
|
|
2c65d924f9 | ||
|
|
dd1fecf190 | ||
|
|
aa96a37db4 | ||
|
|
ec9e9638f5 | ||
|
|
e33019cab8 | ||
|
|
951cb82e0c | ||
|
|
0643f96053 | ||
|
|
56d835f2d2 | ||
|
|
d8fb8f8649 | ||
|
|
7197d628e5 | ||
|
|
96083178dd | ||
|
|
9d1b705ab1 | ||
|
|
2582bc9ba3 | ||
|
|
44f34b9b40 | ||
|
|
bbb18d8280 | ||
|
|
da95b7d074 | ||
|
|
6b25309aed | ||
|
|
f8e371af31 | ||
|
|
a0f5ae8c7f | ||
|
|
2f371af288 | ||
|
|
76840efddc | ||
|
|
8f01e441a4 | ||
|
|
41a6e0a65a | ||
|
|
40027f7430 | ||
|
|
a2c69aba87 | ||
|
|
a5d3fbedc6 | ||
|
|
b311b11785 | ||
|
|
7a0b29b387 | ||
|
|
d3a87fdb5f | ||
|
|
9b7ab6d98a | ||
|
|
c302e28d86 | ||
|
|
33be989e3a | ||
|
|
c9d65300c9 | ||
|
|
e96b676366 | ||
|
|
0bff4a5e51 | ||
|
|
9b12334922 | ||
|
|
68659630fc | ||
|
|
8b33647620 | ||
|
|
871aba62d5 | ||
|
|
c649094a8a | ||
|
|
e3c11db89e | ||
|
|
c43293109d | ||
|
|
d3e4f9f638 | ||
|
|
eecf583b0e | ||
|
|
a518806d8b | ||
|
|
8a4611c380 | ||
|
|
d679fbe72f | ||
|
|
1cf02bc4b4 | ||
|
|
bf703eef35 | ||
|
|
985058afb0 | ||
|
|
eac1145958 | ||
|
|
1b408d92d9 | ||
|
|
ee95c2c76b | ||
|
|
e83124ebff | ||
|
|
e912ae050a | ||
|
|
99253d6182 | ||
|
|
ef91577ac5 | ||
|
|
b97f9b30b3 | ||
|
|
7cb11dbc5d | ||
|
|
6d815629fc | ||
|
|
f8021d8541 | ||
|
|
1f444fdbc2 | ||
|
|
af76dd1be4 | ||
|
|
5cb91c6f8d | ||
|
|
de5502aec7 | ||
|
|
ef4ae4c5f2 | ||
|
|
866eb6d81b | ||
|
|
58d6c16eea | ||
|
|
ccbf13ae84 | ||
|
|
21f6acd3d7 | ||
|
|
dce59d1383 | ||
|
|
2fb544c3b0 | ||
|
|
1ba288be79 | ||
|
|
1ff21d2986 | ||
|
|
79cc2c1bb7 | ||
|
|
17b2e6660c | ||
|
|
4ef095fe55 | ||
|
|
fb0a7352e3 | ||
|
|
9a087e5975 | ||
|
|
814e47031d | ||
|
|
1304565e40 | ||
|
|
85616d0669 | ||
|
|
feff4647e7 | ||
|
|
549e15bfe2 | ||
|
|
a08baf8432 | ||
|
|
99c47ce133 | ||
|
|
26a4691c0b | ||
|
|
addb35aa69 | ||
|
|
16bf78f9ad | ||
|
|
3ed4f91d82 | ||
|
|
653fb894a2 | ||
|
|
0f9798a5f2 | ||
|
|
6776a20ec5 | ||
|
|
fb21e8586f | ||
|
|
8b2c4d604a | ||
|
|
c0b010d5ce | ||
|
|
97de34a088 | ||
|
|
6c0b76a270 | ||
|
|
eebd44ab9b | ||
|
|
783250c5ce | ||
|
|
70ff93050f | ||
|
|
1cc1813185 | ||
|
|
b4f9b87d06 | ||
|
|
26b09a767e | ||
|
|
bba6c4d8b6 | ||
|
|
ea440235c4 | ||
|
|
f9949bf988 | ||
|
|
b978db012e | ||
|
|
bc2fbdd657 | ||
|
|
a5571bcf4d | ||
|
|
683a528dd9 | ||
|
|
4a283b6052 | ||
|
|
37224ee1ad | ||
|
|
5e7445b10d | ||
|
|
1829a7da34 | ||
|
|
4a1a653bd9 | ||
|
|
840c1a87d0 | ||
|
|
c90368e2af | ||
|
|
1f9d74fadb | ||
|
|
5b261058fe | ||
|
|
cf6632ba02 | ||
|
|
c7124bd63c | ||
|
|
ba19e45607 | ||
|
|
20282ffcbb | ||
|
|
cb8ad90838 | ||
|
|
caac3fdcc4 | ||
|
|
44da282060 | ||
|
|
a2e27b09fc | ||
|
|
c1b1f397fd | ||
|
|
1d0f239594 | ||
|
|
549bc78799 | ||
|
|
9eb9b57e36 | ||
|
|
c38849961e | ||
|
|
3acfa0c4b1 | ||
|
|
62b083c3be | ||
|
|
ee6fc4c590 | ||
|
|
4fa550b3d3 | ||
|
|
1c44ae98fb | ||
|
|
148223f995 | ||
|
|
4678f83542 | ||
|
|
340bac078f | ||
|
|
3f8e75bbd8 | ||
|
|
c4278d14a9 | ||
|
|
4909106c3c | ||
|
|
0f04a2848a | ||
|
|
ba073bf8b2 | ||
|
|
640809aa6b | ||
|
|
943bb4c61a | ||
|
|
ef43cb7920 | ||
|
|
1abe634679 | ||
|
|
18ab18b6f6 | ||
|
|
aad971a599 | ||
|
|
3d9da97d7b | ||
|
|
2d0a09f760 | ||
|
|
568c317d6d | ||
|
|
f0e1f253f4 | ||
|
|
7634b9fa1d | ||
|
|
2e1795eba6 | ||
|
|
0f6b3d6e9b | ||
|
|
a70afcc461 | ||
|
|
d78fc2b282 | ||
|
|
249610afce | ||
|
|
b93b639b40 | ||
|
|
13ceb55fe8 | ||
|
|
7ebd38d350 | ||
|
|
c12c74005e | ||
|
|
c44dc1c6f6 | ||
|
|
cd2b9ec4ed | ||
|
|
dc62442c00 | ||
|
|
81a90f7ae4 | ||
|
|
af092e0d88 | ||
|
|
0e70015fd1 | ||
|
|
a214deef86 | ||
|
|
0efb2966b0 | ||
|
|
1e8422a506 | ||
|
|
7bb386e6d0 | ||
|
|
c464ca5612 | ||
|
|
742630fdee | ||
|
|
154fd899fe | ||
|
|
aa25f9d4c9 | ||
|
|
eeed94c8fd | ||
|
|
b2f771199d | ||
|
|
4bceb97a66 | ||
|
|
8734e6fc4c | ||
|
|
effd9315cb | ||
|
|
d45f60f604 | ||
|
|
1c7582d0a2 | ||
|
|
566e1090da | ||
|
|
a9b3054de3 | ||
|
|
73d179b355 | ||
|
|
97486242a0 | ||
|
|
a0ddff0618 | ||
|
|
f91e95bf63 | ||
|
|
431ef82f3e | ||
|
|
a07bc9fbca | ||
|
|
1e13cd9261 | ||
|
|
ae0b59179d | ||
|
|
a9ef12d687 | ||
|
|
b73c13172a | ||
|
|
c9b41e7449 | ||
|
|
fccc15df4a | ||
|
|
4f7eeacebc | ||
|
|
9b172a833a | ||
|
|
fd6fc925d6 | ||
|
|
6318670b6c | ||
|
|
7fa5fd83d2 | ||
|
|
e286acb123 | ||
|
|
ace6dd5a9a | ||
|
|
de70ab5eda | ||
|
|
56ab104ac5 | ||
|
|
56da5c04f2 | ||
|
|
3d73f325fe | ||
|
|
7b778631f3 | ||
|
|
de4c657868 | ||
|
|
b6df4a08b1 | ||
|
|
923c1d6cf6 | ||
|
|
ff892afa16 | ||
|
|
3e9e0a9be4 | ||
|
|
dc796900cd | ||
|
|
b8afd43d07 | ||
|
|
ba52ce79fc | ||
|
|
9936f9f357 | ||
|
|
677e1a3830 | ||
|
|
d46ff30540 | ||
|
|
98453580c0 | ||
|
|
5683929bbe | ||
|
|
ca368340d5 | ||
|
|
e66d2fac95 | ||
|
|
1c74d388dc | ||
|
|
6e698fec05 | ||
|
|
d06b2abea4 | ||
|
|
e7a4a364c2 | ||
|
|
31bcbf36dd | ||
|
|
afbf28668b | ||
|
|
7427a6d6d1 | ||
|
|
fab4e8e534 | ||
|
|
0fc6e89ffe | ||
|
|
9509b23dc1 | ||
|
|
b5ea6d43f3 | ||
|
|
92ef0addd5 | ||
|
|
a61d50b049 | ||
|
|
9021f1beea | ||
|
|
f3ecf30b3d | ||
|
|
ea40073fcc | ||
|
|
ed7e0a38d8 | ||
|
|
0d8d41f85d | ||
|
|
45bf8ae6b0 | ||
|
|
db9c2d924c | ||
|
|
62b34ab9a5 | ||
|
|
85157ddfb9 | ||
|
|
de746096ab | ||
|
|
f272612e74 | ||
|
|
d85bd25ed4 | ||
|
|
f39c786a64 | ||
|
|
26cae20505 | ||
|
|
58a8ebee0c | ||
|
|
5c8adf031c | ||
|
|
9b168d35d6 | ||
|
|
c4ece715f9 | ||
|
|
8e02de909f | ||
|
|
176b12f18c | ||
|
|
43514acc92 | ||
|
|
4894568651 | ||
|
|
f16d079e66 | ||
|
|
4ed71812a4 | ||
|
|
577b93e2dc | ||
|
|
c676b5168a | ||
|
|
4674af2f1b | ||
|
|
2429ab050d | ||
|
|
8698c0f5be | ||
|
|
c583a5dc62 | ||
|
|
528b74d156 | ||
|
|
ebedebd761 | ||
|
|
ee2953b2e9 | ||
|
|
7a2044b395 | ||
|
|
3123d021d9 | ||
|
|
c46b2cf59d | ||
|
|
79b4bae40a | ||
|
|
cb9e0ae252 | ||
|
|
a7a7d0552b | ||
|
|
01ea85e627 | ||
|
|
e81be79cb4 | ||
|
|
fca324480f | ||
|
|
762317e08a | ||
|
|
6d2f43e40a | ||
|
|
3823df8362 | ||
|
|
17398fc932 | ||
|
|
9b0e96a59a | ||
|
|
c5fdb914ff | ||
|
|
65ae5d9465 | ||
|
|
c4548f9e7e | ||
|
|
86cfb2ebc7 | ||
|
|
3bad049682 | ||
|
|
5352afee06 | ||
|
|
a83dedbcd0 | ||
|
|
beee584cc2 | ||
|
|
ccc7852576 | ||
|
|
0bdb3ddfea | ||
|
|
0f2b23bb6c | ||
|
|
a5537a0758 | ||
|
|
a64723269f | ||
|
|
2b93aa3dca | ||
|
|
992054f943 | ||
|
|
784aa754f7 | ||
|
|
5124d3aae8 | ||
|
|
0fb746bc03 | ||
|
|
a2d301bfbc | ||
|
|
4523f3e112 | ||
|
|
012cea8fce | ||
|
|
48a62232a7 | ||
|
|
46ee857c22 | ||
|
|
7bbd22b4b2 | ||
|
|
862c5b7a7c | ||
|
|
5eb7e27732 | ||
|
|
1231193c89 | ||
|
|
719938f442 | ||
|
|
7b5f2ea69b | ||
|
|
01709deced | ||
|
|
23c87bfaa4 | ||
|
|
0ef8e1861b | ||
|
|
10b278a141 | ||
|
|
72c884f9ec | ||
|
|
ecdae0e8df | ||
|
|
2e67b16ba2 | ||
|
|
18d31235d4 | ||
|
|
4f8c150c0b | ||
|
|
f26c937747 | ||
|
|
34176c336e | ||
|
|
635f3678ef | ||
|
|
12bd8cc265 | ||
|
|
64316a8a61 | ||
|
|
b85c3b25f7 | ||
|
|
144344bcfc | ||
|
|
7736ba8999 | ||
|
|
90c7bd56bf | ||
|
|
714edd70fb | ||
|
|
9c31813a16 | ||
|
|
d6b76134a3 | ||
|
|
b7692e39c8 | ||
|
|
60b5179b3e | ||
|
|
46a1c86cb6 | ||
|
|
393363b33e | ||
|
|
e762363d96 | ||
|
|
5d2082b478 | ||
|
|
ad09fd81b4 | ||
|
|
54fdbf9fd0 | ||
|
|
8c3de939da | ||
|
|
2f3a3f8f23 | ||
|
|
daf13d693d | ||
|
|
bbd5384589 | ||
|
|
fb72bbf81e | ||
|
|
29edd6b9b5 | ||
|
|
215b35575d | ||
|
|
6394275570 | ||
|
|
eaca72991b | ||
|
|
634e895469 | ||
|
|
77c8033d22 | ||
|
|
7508ae21b8 | ||
|
|
fd90a62af1 | ||
|
|
707dd682ed | ||
|
|
7396988032 | ||
|
|
58960cdc6e | ||
|
|
31a42dce7e | ||
|
|
9b248a8fa4 | ||
|
|
f099e5c6b7 | ||
|
|
5c093c81ab | ||
|
|
e73146533a | ||
|
|
db1237184f | ||
|
|
1e5ed1d29e | ||
|
|
d68ea2c28f | ||
|
|
9700ab2cb6 | ||
|
|
d8321c873e | ||
|
|
44784487a0 | ||
|
|
76471fa694 | ||
|
|
6ec7b4305c | ||
|
|
896a344ac7 | ||
|
|
34cefcdaf6 | ||
|
|
4c8eb68611 | ||
|
|
50866659ea | ||
|
|
8262cb858e | ||
|
|
e710317cac | ||
|
|
138cf781f3 | ||
|
|
70d315b2d7 | ||
|
|
bca8ca52da | ||
|
|
42c769ed56 | ||
|
|
592af39550 | ||
|
|
79620030bc | ||
|
|
294bc8712b | ||
|
|
f75d30d8ce | ||
|
|
de81e1e790 | ||
|
|
450f5c45a1 | ||
|
|
b864b32cb2 | ||
|
|
0d13ac8f38 | ||
|
|
edd517e21c | ||
|
|
32d356600a | ||
|
|
9e290278d0 | ||
|
|
4140dcb9dc | ||
|
|
1ed3d31011 | ||
|
|
3915f921f1 | ||
|
|
050571196c | ||
|
|
3c1a129ac9 | ||
|
|
57a3561aa8 | ||
|
|
ec37bfc0c6 | ||
|
|
82387e95da | ||
|
|
36b018151d | ||
|
|
b4754b64d5 | ||
|
|
3a5530a186 | ||
|
|
ee22044b34 | ||
|
|
8f5570128d | ||
|
|
24d2e744a4 | ||
|
|
bc9877133c | ||
|
|
05886bed71 | ||
|
|
c882c12890 | ||
|
|
12647896c4 | ||
|
|
ebbec7d68c | ||
|
|
cf054395bb | ||
|
|
8c04cf3db2 | ||
|
|
516690b260 | ||
|
|
fc602054ba | ||
|
|
e2244429ce | ||
|
|
b60ac6e1be | ||
|
|
6999fb8c2a | ||
|
|
fd84b5920b | ||
|
|
d02df02bbe | ||
|
|
1f143b814e | ||
|
|
65700ad70e | ||
|
|
6a16f1344a | ||
|
|
3ecacc69a5 | ||
|
|
9db2e858d2 | ||
|
|
f8cdf6bf45 | ||
|
|
1b97d85023 | ||
|
|
74a5f429e9 | ||
|
|
7554055767 | ||
|
|
f617f91d96 | ||
|
|
7b95e6b276 | ||
|
|
1510ed2147 | ||
|
|
f8237217b0 | ||
|
|
5f95fe16fd | ||
|
|
cf8fd893fa | ||
|
|
1032629800 | ||
|
|
12576711fa | ||
|
|
233c8085b5 | ||
|
|
1b1650eba4 | ||
|
|
73c17c68c4 | ||
|
|
3492eb3126 | ||
|
|
6dcc4b30de | ||
|
|
a5e15f9c5e | ||
|
|
851c1f450c | ||
|
|
fd12921f6d | ||
|
|
1100e160de | ||
|
|
bfcdf011c4 | ||
|
|
48ff1ebefa | ||
|
|
f1c9d05abc | ||
|
|
73217c9178 | ||
|
|
7c5acb8c21 | ||
|
|
10662ec679 | ||
|
|
65aba12dd0 | ||
|
|
8d1410b181 | ||
|
|
d9fbccb644 | ||
|
|
c9334074f6 | ||
|
|
f4b77598c3 | ||
|
|
478a046074 | ||
|
|
c4cb1f9c66 | ||
|
|
f84a3a9034 | ||
|
|
fa389515cd | ||
|
|
06c480ef00 | ||
|
|
71dd07ac28 | ||
|
|
30f15cf7e8 | ||
|
|
cc99e91935 | ||
|
|
6544c1887a | ||
|
|
9636d4d347 | ||
|
|
c7ce80c5e3 | ||
|
|
5d0050535d | ||
|
|
340d013bb3 | ||
|
|
f6c99c4c20 | ||
|
|
7c6eecb0e9 | ||
|
|
59b09e580e | ||
|
|
4174eebffe | ||
|
|
200aefac54 | ||
|
|
b200088093 | ||
|
|
8669ed8c73 | ||
|
|
8a96724247 | ||
|
|
3a913fba44 | ||
|
|
05a08d8640 | ||
|
|
2bea16d003 | ||
|
|
475f0438f9 | ||
|
|
c9720c15b9 | ||
|
|
0232fc1c2c | ||
|
|
9d5550bf5f | ||
|
|
873d9c2df6 | ||
|
|
5266b01e4c | ||
|
|
12315e90de | ||
|
|
c32905cca4 | ||
|
|
71571e2625 | ||
|
|
25eb29946b | ||
|
|
bb98d3209d | ||
|
|
73f624d6a1 | ||
|
|
d1f8c130a1 | ||
|
|
93e16c4b7c | ||
|
|
ee91d1d83e | ||
|
|
46d408056c | ||
|
|
d8ff64c1d9 | ||
|
|
453600a70c | ||
|
|
597e466d84 | ||
|
|
65164bd8ef | ||
|
|
dbf589da91 | ||
|
|
0857cbfa92 | ||
|
|
225edce50a | ||
|
|
5893fdefaf | ||
|
|
2b4ebd6e10 | ||
|
|
f97e48e886 | ||
|
|
b788bc6b6a | ||
|
|
8c91b01dd9 | ||
|
|
30b6dac7dd | ||
|
|
b6ec89e4aa | ||
|
|
39a0fcb358 | ||
|
|
9d353806a3 | ||
|
|
d24f8d5c7c | ||
|
|
cd7d51a16b | ||
|
|
d438fbba49 | ||
|
|
d73c71e18b | ||
|
|
3f4d1983e1 | ||
|
|
1c87ac0546 | ||
|
|
a11aa5c751 | ||
|
|
5cae0b99c7 | ||
|
|
58d0b6b458 | ||
|
|
4815d225d7 | ||
|
|
fd487350f5 | ||
|
|
dafcf22d49 | ||
|
|
78cefe19bd | ||
|
|
bbca105077 | ||
|
|
e3e4278206 | ||
|
|
e479e62cce | ||
|
|
8f73b08fbf | ||
|
|
5d02a8874f | ||
|
|
2ec5789d71 | ||
|
|
1c7a159e40 | ||
|
|
81a4caf23c | ||
|
|
9065b6034b | ||
|
|
49104922c1 | ||
|
|
409e064452 | ||
|
|
61f7efaa85 | ||
|
|
f0baa7496f | ||
|
|
7ed115cddb | ||
|
|
cb6dc29469 | ||
|
|
44032e2a9e | ||
|
|
7390a9421d | ||
|
|
f1cbe6fba9 | ||
|
|
5925cf5cc9 | ||
|
|
219a914cb1 | ||
|
|
6b6d6337c8 | ||
|
|
c511ae09a1 | ||
|
|
e658cb3aa0 | ||
|
|
9f4cf475b6 | ||
|
|
af53cbebed | ||
|
|
1895ebcf25 | ||
|
|
02a313d70b | ||
|
|
57ed905140 | ||
|
|
d46741c5ae | ||
|
|
ba3692e085 | ||
|
|
593cad65dd | ||
|
|
8bd05144da | ||
|
|
899ebcd681 | ||
|
|
f4917762c0 | ||
|
|
88c35281bf | ||
|
|
7e1810a0a7 | ||
|
|
7eb62de74b | ||
|
|
69bb7ef1fd | ||
|
|
57f0404a02 | ||
|
|
d8dccda1c0 | ||
|
|
16f3c4190d | ||
|
|
823878e963 | ||
|
|
f4246e3e0a | ||
|
|
9a8730a832 | ||
|
|
31d01aef11 | ||
|
|
a61da39038 | ||
|
|
7b9def2aeb | ||
|
|
92b8cc9f6b | ||
|
|
d3d5f7a745 | ||
|
|
be78e4c48a | ||
|
|
be9937355b | ||
|
|
f1d36f0f4a | ||
|
|
2a3f223a2b | ||
|
|
21401c544d | ||
|
|
6ceb7404bb | ||
|
|
a60d2e7f0f | ||
|
|
4480eff46d | ||
|
|
b265bd68b3 | ||
|
|
9e47eb3470 | ||
|
|
a050f419e4 | ||
|
|
d1373aa5b3 | ||
|
|
c53704849a | ||
|
|
e27a25b147 | ||
|
|
b1a88a9c2d | ||
|
|
780fd0490e | ||
|
|
b3d0ee7080 | ||
|
|
6d2287c4e5 | ||
|
|
8ae8c43c3c | ||
|
|
2a18321225 | ||
|
|
2f67e4de94 | ||
|
|
84c6af1ee9 | ||
|
|
caae39b2de | ||
|
|
2af30856ba | ||
|
|
a85e842911 | ||
|
|
2f9805e97b | ||
|
|
2c66b9ecc5 | ||
|
|
35e5f8928a | ||
|
|
4c647db584 | ||
|
|
e523ea0cc3 | ||
|
|
404343e5e7 | ||
|
|
27f5353ee6 | ||
|
|
848fd4d4c8 | ||
|
|
2f58bf2dd1 | ||
|
|
dbe32056d0 | ||
|
|
c88e210136 | ||
|
|
c002ba2f00 | ||
|
|
a8c076f2c9 | ||
|
|
99da264f31 | ||
|
|
6c862be9b9 | ||
|
|
6c75d64b5d | ||
|
|
bf59390f68 | ||
|
|
406cfaefef | ||
|
|
1a1919f38c | ||
|
|
3d99c300a4 | ||
|
|
1a63be7746 | ||
|
|
ae7e50396e | ||
|
|
f7ffb41dea | ||
|
|
1e6987a229 | ||
|
|
d0bc5caebf | ||
|
|
59c7481fd7 | ||
|
|
70a11de316 | ||
|
|
3da88135c2 | ||
|
|
d2a706fd9d | ||
|
|
8f57af0667 | ||
|
|
cb46029f35 | ||
|
|
e35c63ff5d | ||
|
|
440d54f500 | ||
|
|
f4a78ef397 | ||
|
|
96831e5973 | ||
|
|
c5f9cc104c | ||
|
|
dc6c391798 | ||
|
|
80e5879c43 | ||
|
|
d9864f31c1 | ||
|
|
75b0d25d3e | ||
|
|
625de2e38e | ||
|
|
0f7d64350f | ||
|
|
63620f246b | ||
|
|
f416d628ae | ||
|
|
24bf642dfa | ||
|
|
8a0321bf67 | ||
|
|
5e033e1f76 | ||
|
|
ec47fa42b8 | ||
|
|
080ecf9fd9 | ||
|
|
f0deda32dc | ||
|
|
7b3298e9e3 | ||
|
|
f26661459d | ||
|
|
b4c0047d45 | ||
|
|
4e03cf9675 | ||
|
|
33eb5fc020 | ||
|
|
01d62a6a15 | ||
|
|
bae22e0104 | ||
|
|
33b49d46ce | ||
|
|
c6beda1b8f | ||
|
|
8faba6ec5b | ||
|
|
e2824da846 | ||
|
|
b4dcadbf17 | ||
|
|
e96e1ebb9c | ||
|
|
b59158d314 | ||
|
|
82588a0f83 | ||
|
|
c6c91b6b11 | ||
|
|
a8918156f6 | ||
|
|
df56123ceb | ||
|
|
76d331ef03 | ||
|
|
71524b2073 | ||
|
|
f286efb174 | ||
|
|
feea9384c1 | ||
|
|
7dfcc9a3b6 | ||
|
|
43efd8e60b | ||
|
|
28fce80476 | ||
|
|
cf4c4664e1 | ||
|
|
9a8b2a308d | ||
|
|
188a70de21 | ||
|
|
5fccd1064f | ||
|
|
9f58943dfc | ||
|
|
9111f8b8c7 | ||
|
|
89dba2dbc0 | ||
|
|
0b79f125ed | ||
|
|
59bc429ce5 | ||
|
|
46db31c695 | ||
|
|
c45a617a90 | ||
|
|
2982ac051f | ||
|
|
a6b1b385e5 | ||
|
|
3f8c62c1a4 | ||
|
|
e31443b285 | ||
|
|
10beeab4b5 | ||
|
|
a5fdfc3b67 | ||
|
|
73d0e2589b | ||
|
|
2c6d2a30d7 | ||
|
|
d0566bf336 | ||
|
|
e93e00f4f7 | ||
|
|
720252f6ae | ||
|
|
43ee9b05a3 | ||
|
|
bcfb847412 | ||
|
|
402a7a730d | ||
|
|
5e975a6acd | ||
|
|
f1639126df | ||
|
|
7aa048a512 | ||
|
|
76b958ed89 | ||
|
|
d0c8e0bb49 | ||
|
|
7f1df2d24c | ||
|
|
7331fdc083 | ||
|
|
980d675572 | ||
|
|
749df9e554 | ||
|
|
7bb97dadf4 | ||
|
|
a223357ef1 | ||
|
|
9ef1e3cd9b | ||
|
|
1459b05189 | ||
|
|
2c992a0d23 | ||
|
|
f6a712c17b | ||
|
|
97c03d97ad | ||
|
|
b80589ea63 | ||
|
|
4fd3e606fd | ||
|
|
d1ae925454 | ||
|
|
3e260e4464 | ||
|
|
b7d841be14 | ||
|
|
854ce07e8f | ||
|
|
5e572ce5ef | ||
|
|
bec8b3ce0f | ||
|
|
df302015a3 | ||
|
|
fd9c7848c2 | ||
|
|
3c6a2e2546 | ||
|
|
af7229acb0 | ||
|
|
e1fbbe7c4e | ||
|
|
cdc7827b7e | ||
|
|
f24005e02e | ||
|
|
0ff9a7e50d | ||
|
|
3ec810ed82 | ||
|
|
93fc8f7452 | ||
|
|
172b3daeab | ||
|
|
1b3e3b19cf | ||
|
|
29b31ffa1e | ||
|
|
f1aca3df4d | ||
|
|
180ac60d21 | ||
|
|
b22621e3c4 | ||
|
|
db1d61f1e3 | ||
|
|
8e0c5b3792 | ||
|
|
1b30a8edd4 | ||
|
|
fe1376a70c | ||
|
|
2dbce8f695 | ||
|
|
b1700f3d14 | ||
|
|
63e3e0b9d3 | ||
|
|
cdd55ee61d | ||
|
|
c72050d329 | ||
|
|
9a89b82f83 | ||
|
|
37becd9327 | ||
|
|
d5f2ae7871 | ||
|
|
08b74b5304 | ||
|
|
6183dbf653 | ||
|
|
939f3db719 | ||
|
|
d3e2ee2974 | ||
|
|
0b6d955672 | ||
|
|
6884453b0f | ||
|
|
0d37600cda | ||
|
|
c32211aa7a | ||
|
|
e0776952ee | ||
|
|
d92e4a014a | ||
|
|
48fc51a89a | ||
|
|
0af29e146d | ||
|
|
4467c64edd | ||
|
|
1314d7744f | ||
|
|
9a984e7e41 | ||
|
|
89e4010566 | ||
|
|
1ecfc56f19 | ||
|
|
579c6eaf77 | ||
|
|
4cecfd1611 | ||
|
|
5a9899c63d | ||
|
|
a1d3c46127 | ||
|
|
902f3f7d00 | ||
|
|
8ef32c50c2 | ||
|
|
e0d0512777 | ||
|
|
9c73ffd7da | ||
|
|
0e2ca7c0d9 | ||
|
|
2d8f5ef337 | ||
|
|
a02e9b8318 | ||
|
|
4f844f673f | ||
|
|
1a068ed82b | ||
|
|
b8df4caf75 | ||
|
|
11da11c326 | ||
|
|
3e41a5568f | ||
|
|
3f73ee6cff | ||
|
|
866137e128 | ||
|
|
66b0ac7d51 | ||
|
|
7ac7edb437 | ||
|
|
82ed74429c | ||
|
|
417dc7dac1 | ||
|
|
999ce044cb | ||
|
|
8759508c86 | ||
|
|
56a40edc66 | ||
|
|
5021c77e53 | ||
|
|
3fbe859c28 | ||
|
|
560e94a232 | ||
|
|
d982ccd4d1 | ||
|
|
45b38c23a5 | ||
|
|
442180014d | ||
|
|
867a968b5b | ||
|
|
334529ab67 | ||
|
|
c66fc2d6de | ||
|
|
5eea95d118 | ||
|
|
804a215190 | ||
|
|
da6dfc9bcc | ||
|
|
0d4ec308d6 | ||
|
|
24437e7b10 | ||
|
|
1abaff6625 | ||
|
|
0c1692866c | ||
|
|
d28e9bb51c | ||
|
|
760c5d521b | ||
|
|
6ce14ffc80 | ||
|
|
cc54725011 | ||
|
|
c1a6f2b957 | ||
|
|
5d5578f89f | ||
|
|
e7bb58397e | ||
|
|
04690f1949 | ||
|
|
4df449c724 | ||
|
|
315be1b61d | ||
|
|
4c30fb09ab | ||
|
|
2a24d1d9b7 | ||
|
|
fe771928ee | ||
|
|
768baccea7 | ||
|
|
c5a6f06c35 | ||
|
|
e57a90ca01 | ||
|
|
9d95750092 | ||
|
|
3c5a218152 | ||
|
|
cf6148eca5 | ||
|
|
df1945fb38 | ||
|
|
ed93efd1bd | ||
|
|
2d48073262 | ||
|
|
8bba27361f | ||
|
|
de54794506 | ||
|
|
0aee4d8466 | ||
|
|
1d8e0c7b1d | ||
|
|
6fbec9fbd7 | ||
|
|
6d20457f0e | ||
|
|
73dc18fb78 | ||
|
|
440284663a | ||
|
|
796793eefd | ||
|
|
260fd572bf | ||
|
|
d6c03a51b1 | ||
|
|
fe89c5835c | ||
|
|
ddcc35ec58 | ||
|
|
0f39497bee | ||
|
|
58f1346124 | ||
|
|
997366655c | ||
|
|
de057fd08f | ||
|
|
d15607048e | ||
|
|
5460d19f80 | ||
|
|
ed4ae5efb8 | ||
|
|
530da2589d | ||
|
|
05b2a16746 | ||
|
|
35cbad2177 | ||
|
|
f13ac340b1 | ||
|
|
14dc5e40fe | ||
|
|
4820313158 | ||
|
|
9d573932f5 | ||
|
|
ab88fbc1bd | ||
|
|
1a806c209b | ||
|
|
f15540e860 | ||
|
|
d1be56923f | ||
|
|
493044f4e4 | ||
|
|
d15ca72e07 | ||
|
|
9a16634c2e | ||
|
|
997940b4ab | ||
|
|
fee4585ca4 | ||
|
|
94c1df79ef | ||
|
|
b4e6626feb | ||
|
|
b8ad77085c | ||
|
|
750f1fc379 | ||
|
|
8b93b5d273 | ||
|
|
2ffbb9d9fa | ||
|
|
97180c2f04 | ||
|
|
a9b9ed2d99 | ||
|
|
097f6c3b64 | ||
|
|
a279982187 | ||
|
|
26c692978a | ||
|
|
482f8bb862 | ||
|
|
3ac7cc1a59 | ||
|
|
7c5aff44a9 | ||
|
|
a55d4e3c6f | ||
|
|
485b337c72 | ||
|
|
02f16168d3 | ||
|
|
bd42af9c3e | ||
|
|
ce5a5a6e50 | ||
|
|
bf893ca8b4 | ||
|
|
20a450d1d4 | ||
|
|
70d11c3f5d | ||
|
|
dc7690f4cf | ||
|
|
603260df5d | ||
|
|
51bdc6b221 | ||
|
|
4f6a9545e9 | ||
|
|
14edb4f913 | ||
|
|
ffe0459994 | ||
|
|
5138ce2040 | ||
|
|
db85de0722 | ||
|
|
082a76bfb7 | ||
|
|
9a3b160e62 | ||
|
|
c8b312bcb1 | ||
|
|
9cc3fa0897 | ||
|
|
49063cac26 | ||
|
|
59a06c84a1 | ||
|
|
6f5a7b97b5 | ||
|
|
0db97fd5d5 | ||
|
|
b31f74f0c5 | ||
|
|
ffb3957d4f | ||
|
|
3b46e9a6bb | ||
|
|
b67cb3bd9e | ||
|
|
91a6485c20 | ||
|
|
da24634774 | ||
|
|
88198f7696 | ||
|
|
2bdc7cd39a | ||
|
|
ec4fcbc553 | ||
|
|
30820a8d81 | ||
|
|
1c439a0392 | ||
|
|
d6fe3461f7 | ||
|
|
18a211592d | ||
|
|
8396c04b4a | ||
|
|
872254e865 | ||
|
|
2a8fb84dc3 | ||
|
|
008dab08f8 | ||
|
|
781a3958d8 | ||
|
|
ec344cafb5 | ||
|
|
ec20b288be | ||
|
|
9206e00a99 | ||
|
|
438a172e77 | ||
|
|
8aaa559fbb | ||
|
|
79b3d30c1e | ||
|
|
3c5293b60b | ||
|
|
d16da528af | ||
|
|
6407a3a06d | ||
|
|
145804c05d | ||
|
|
7b5da511fe | ||
|
|
cc65d5ac23 | ||
|
|
6c41362bfa | ||
|
|
b57688033e | ||
|
|
9c120118e9 | ||
|
|
70ce321c75 | ||
|
|
ed3df2beba | ||
|
|
613d445c99 | ||
|
|
5cc3885e32 | ||
|
|
7e66eca6ae | ||
|
|
eef1c3f41d | ||
|
|
09defb60a5 | ||
|
|
4fd931c508 | ||
|
|
9b656a13e6 | ||
|
|
e95fe8f410 | ||
|
|
91628c61ee | ||
|
|
0747bc9bc1 | ||
|
|
2df881d43f | ||
|
|
cd10eaf9db | ||
|
|
e0f69a6420 | ||
|
|
e8828ed74b | ||
|
|
ce29e0685c | ||
|
|
6c7dda2b67 | ||
|
|
7694e3f565 | ||
|
|
b8c9731081 | ||
|
|
14ec8b2bcb | ||
|
|
e9d7543c43 | ||
|
|
311e090826 | ||
|
|
1c1172e0e7 | ||
|
|
2a1b719f67 | ||
|
|
9b9cf8ad9f | ||
|
|
946f427c4b | ||
|
|
100f34a32b | ||
|
|
7a9b506958 | ||
|
|
4cd074ee2a | ||
|
|
e2a76274ef | ||
|
|
aa197bd89e | ||
|
|
fde1165408 | ||
|
|
731b91e0a9 | ||
|
|
60205ece9d | ||
|
|
6b8fc5d624 | ||
|
|
54df0af48e | ||
|
|
f1101cae5a | ||
|
|
f7a1edb7e5 | ||
|
|
4e7311bdfe | ||
|
|
818210805f | ||
|
|
0e1026a100 | ||
|
|
553a2e3dad | ||
|
|
c12eb40459 | ||
|
|
9b68283eb3 | ||
|
|
e9ee467940 | ||
|
|
91f1de5d95 | ||
|
|
6ce6b55b68 | ||
|
|
d0b5e3e241 | ||
|
|
f16176f298 | ||
|
|
2dc4d41524 | ||
|
|
2bf2901dcd | ||
|
|
99c2d6195d | ||
|
|
c5b1079be9 | ||
|
|
c25e05fde7 | ||
|
|
c6290cb7f6 | ||
|
|
b08d5090a2 | ||
|
|
3da824b4c3 | ||
|
|
9a1619c554 | ||
|
|
312f7a9003 | ||
|
|
a197948837 | ||
|
|
765d510127 | ||
|
|
b5179e443b | ||
|
|
fe6c371a9f | ||
|
|
d66c5ed618 | ||
|
|
70ba120cfb | ||
|
|
33f1d85194 | ||
|
|
e3f1b449e2 | ||
|
|
7e580efc77 | ||
|
|
b33501cce6 | ||
|
|
402259ef11 | ||
|
|
2d4c682fac | ||
|
|
2affc5f555 | ||
|
|
9ce1dded38 | ||
|
|
383b0d125c | ||
|
|
8023710702 | ||
|
|
82c0cbc8bd | ||
|
|
893a7f22a4 | ||
|
|
b903e511b5 | ||
|
|
86379d1c79 | ||
|
|
6320d6fbeb | ||
|
|
636023167d | ||
|
|
8a86825357 | ||
|
|
f5d68b4d78 | ||
|
|
7295e656eb | ||
|
|
5e51050fa3 | ||
|
|
c510a64207 | ||
|
|
7269050cf4 | ||
|
|
5730e5d2ae | ||
|
|
76ddbe1869 | ||
|
|
b5ef174312 | ||
|
|
31007f3d78 | ||
|
|
c5a66709ad | ||
|
|
aa4815b01d | ||
|
|
7d6b975592 | ||
|
|
3a57712dd2 | ||
|
|
62eb35340e | ||
|
|
e9880d00ab | ||
|
|
73c1792fea | ||
|
|
d36f399d08 | ||
|
|
c8a24dbcbc | ||
|
|
fecc5134ad | ||
|
|
8ad84a8cec | ||
|
|
51ebee2910 | ||
|
|
20d496e617 | ||
|
|
44a16dd214 | ||
|
|
4695b79b73 | ||
|
|
9c0be07ae1 | ||
|
|
ab945aadde | ||
|
|
c1c461c273 | ||
|
|
336742ee69 | ||
|
|
405dacce1c | ||
|
|
9acd45aa93 | ||
|
|
c889c2cc03 | ||
|
|
7ac91ef416 | ||
|
|
8e28669aa1 | ||
|
|
6cdb91f8b8 | ||
|
|
e892474713 | ||
|
|
abdae98816 | ||
|
|
ab4fe49f33 | ||
|
|
1ace35103b | ||
|
|
dbee729eee | ||
|
|
792576ce59 | ||
|
|
a07624e9b9 | ||
|
|
bb8054af8a | ||
|
|
7738f3e066 | ||
|
|
5dee16a100 | ||
|
|
35f3bcdf2f | ||
|
|
130ca8e1f1 | ||
|
|
ced4c21688 | ||
|
|
6ec7078024 | ||
|
|
b28d8f2506 | ||
|
|
c88a9291a0 | ||
|
|
1e82d19306 | ||
|
|
dd87e50cb2 | ||
|
|
4c8f96a30f | ||
|
|
c4f45e05f1 | ||
|
|
6aa382c7c1 | ||
|
|
ccb9f059e6 | ||
|
|
1cdcea0771 | ||
|
|
88dda0de80 | ||
|
|
30ed99e2b0 | ||
|
|
e5953b7541 | ||
|
|
1f9d01c59f | ||
|
|
cc5210a3d8 | ||
|
|
26559e2d3b | ||
|
|
7eeddb300f | ||
|
|
1e01bae16b | ||
|
|
87c03924e5 | ||
|
|
f0998b1d43 | ||
|
|
1995a04244 | ||
|
|
420fe6bcd5 | ||
|
|
d4e26c0553 | ||
|
|
5f5e7cb45e | ||
|
|
8aa0304738 | ||
|
|
8ec98c33a4 | ||
|
|
2667182ca3 | ||
|
|
1cd0018b93 | ||
|
|
359789ee29 | ||
|
|
e79c860c0f | ||
|
|
765f53f30e | ||
|
|
3c3c21d7f5 | ||
|
|
eb700cb500 | ||
|
|
b3b723a717 | ||
|
|
555c230d2e | ||
|
|
adf4b97aef | ||
|
|
32c38d796b | ||
|
|
c8829e15ed | ||
|
|
453df417d0 | ||
|
|
02a7741a9c | ||
|
|
96fc5b0ca8 | ||
|
|
b13e624a66 | ||
|
|
6a8f66f272 | ||
|
|
0c638a08fd | ||
|
|
b07f8af8e5 | ||
|
|
3bbb2a985f | ||
|
|
afdf71c545 | ||
|
|
8de8d2df9a | ||
|
|
1dffdbddc2 | ||
|
|
11fff633b0 | ||
|
|
61bc44d1f4 | ||
|
|
e8fabb8cfa | ||
|
|
7a50885847 | ||
|
|
6239da45f4 | ||
|
|
af597eb3c7 | ||
|
|
d66cda068c | ||
|
|
91fcd07c1c | ||
|
|
85aa470da1 | ||
|
|
6f0d5f37a5 | ||
|
|
1b4d604404 | ||
|
|
a7f6cbe0b9 | ||
|
|
9cf28bf123 | ||
|
|
c92e04294a | ||
|
|
36f059b455 | ||
|
|
4aac301852 | ||
|
|
b375708bbd | ||
|
|
10b6a9482b | ||
|
|
84d45c5df8 | ||
|
|
c6559814b1 | ||
|
|
c8c080183f | ||
|
|
597b67f799 | ||
|
|
ec52d5f422 | ||
|
|
34806304d6 | ||
|
|
87953d5495 | ||
|
|
b6c7c80c95 | ||
|
|
77e568d5c3 | ||
|
|
699fc51cf7 | ||
|
|
21029c90b7 | ||
|
|
6b0530eb7f | ||
|
|
f7061c7225 | ||
|
|
750f698369 | ||
|
|
ec5ef42298 | ||
|
|
46820b0044 | ||
|
|
425a6648f7 | ||
|
|
349fc297ce | ||
|
|
5ad87c03ed | ||
|
|
d16006f28f | ||
|
|
7f0452a5f5 | ||
|
|
c605b2f6fc | ||
|
|
6c2d8a8494 | ||
|
|
874691f729 | ||
|
|
cdf702e17d | ||
|
|
25fdb32627 | ||
|
|
e976ea0a3a | ||
|
|
34e6b4fc69 | ||
|
|
a2d77567b3 | ||
|
|
ecb460f9b5 | ||
|
|
63444b089c | ||
|
|
c787984b77 | ||
|
|
bf3d03e801 | ||
|
|
bc2e69b975 | ||
|
|
7b94fcf3da | ||
|
|
9cf03b8b88 | ||
|
|
a288edcf61 | ||
|
|
89cc18ad37 | ||
|
|
ffa3b671e1 | ||
|
|
f32eeb413b | ||
|
|
b5a5103cfc | ||
|
|
c5697e59f3 | ||
|
|
f030667ff4 | ||
|
|
e9fef5d97c | ||
|
|
f5818ac7ea | ||
|
|
c85ab4110d | ||
|
|
9690ea35b8 | ||
|
|
6300c8011b | ||
|
|
97f582b381 | ||
|
|
5135a9c228 | ||
|
|
b7d1212a82 | ||
|
|
7d9d0a9fc4 | ||
|
|
ed9aef4321 | ||
|
|
0aa638bdf4 | ||
|
|
0ec39d793d |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
rustflags = ["-Wunused-crate-dependencies"]
|
||||
33
.devcontainer/dev.compose.yaml
Normal file
33
.devcontainer/dev.compose.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
services:
|
||||
dev:
|
||||
image: mcr.microsoft.com/devcontainers/rust:1-1-bullseye
|
||||
volumes:
|
||||
# Mount the root folder that contains .git
|
||||
- ../:/workspace:cached
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /proc:/proc
|
||||
- repos:/etc/komodo/repos
|
||||
- stacks:/etc/komodo/stacks
|
||||
command: sleep infinity
|
||||
ports:
|
||||
- "9121:9121"
|
||||
environment:
|
||||
KOMODO_FIRST_SERVER: http://localhost:8120
|
||||
KOMODO_DATABASE_ADDRESS: db
|
||||
KOMODO_ENABLE_NEW_USERS: true
|
||||
KOMODO_LOCAL_AUTH: true
|
||||
KOMODO_JWT_SECRET: a_random_secret
|
||||
links:
|
||||
- db
|
||||
# ...
|
||||
|
||||
db:
|
||||
extends:
|
||||
file: ../dev.compose.yaml
|
||||
service: ferretdb
|
||||
|
||||
volumes:
|
||||
data:
|
||||
repo-cache:
|
||||
repos:
|
||||
stacks:
|
||||
46
.devcontainer/devcontainer.json
Normal file
46
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,46 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
|
||||
{
|
||||
"name": "Komodo",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
//"image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye",
|
||||
"dockerComposeFile": ["dev.compose.yaml"],
|
||||
"workspaceFolder": "/workspace",
|
||||
"service": "dev",
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "20.12.2"
|
||||
},
|
||||
"ghcr.io/devcontainers-community/features/deno:1": {
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
// Use 'mounts' to make the cargo cache persistent in a Docker Volume.
|
||||
"mounts": [
|
||||
{
|
||||
"source": "devcontainer-cargo-cache-${devcontainerId}",
|
||||
"target": "/usr/local/cargo",
|
||||
"type": "volume"
|
||||
}
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [
|
||||
9121
|
||||
],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "./.devcontainer/postCreate.sh",
|
||||
|
||||
"runServices": [
|
||||
"db"
|
||||
]
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
3
.devcontainer/postCreate.sh
Executable file
3
.devcontainer/postCreate.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
cargo install typeshare-cli
|
||||
@@ -1,4 +0,0 @@
|
||||
/target
|
||||
/config_example
|
||||
config.*
|
||||
.env
|
||||
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
|
||||
open_collective: komodo
|
||||
21
.gitignore
vendored
21
.gitignore
vendored
@@ -1,12 +1,9 @@
|
||||
target
|
||||
/frontend/build
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.env.development
|
||||
|
||||
repos
|
||||
config.json
|
||||
config.toml
|
||||
secrets.json
|
||||
secrets.toml
|
||||
target
|
||||
node_modules
|
||||
dist
|
||||
deno.lock
|
||||
.env
|
||||
.env.development
|
||||
.DS_Store
|
||||
.idea
|
||||
.dev
|
||||
|
||||
1
.kminclude
Normal file
1
.kminclude
Normal file
@@ -0,0 +1 @@
|
||||
.dev
|
||||
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"rust-lang.rust-analyzer",
|
||||
"tamasfe.even-better-toml",
|
||||
"vadimcn.vscode-lldb",
|
||||
"denoland.vscode-deno"
|
||||
]
|
||||
}
|
||||
25
.vscode/resolver.code-snippets
vendored
Normal file
25
.vscode/resolver.code-snippets
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"resolve": {
|
||||
"scope": "rust",
|
||||
"prefix": "resolve",
|
||||
"body": [
|
||||
"impl Resolve<${0}> for ${1} {",
|
||||
"\tasync fn resolve(self, _: &${0}) -> Result<Self::Response, Self::Error> {",
|
||||
"\t\ttodo!()",
|
||||
"\t}",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"static": {
|
||||
"scope": "rust",
|
||||
"prefix": "static",
|
||||
"body": [
|
||||
"fn ${1}() -> &'static ${2} {",
|
||||
"\tstatic ${0}: OnceLock<${2}> = OnceLock::new();",
|
||||
"\t${0}.get_or_init(|| {",
|
||||
"\t\ttodo!()",
|
||||
"\t})",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
64
.vscode/solid.code-snippets
vendored
64
.vscode/solid.code-snippets
vendored
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"component": {
|
||||
"scope": "typescriptreact,javascriptreact",
|
||||
"prefix": "comp",
|
||||
"body": [
|
||||
"import { Component } from \"solid-js\";",
|
||||
"",
|
||||
"const ${1:$TM_FILENAME_BASE}: Component<{}> = (p) => {",
|
||||
"\treturn (",
|
||||
"\t\t<div>",
|
||||
"\t\t\t${0}",
|
||||
"\t\t</div>",
|
||||
"\t);",
|
||||
"}",
|
||||
"",
|
||||
"export default ${1:$TM_FILENAME_BASE};"
|
||||
]
|
||||
},
|
||||
"component-with-css": {
|
||||
"scope": "typescriptreact,javascriptreact",
|
||||
"prefix": "css-comp",
|
||||
"body": [
|
||||
"import { Component } from \"solid-js\";",
|
||||
"import s from \"./${1:$TM_FILENAME_BASE}.module.scss\";",
|
||||
"",
|
||||
"const ${2:$TM_FILENAME_BASE}: Component<{}> = (p) => {",
|
||||
"\treturn (",
|
||||
"\t\t<div class={s.${2:$TM_FILENAME_BASE}} >",
|
||||
"\t\t\t${0}",
|
||||
"\t\t</div>",
|
||||
"\t);",
|
||||
"}",
|
||||
"",
|
||||
"export default ${2:$TM_FILENAME_BASE};"
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"scope": "typescriptreact,javascriptreact",
|
||||
"prefix": "provider",
|
||||
"body": [
|
||||
"import { ParentComponent, createContext, useContext } from \"solid-js\";",
|
||||
"",
|
||||
"const value = () => {",
|
||||
"\treturn {};",
|
||||
"}",
|
||||
"",
|
||||
"export type Value = ReturnType<typeof value>;",
|
||||
"",
|
||||
"const context = createContext<Value>();",
|
||||
"",
|
||||
"export const Provider: ParentComponent<{}> = (p) => {",
|
||||
"\treturn (",
|
||||
"\t\t<context.Provider value={value()}>",
|
||||
"\t\t\t{p.children}",
|
||||
"\t\t</context.Provider>",
|
||||
"\t);",
|
||||
"}",
|
||||
"",
|
||||
"export function useValue() {",
|
||||
"\treturn useContext(context) as Value;",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
351
.vscode/tasks.json
vendored
351
.vscode/tasks.json
vendored
@@ -1,172 +1,179 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "build",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"label": "rust: cargo build"
|
||||
},
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "fmt",
|
||||
"label": "rust: cargo fmt"
|
||||
},
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "check",
|
||||
"label": "rust: cargo check"
|
||||
},
|
||||
{
|
||||
"label": "start dev",
|
||||
"dependsOn": [
|
||||
"run core",
|
||||
"yarn: start frontend"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "yarn start",
|
||||
"label": "yarn: start frontend",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/frontend"
|
||||
},
|
||||
"presentation": {
|
||||
"group": "start"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "run",
|
||||
"label": "run core",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/core"
|
||||
},
|
||||
"presentation": {
|
||||
"group": "start"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "run",
|
||||
"label": "run periphery",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/periphery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "cargo install --path . && if pgrep periphery; then pkill periphery; fi && periphery --daemon --config-path ~/.monitor/local.periphery.config.toml",
|
||||
"label": "run periphery daemon",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/periphery"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "run",
|
||||
"label": "run cli",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/cli"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "run",
|
||||
"label": "run tests",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/tests"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "publish",
|
||||
"args": ["--allow-dirty"],
|
||||
"label": "publish monitor types",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/lib/types"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "publish",
|
||||
"label": "publish monitor client",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/lib/monitor_client"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "publish",
|
||||
"label": "publish monitor cli",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/cli"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "docker compose up -d",
|
||||
"label": "docker compose up",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/tests"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "docker compose down",
|
||||
"label": "docker compose down",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/tests"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "docker compose build",
|
||||
"label": "docker compose build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/tests"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "docker compose down && docker compose up -d",
|
||||
"label": "docker compose restart",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/tests"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "docker compose build && docker compose down && docker compose up -d",
|
||||
"label": "docker compose build and restart",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/tests"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "docker compose build periphery",
|
||||
"label": "docker compose build periphery",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/tests"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "typeshare ./lib/types --lang=typescript --output-file=./frontend/src/types.ts && typeshare ./core --lang=typescript --output-file=./frontend/src/util/client_types.ts",
|
||||
"label": "generate typescript types",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Run Core",
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"run",
|
||||
"-p",
|
||||
"komodo_core",
|
||||
"--release"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"KOMODO_CONFIG_PATH": "test.core.config.toml"
|
||||
}
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Build Core",
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"build",
|
||||
"-p",
|
||||
"komodo_core",
|
||||
"--release"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"KOMODO_CONFIG_PATH": "test.core.config.toml"
|
||||
}
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Run Periphery",
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"run",
|
||||
"-p",
|
||||
"komodo_periphery",
|
||||
"--release"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"KOMODO_CONFIG_PATH": "test.periphery.config.toml"
|
||||
}
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Build Periphery",
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"build",
|
||||
"-p",
|
||||
"komodo_periphery",
|
||||
"--release"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"KOMODO_CONFIG_PATH": "test.periphery.config.toml"
|
||||
}
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Run Backend",
|
||||
"dependsOn": [
|
||||
"Run Core",
|
||||
"Run Periphery"
|
||||
],
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Build TS Client Types",
|
||||
"type": "process",
|
||||
"command": "node",
|
||||
"args": [
|
||||
"./client/core/ts/generate_types.mjs"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Init TS Client",
|
||||
"type": "shell",
|
||||
"command": "yarn && yarn build && yarn link",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/client/core/ts",
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Init UI Client",
|
||||
"type": "shell",
|
||||
"command": "yarn link komodo_client && yarn install",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/ui",
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Init UI",
|
||||
"dependsOn": [
|
||||
"Build TS Client Types",
|
||||
"Init TS Client",
|
||||
"Init UI Client"
|
||||
],
|
||||
"dependsOrder": "sequence",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Build UI",
|
||||
"type": "shell",
|
||||
"command": "yarn build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/ui",
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Prepare UI For Run",
|
||||
"type": "shell",
|
||||
"command": "cp -r ./client/core/ts/dist/. ui/public/client/.",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}",
|
||||
},
|
||||
"dependsOn": [
|
||||
"Build TS Client Types",
|
||||
"Build UI"
|
||||
],
|
||||
"dependsOrder": "sequence",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Run UI",
|
||||
"type": "shell",
|
||||
"command": "yarn dev",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/ui",
|
||||
},
|
||||
"dependsOn": ["Prepare UI For Run"],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Init",
|
||||
"dependsOn": [
|
||||
"Build Backend",
|
||||
"Init UI"
|
||||
],
|
||||
"dependsOrder": "sequence",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Run Komodo",
|
||||
"dependsOn": [
|
||||
"Run Core",
|
||||
"Run Periphery",
|
||||
"Run UI"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
]
|
||||
}
|
||||
6645
Cargo.lock
generated
6645
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
152
Cargo.toml
152
Cargo.toml
@@ -1,14 +1,138 @@
|
||||
[workspace]
|
||||
|
||||
members = [
|
||||
"cli",
|
||||
"core",
|
||||
"periphery",
|
||||
"tests",
|
||||
"lib/axum_oauth2",
|
||||
"lib/db_client",
|
||||
"lib/helpers",
|
||||
"lib/periphery_client",
|
||||
"lib/types",
|
||||
"lib/monitor_client"
|
||||
]
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"bin/*",
|
||||
"lib/*",
|
||||
"client/core/rs",
|
||||
"client/periphery/rs",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "2.0.0-dev-120"
|
||||
edition = "2024"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/moghtech/komodo"
|
||||
homepage = "https://komo.do"
|
||||
|
||||
[profile.release]
|
||||
strip = "debuginfo"
|
||||
|
||||
[workspace.dependencies]
|
||||
# LOCAL
|
||||
komodo_client = { path = "client/core/rs" }
|
||||
periphery_client = { path = "client/periphery/rs" }
|
||||
environment = { path = "lib/environment" }
|
||||
interpolate = { path = "lib/interpolate" }
|
||||
formatting = { path = "lib/formatting" }
|
||||
transport = { path = "lib/transport" }
|
||||
database = { path = "lib/database" }
|
||||
encoding = { path = "lib/encoding" }
|
||||
command = { path = "lib/command" }
|
||||
git = { path = "lib/git" }
|
||||
|
||||
# MOGH
|
||||
slack = { version = "2.0.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
|
||||
mogh_error = { version = "1.0.3", default-features = false }
|
||||
derive_default_builder = "0.1.8"
|
||||
async_timing_util = "1.1.0"
|
||||
mogh_auth_client = "1.2.2"
|
||||
mogh_auth_server = "1.2.6"
|
||||
mogh_secret_file = "1.0.1"
|
||||
mogh_validations = "1.0.1"
|
||||
mogh_rate_limit = "1.0.1"
|
||||
partial_derive2 = "0.4.5"
|
||||
mongo_indexed = "2.0.2"
|
||||
mogh_resolver = "1.0.0"
|
||||
mogh_config = "1.0.2"
|
||||
mogh_logger = "1.3.1"
|
||||
mogh_server = "1.4.4"
|
||||
toml_pretty = "2.0.0"
|
||||
mogh_cache = "1.1.1"
|
||||
mogh_pki = "1.1.0"
|
||||
mungos = "3.2.2"
|
||||
svi = "1.2.0"
|
||||
|
||||
# ASYNC
|
||||
reqwest = { version = "0.13.2", default-features = false, features = ["json", "stream", "form", "query", "rustls"] }
|
||||
tokio = { version = "1.49.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7.18", features = ["io", "codec"] }
|
||||
tokio-stream = { version = "0.1.18", features = ["sync"] }
|
||||
pin-project-lite = "0.2.16"
|
||||
futures-util = "0.3.32"
|
||||
arc-swap = "1.8.2"
|
||||
|
||||
# SERVER
|
||||
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-native-roots"] }
|
||||
axum = { version = "0.8.8", features = ["ws", "json", "macros"] }
|
||||
axum-extra = { version = "0.12.5", features = ["typed-header"] }
|
||||
|
||||
# OPENAPI
|
||||
utoipa-scalar = { version = "0.3.0", features = ["axum"] }
|
||||
utoipa = "5.4.0"
|
||||
|
||||
# SER/DE
|
||||
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||
indexmap = { version = "2.13.0", features = ["serde"] }
|
||||
serde = { version = "1.0.227", features = ["derive"] }
|
||||
strum = { version = "0.28.0", features = ["derive"] }
|
||||
bson = { version = "2.15.0" } # must keep in sync with mongodb version
|
||||
toml = "0.9.11"
|
||||
serde_yaml_ng = "0.10.0"
|
||||
serde_json = "1.0.149"
|
||||
serde_qs = "1.0.0"
|
||||
url = "2.5.8"
|
||||
|
||||
# ERROR
|
||||
anyhow = "1.0.102"
|
||||
thiserror = "2.0.18"
|
||||
|
||||
# LOGGING
|
||||
tracing = "0.1.44"
|
||||
|
||||
# CONFIG
|
||||
clap = { version = "4.5.60", features = ["derive"] }
|
||||
dotenvy = "0.15.7"
|
||||
envy = "0.4.2"
|
||||
|
||||
# CRYPTO / AUTH
|
||||
uuid = { version = "1.21.0", features = ["v4", "fast-rng", "serde"] }
|
||||
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
|
||||
data-encoding = "2.10.0"
|
||||
urlencoding = "2.1.3"
|
||||
bcrypt = "0.18.0"
|
||||
hmac = "0.12.1"
|
||||
sha1 = "0.10.6"
|
||||
sha2 = "0.10.9"
|
||||
rand = "0.10.0"
|
||||
hex = "0.4.3"
|
||||
|
||||
# SYSTEM
|
||||
hickory-resolver = "0.25.2"
|
||||
portable-pty = "0.9.0"
|
||||
shell-escape = "0.1.5"
|
||||
crossterm = "0.29.0"
|
||||
bollard = "0.20.1"
|
||||
sysinfo = "0.38.2"
|
||||
shlex = "1.3.0"
|
||||
|
||||
# CLOUD
|
||||
aws-config = "1.8.14"
|
||||
aws-sdk-ec2 = "1.214.0"
|
||||
aws-credential-types = "1.2.13"
|
||||
|
||||
## CRON
|
||||
english-to-cron = "0.1.7"
|
||||
chrono-tz = "0.10.4"
|
||||
chrono = "0.4.44"
|
||||
croner = "3.0.1"
|
||||
|
||||
# MISC
|
||||
async-compression = { version = "0.4.41", features = ["tokio", "gzip"] }
|
||||
derive_builder = "0.20.2"
|
||||
comfy-table = "7.2.2"
|
||||
typeshare = "1.0.5"
|
||||
wildcard = "0.3.0"
|
||||
colored = "3.1.1"
|
||||
bytes = "1.11.1"
|
||||
regex = "1.12.3"
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
FROM rust:latest as builder
|
||||
WORKDIR /builder
|
||||
|
||||
COPY ./core ./core
|
||||
|
||||
COPY ./lib/types ./lib/types
|
||||
COPY ./lib/helpers ./lib/helpers
|
||||
|
||||
COPY ./lib/db_client ./lib/db_client
|
||||
COPY ./lib/periphery_client ./lib/periphery_client
|
||||
COPY ./lib/axum_oauth2 ./lib/axum_oauth2
|
||||
|
||||
RUN cd core && cargo build --release
|
||||
|
||||
FROM gcr.io/distroless/cc
|
||||
|
||||
COPY ./frontend/build /frontend
|
||||
|
||||
COPY --from=builder /builder/core/target/release/core /
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
CMD ["./core"]
|
||||
@@ -1,22 +0,0 @@
|
||||
FROM rust:latest as builder
|
||||
WORKDIR /builder
|
||||
|
||||
COPY ./periphery ./periphery
|
||||
|
||||
COPY ./lib/types ./lib/types
|
||||
COPY ./lib/helpers ./lib/helpers
|
||||
|
||||
RUN cd periphery && cargo build --release
|
||||
|
||||
FROM debian:stable-slim
|
||||
|
||||
ARG DEPS_INSTALLER
|
||||
|
||||
COPY ./${DEPS_INSTALLER}.sh ./
|
||||
RUN sh ./${DEPS_INSTALLER}.sh
|
||||
|
||||
COPY --from=builder /builder/periphery/target/release/periphery /usr/local/bin/periphery
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD "periphery"
|
||||
2
action/build.ts
Normal file
2
action/build.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import { run } from "./run.ts";
|
||||
await run("build-komodo");
|
||||
5
action/deno.json
Normal file
5
action/deno.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"imports": {
|
||||
"@std/toml": "jsr:@std/toml"
|
||||
}
|
||||
}
|
||||
4
action/deploy-fe.ts
Normal file
4
action/deploy-fe.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
const cmd = "km run -y action deploy-komodo-fe-change";
|
||||
new Deno.Command("bash", {
|
||||
args: ["-c", cmd],
|
||||
}).spawn();
|
||||
2
action/deploy.ts
Executable file
2
action/deploy.ts
Executable file
@@ -0,0 +1,2 @@
|
||||
import { run } from "./run.ts";
|
||||
await run("deploy-komodo");
|
||||
52
action/run.ts
Normal file
52
action/run.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as TOML from "@std/toml";
|
||||
|
||||
export const run = async (action: string) => {
|
||||
const branch = await new Deno.Command("bash", {
|
||||
args: ["-c", "git rev-parse --abbrev-ref HEAD"],
|
||||
})
|
||||
.output()
|
||||
.then((r) => new TextDecoder("utf-8").decode(r.stdout).trim());
|
||||
|
||||
const cargo_toml_str = await Deno.readTextFile("Cargo.toml");
|
||||
const prev_version = (
|
||||
TOML.parse(cargo_toml_str) as {
|
||||
workspace: { package: { version: string } };
|
||||
}
|
||||
).workspace.package.version;
|
||||
|
||||
const [version, tag, count] = prev_version.split("-");
|
||||
const next_count = Number(count) + 1;
|
||||
|
||||
const next_version = `${version}-${tag}-${next_count}`;
|
||||
|
||||
await Deno.writeTextFile(
|
||||
"Cargo.toml",
|
||||
cargo_toml_str.replace(
|
||||
`version = "${prev_version}"`,
|
||||
`version = "${next_version}"`
|
||||
)
|
||||
);
|
||||
|
||||
// Cargo check first here to make sure lock file is updated before commit.
|
||||
const cmd = `
|
||||
cargo check
|
||||
echo ""
|
||||
|
||||
git add --all
|
||||
git commit --all --message "deploy ${version}-${tag}-${next_count}"
|
||||
|
||||
echo ""
|
||||
git push
|
||||
echo ""
|
||||
|
||||
km run -y action ${action} "KOMODO_BRANCH=${branch}&KOMODO_VERSION=${version}&KOMODO_TAG=${tag}-${next_count}"
|
||||
`
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0 && !line.startsWith("//"))
|
||||
.join(" && ");
|
||||
|
||||
new Deno.Command("bash", {
|
||||
args: ["-c", cmd],
|
||||
}).spawn();
|
||||
};
|
||||
32
bin/binaries.Dockerfile
Normal file
32
bin/binaries.Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
||||
## Builds the Komodo Core, Periphery, and Util binaries
|
||||
## for a specific architecture. Requires OpenSSL 3 or later.
|
||||
|
||||
FROM rust:1.93.1-bookworm AS builder
|
||||
RUN cargo install cargo-strip
|
||||
|
||||
WORKDIR /builder
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY ./lib ./lib
|
||||
COPY ./client/core/rs ./client/core/rs
|
||||
COPY ./client/periphery ./client/periphery
|
||||
COPY ./bin/core ./bin/core
|
||||
COPY ./bin/periphery ./bin/periphery
|
||||
COPY ./bin/cli ./bin/cli
|
||||
|
||||
# Compile bin
|
||||
RUN \
|
||||
cargo build -p komodo_core --release && \
|
||||
cargo build -p komodo_periphery --release && \
|
||||
cargo build -p komodo_cli --release && \
|
||||
cargo strip
|
||||
|
||||
# Copy just the binaries to scratch image
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /builder/target/release/core /core
|
||||
COPY --from=builder /builder/target/release/periphery /periphery
|
||||
COPY --from=builder /builder/target/release/km /km
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
|
||||
LABEL org.opencontainers.image.description="Komodo Binaries"
|
||||
LABEL org.opencontainers.image.licenses="GPL-3.0"
|
||||
36
bin/chef.binaries.Dockerfile
Normal file
36
bin/chef.binaries.Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
||||
## Builds the Komodo Core, Periphery, and Util binaries
|
||||
## for a specific architecture. Requires OpenSSL 3 or later.
|
||||
|
||||
## Uses chef for dependency caching to help speed up back-to-back builds.
|
||||
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1.93.1-bookworm AS chef
|
||||
WORKDIR /builder
|
||||
|
||||
# Plan just the RECIPE to see if things have changed
|
||||
FROM chef AS planner
|
||||
COPY . .
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM chef AS builder
|
||||
RUN cargo install cargo-strip
|
||||
COPY --from=planner /builder/recipe.json recipe.json
|
||||
# Build JUST dependencies - cached layer
|
||||
RUN cargo chef cook --release --recipe-path recipe.json
|
||||
# NOW copy again (this time into builder) and build app
|
||||
COPY . .
|
||||
RUN \
|
||||
cargo build --release --bin core && \
|
||||
cargo build --release --bin periphery && \
|
||||
cargo build --release --bin km && \
|
||||
cargo strip
|
||||
|
||||
# Copy just the binaries to scratch image
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /builder/target/release/core /core
|
||||
COPY --from=builder /builder/target/release/periphery /periphery
|
||||
COPY --from=builder /builder/target/release/km /km
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
|
||||
LABEL org.opencontainers.image.description="Komodo Binaries"
|
||||
LABEL org.opencontainers.image.licenses="GPL-3.0"
|
||||
39
bin/cli/Cargo.toml
Normal file
39
bin/cli/Cargo.toml
Normal file
@@ -0,0 +1,39 @@
|
||||
[package]
|
||||
name = "komodo_cli"
|
||||
description = "Command line tool for Komodo"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "km"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# local
|
||||
komodo_client = { workspace = true, features = ["cli"] }
|
||||
mogh_secret_file.workspace = true
|
||||
mogh_pki.workspace = true
|
||||
database.workspace = true
|
||||
mogh_config.workspace = true
|
||||
mogh_logger.workspace = true
|
||||
# external
|
||||
futures-util.workspace = true
|
||||
comfy-table.workspace = true
|
||||
tokio-util.workspace = true
|
||||
serde_json.workspace = true
|
||||
crossterm.workspace = true
|
||||
serde_qs.workspace = true
|
||||
wildcard.workspace = true
|
||||
tracing.workspace = true
|
||||
colored.workspace = true
|
||||
dotenvy.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
clap.workspace = true
|
||||
envy.workspace = true
|
||||
118
bin/cli/README.md
Normal file
118
bin/cli/README.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Komodo CLI
|
||||
|
||||
Komodo CLI is a tool to execute actions on your Komodo instance from shell scripts.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
cargo install komodo_cli
|
||||
```
|
||||
|
||||
Note: On Ubuntu, also requires `apt install build-essential pkg-config libssl-dev`.
|
||||
|
||||
## Usage
|
||||
|
||||
### Credentials
|
||||
|
||||
Configure a file `~/.config/komodo/creds.toml` file with contents:
|
||||
```toml
|
||||
url = "https://your.komodo.address"
|
||||
key = "YOUR-API-KEY"
|
||||
secret = "YOUR-API-SECRET"
|
||||
```
|
||||
|
||||
Note. You can specify a different creds file by using `--creds ./other/path.toml`.
|
||||
You can also bypass using any file and pass the information using `--url`, `--key`, `--secret`:
|
||||
|
||||
```sh
|
||||
komodo --url "https://your.komodo.address" --key "YOUR-API-KEY" --secret "YOUR-API-SECRET" ...
|
||||
```
|
||||
|
||||
### Run Executions
|
||||
|
||||
```sh
|
||||
# Triggers an example build
|
||||
komodo execute run-build test_build
|
||||
```
|
||||
|
||||
#### Manual
|
||||
`komodo --help`
|
||||
```md
|
||||
Command line tool to execute Komodo actions
|
||||
|
||||
Usage: komodo [OPTIONS] <COMMAND>
|
||||
|
||||
Commands:
|
||||
execute Runs an execution
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
--creds <CREDS> The path to a creds file [default: /Users/max/.config/komodo/creds.toml]
|
||||
--url <URL> Pass url in args instead of creds file
|
||||
--key <KEY> Pass api key in args instead of creds file
|
||||
--secret <SECRET> Pass api secret in args instead of creds file
|
||||
-y, --yes Always continue on user confirmation prompts
|
||||
-h, --help Print help (see more with '--help')
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
`komodo execute --help`
|
||||
```md
|
||||
Runs an execution
|
||||
|
||||
Usage: komodo execute <COMMAND>
|
||||
|
||||
Commands:
|
||||
none The "null" execution. Does nothing
|
||||
run-procedure Runs the target procedure. Response: [Update]
|
||||
run-build Runs the target build. Response: [Update]
|
||||
cancel-build Cancels the target build. Only does anything if the build is `building` when called. Response: [Update]
|
||||
deploy Deploys the container for the target deployment. Response: [Update]
|
||||
start-deployment Starts the container for the target deployment. Response: [Update]
|
||||
restart-deployment Restarts the container for the target deployment. Response: [Update]
|
||||
pause-deployment Pauses the container for the target deployment. Response: [Update]
|
||||
unpause-deployment Unpauses the container for the target deployment. Response: [Update]
|
||||
stop-deployment Stops the container for the target deployment. Response: [Update]
|
||||
destroy-deployment Stops and destroys the container for the target deployment. Reponse: [Update]
|
||||
clone-repo Clones the target repo. Response: [Update]
|
||||
pull-repo Pulls the target repo. Response: [Update]
|
||||
build-repo Builds the target repo, using the attached builder. Response: [Update]
|
||||
cancel-repo-build Cancels the target repo build. Only does anything if the repo build is `building` when called. Response: [Update]
|
||||
start-container Starts the container on the target server. Response: [Update]
|
||||
restart-container Restarts the container on the target server. Response: [Update]
|
||||
pause-container Pauses the container on the target server. Response: [Update]
|
||||
unpause-container Unpauses the container on the target server. Response: [Update]
|
||||
stop-container Stops the container on the target server. Response: [Update]
|
||||
destroy-container Stops and destroys the container on the target server. Reponse: [Update]
|
||||
start-all-containers Starts all containers on the target server. Response: [Update]
|
||||
restart-all-containers Restarts all containers on the target server. Response: [Update]
|
||||
pause-all-containers Pauses all containers on the target server. Response: [Update]
|
||||
unpause-all-containers Unpauses all containers on the target server. Response: [Update]
|
||||
stop-all-containers Stops all containers on the target server. Response: [Update]
|
||||
prune-containers Prunes the docker containers on the target server. Response: [Update]
|
||||
delete-network Delete a docker network. Response: [Update]
|
||||
prune-networks Prunes the docker networks on the target server. Response: [Update]
|
||||
delete-image Delete a docker image. Response: [Update]
|
||||
prune-images Prunes the docker images on the target server. Response: [Update]
|
||||
delete-volume Delete a docker volume. Response: [Update]
|
||||
prune-volumes Prunes the docker volumes on the target server. Response: [Update]
|
||||
prune-system Prunes the docker system on the target server, including volumes. Response: [Update]
|
||||
run-sync Runs the target resource sync. Response: [Update]
|
||||
deploy-stack Deploys the target stack. `docker compose up`. Response: [Update]
|
||||
start-stack Starts the target stack. `docker compose start`. Response: [Update]
|
||||
restart-stack Restarts the target stack. `docker compose restart`. Response: [Update]
|
||||
pause-stack Pauses the target stack. `docker compose pause`. Response: [Update]
|
||||
unpause-stack Unpauses the target stack. `docker compose unpause`. Response: [Update]
|
||||
stop-stack Starts the target stack. `docker compose stop`. Response: [Update]
|
||||
destroy-stack Destoys the target stack. `docker compose down`. Response: [Update]
|
||||
sleep
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### --yes
|
||||
|
||||
You can use `--yes` to avoid any human prompt to continue, for use in automated environments.
|
||||
|
||||
25
bin/cli/aio.Dockerfile
Normal file
25
bin/cli/aio.Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM rust:1.93.1-bullseye AS builder
|
||||
RUN cargo install cargo-strip
|
||||
|
||||
WORKDIR /builder
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY ./lib ./lib
|
||||
COPY ./client/core/rs ./client/core/rs
|
||||
COPY ./client/periphery ./client/periphery
|
||||
COPY ./bin/cli ./bin/cli
|
||||
|
||||
# Compile bin
|
||||
RUN cargo build -p komodo_cli --release && cargo strip
|
||||
|
||||
# Copy binaries to distroless base
|
||||
FROM gcr.io/distroless/cc
|
||||
|
||||
COPY --from=builder /builder/target/release/km /usr/local/bin/km
|
||||
|
||||
ENV KOMODO_CLI_CONFIG_PATHS="/config"
|
||||
|
||||
CMD [ "km" ]
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
|
||||
LABEL org.opencontainers.image.description="Komodo CLI"
|
||||
LABEL org.opencontainers.image.licenses="GPL-3.0"
|
||||
135
bin/cli/docs/copy-database.md
Normal file
135
bin/cli/docs/copy-database.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Copy Database Utility
|
||||
|
||||
Copy the Komodo database contents between running, mongo-compatible databases.
|
||||
Can be used to move between MongoDB / FerretDB, or upgrade from FerretDB v1 to v2.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
|
||||
copy_database:
|
||||
image: ghcr.io/moghtech/komodo-cli
|
||||
command: km database copy -y
|
||||
environment:
|
||||
KOMODO_DATABASE_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@source:27017
|
||||
KOMODO_DATABASE_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo}
|
||||
KOMODO_CLI_DATABASE_TARGET_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@target:27017
|
||||
KOMODO_CLI_DATABASE_TARGET_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo}
|
||||
|
||||
```
|
||||
|
||||
## FerretDB v2 Update Guide
|
||||
|
||||
Up to Komodo 1.17.5, users who wanted to use Postgres / Sqlite were instructed to deploy FerretDB v1.
|
||||
Now that v2 is out however, v1 will go largely unsupported. Users are recommended to migrate to v2 for
|
||||
the best performance and ongoing support / updates, however the internal data structures
|
||||
have changed and this cannot be done in-place.
|
||||
|
||||
Also note that FerretDB v2 no longer supports Sqlite, and only supports
|
||||
a [customized Postgres distribution](https://docs.ferretdb.io/installation/documentdb/docker/).
|
||||
Nonetheless, it remains a solid option for hosts which [do not support mongo](https://github.com/moghtech/komodo/issues/59).
|
||||
|
||||
Also note, the same basic process outlined below can also be used to move between MongoDB and FerretDB, just replace FerretDB v2
|
||||
with the database you wish to move to.
|
||||
|
||||
### **Step 1**: *Add* the new database to the top of your existing Komodo compose file.
|
||||
|
||||
**Don't forget to also add the new volumes.**
|
||||
|
||||
```yaml
|
||||
## In Komodo compose.yaml
|
||||
services:
|
||||
postgres2:
|
||||
# Recommended: Pin to a specific version
|
||||
# https://github.com/FerretDB/documentdb/pkgs/container/postgres-documentdb
|
||||
image: ghcr.io/ferretdb/postgres-documentdb
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
restart: unless-stopped
|
||||
# ports:
|
||||
# - 5432:5432
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: ${KOMODO_DB_USERNAME}
|
||||
POSTGRES_PASSWORD: ${KOMODO_DB_PASSWORD}
|
||||
POSTGRES_DB: postgres # Do not change
|
||||
|
||||
ferretdb2:
|
||||
# Recommended: Pin to a specific version
|
||||
# https://github.com/FerretDB/FerretDB/pkgs/container/ferretdb
|
||||
image: ghcr.io/ferretdb/ferretdb
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- postgres2
|
||||
# ports:
|
||||
# - 27017:27017
|
||||
volumes:
|
||||
- ferretdb-state:/state
|
||||
environment:
|
||||
FERRETDB_POSTGRESQL_URL: postgres://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@postgres2:5432/postgres
|
||||
|
||||
...(unchanged)
|
||||
|
||||
volumes:
|
||||
...(unchanged)
|
||||
postgres-data:
|
||||
ferretdb-state:
|
||||
```
|
||||
|
||||
### **Step 2**: *Add* the database copy utility to Komodo compose file.
|
||||
|
||||
The SOURCE_URI points to the existing database, ie the old FerretDB v1, and it depends
|
||||
on whether it was deployed using Postgres or Sqlite. The example below uses the Postgres one,
|
||||
but if you use Sqlite it should just be something like `mongodb://ferretdb:27017`.
|
||||
|
||||
```yaml
|
||||
## In Komodo compose.yaml
|
||||
services:
|
||||
...(new database)
|
||||
|
||||
copy_database:
|
||||
image: ghcr.io/moghtech/komodo-cli
|
||||
command: km database copy -y
|
||||
environment:
|
||||
KOMODO_DATABASE_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@ferretdb:27017/${KOMODO_DATABASE_DB_NAME:-komodo}?authMechanism=PLAIN
|
||||
KOMODO_DATABASE_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo}
|
||||
KOMODO_CLI_DATABASE_TARGET_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@ferretdb2:27017
|
||||
KOMODO_CLI_DATABASE_TARGET_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo}
|
||||
|
||||
...(unchanged)
|
||||
```
|
||||
|
||||
### **Step 3**: *Compose Up* the new additions
|
||||
|
||||
Run `docker compose -p komodo --env-file compose.env -f xxxxx.compose.yaml up -d`, filling in the name of your compose.yaml.
|
||||
This will start up both the old and new database, and copy the data to the new one.
|
||||
|
||||
Wait a few moments for the `copy_database` service to finish. When it exits,
|
||||
confirm the logs show the data was moved successfully, and move on to the next step.
|
||||
|
||||
### **Step 4**: Point Komodo Core to the new database
|
||||
|
||||
In your Komodo compose.yaml, first *comment out* the `copy_database` service and old ferretdb v1 service/s.
|
||||
Then update the `core` service environment to point to `ferretdb2`.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
...
|
||||
|
||||
core:
|
||||
...(unchanged)
|
||||
environment:
|
||||
KOMODO_DATABASE_ADDRESS: ferretdb2:27017
|
||||
KOMODO_DATABASE_USERNAME: ${KOMODO_DB_USERNAME}
|
||||
KOMODO_DATABASE_PASSWORD: ${KOMODO_DB_PASSWORD}
|
||||
```
|
||||
|
||||
### **Step 5**: Final *Compose Up*
|
||||
|
||||
Repeat the same `docker compose` command as before to apply the changes, and then try navigating to your Komodo web page.
|
||||
If it works, congrats, **you are done**. You can clean up the compose file if you would like, removing the old volumes etc.
|
||||
|
||||
If it does not work, check the logs for any obvious issues, and if necessary you can undo the previous steps
|
||||
to go back to using the previous database.
|
||||
29
bin/cli/multi-arch.Dockerfile
Normal file
29
bin/cli/multi-arch.Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile).
|
||||
## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.
|
||||
|
||||
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
|
||||
ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64
|
||||
ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64
|
||||
|
||||
# This is required to work with COPY --from
|
||||
FROM ${X86_64_BINARIES} AS x86_64
|
||||
FROM ${AARCH64_BINARIES} AS aarch64
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
## Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM.
|
||||
COPY --from=x86_64 /km /app/arch/linux/amd64
|
||||
COPY --from=aarch64 /km /app/arch/linux/arm64
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
RUN mv /app/arch/${TARGETPLATFORM} /usr/local/bin/km && rm -r /app/arch
|
||||
|
||||
ENV KOMODO_CLI_CONFIG_PATHS="/config"
|
||||
|
||||
CMD [ "km" ]
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
|
||||
LABEL org.opencontainers.image.description="Komodo CLI"
|
||||
LABEL org.opencontainers.image.licenses="GPL-3.0"
|
||||
4
bin/cli/runfile.toml
Normal file
4
bin/cli/runfile.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[install-cli]
|
||||
alias = "ic"
|
||||
description = "installs the komodo-cli, available on the command line as 'km'"
|
||||
cmd = "cargo install --path ."
|
||||
18
bin/cli/single-arch.Dockerfile
Normal file
18
bin/cli/single-arch.Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).
|
||||
|
||||
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
|
||||
|
||||
# This is required to work with COPY --from
|
||||
FROM ${BINARIES_IMAGE} AS binaries
|
||||
|
||||
FROM gcr.io/distroless/cc
|
||||
|
||||
COPY --from=binaries /km /usr/local/bin/km
|
||||
|
||||
ENV KOMODO_CLI_CONFIG_PATHS="/config"
|
||||
|
||||
CMD [ "km" ]
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
|
||||
LABEL org.opencontainers.image.description="Komodo CLI"
|
||||
LABEL org.opencontainers.image.licenses="GPL-3.0"
|
||||
314
bin/cli/src/command/container.rs
Normal file
314
bin/cli/src/command/container.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use anyhow::Context;
|
||||
use colored::Colorize;
|
||||
use comfy_table::{Attribute, Cell, Color};
|
||||
use futures_util::{
|
||||
FutureExt, TryStreamExt, stream::FuturesUnordered,
|
||||
};
|
||||
use komodo_client::{
|
||||
api::read::{
|
||||
InspectDockerContainer, ListAllDockerContainers, ListServers,
|
||||
},
|
||||
entities::{
|
||||
config::cli::args::container::{
|
||||
Container, ContainerCommand, InspectContainer,
|
||||
},
|
||||
docker::{
|
||||
self,
|
||||
container::{ContainerListItem, ContainerStateStatusEnum},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
command::{
|
||||
PrintTable, clamp_sha, matches_wildcards, parse_wildcards,
|
||||
print_items,
|
||||
},
|
||||
config::cli_config,
|
||||
};
|
||||
|
||||
pub async fn handle(container: &Container) -> anyhow::Result<()> {
|
||||
match &container.command {
|
||||
None => list_containers(container).await,
|
||||
Some(ContainerCommand::Inspect(inspect)) => {
|
||||
inspect_container(inspect).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_containers(
|
||||
Container {
|
||||
all,
|
||||
down,
|
||||
links,
|
||||
reverse,
|
||||
containers: names,
|
||||
images,
|
||||
networks,
|
||||
servers,
|
||||
format,
|
||||
command: _,
|
||||
}: &Container,
|
||||
) -> anyhow::Result<()> {
|
||||
let client = super::komodo_client().await?;
|
||||
let (server_map, containers) = tokio::try_join!(
|
||||
client
|
||||
.read(ListServers::default())
|
||||
.map(|res| res.map(|res| res
|
||||
.into_iter()
|
||||
.map(|s| (s.id.clone(), s))
|
||||
.collect::<HashMap<_, _>>())),
|
||||
client.read(ListAllDockerContainers {
|
||||
servers: Default::default(),
|
||||
containers: Default::default(),
|
||||
}),
|
||||
)?;
|
||||
|
||||
// (Option<Server Name>, Container)
|
||||
let containers = containers.into_iter().map(|c| {
|
||||
let server = if let Some(server_id) = c.server_id.as_ref()
|
||||
&& let Some(server) = server_map.get(server_id)
|
||||
{
|
||||
server
|
||||
} else {
|
||||
return (None, c);
|
||||
};
|
||||
(Some(server.name.as_str()), c)
|
||||
});
|
||||
|
||||
let names = parse_wildcards(names);
|
||||
let servers = parse_wildcards(servers);
|
||||
let images = parse_wildcards(images);
|
||||
let networks = parse_wildcards(networks);
|
||||
|
||||
let mut containers = containers
|
||||
.into_iter()
|
||||
.filter(|(server_name, c)| {
|
||||
let state_check = if *all {
|
||||
true
|
||||
} else if *down {
|
||||
!matches!(c.state, ContainerStateStatusEnum::Running)
|
||||
} else {
|
||||
matches!(c.state, ContainerStateStatusEnum::Running)
|
||||
};
|
||||
let network_check = matches_wildcards(
|
||||
&networks,
|
||||
&c.network_mode
|
||||
.as_deref()
|
||||
.map(|n| vec![n])
|
||||
.unwrap_or_default(),
|
||||
) || matches_wildcards(
|
||||
&networks,
|
||||
&c.networks.iter().map(String::as_str).collect::<Vec<_>>(),
|
||||
);
|
||||
state_check
|
||||
&& network_check
|
||||
&& matches_wildcards(&names, &[c.name.as_str()])
|
||||
&& matches_wildcards(
|
||||
&servers,
|
||||
&server_name
|
||||
.as_deref()
|
||||
.map(|i| vec![i])
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
&& matches_wildcards(
|
||||
&images,
|
||||
&c.image.as_deref().map(|i| vec![i]).unwrap_or_default(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
containers.sort_by(|(a_s, a), (b_s, b)| {
|
||||
a.state
|
||||
.cmp(&b.state)
|
||||
.then(a.name.cmp(&b.name))
|
||||
.then(a_s.cmp(b_s))
|
||||
.then(a.network_mode.cmp(&b.network_mode))
|
||||
.then(a.image.cmp(&b.image))
|
||||
});
|
||||
if *reverse {
|
||||
containers.reverse();
|
||||
}
|
||||
print_items(containers, *format, *links)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn inspect_container(
|
||||
inspect: &InspectContainer,
|
||||
) -> anyhow::Result<()> {
|
||||
let client = super::komodo_client().await?;
|
||||
let (server_map, mut containers) = tokio::try_join!(
|
||||
client
|
||||
.read(ListServers::default())
|
||||
.map(|res| res.map(|res| res
|
||||
.into_iter()
|
||||
.map(|s| (s.id.clone(), s))
|
||||
.collect::<HashMap<_, _>>())),
|
||||
client.read(ListAllDockerContainers {
|
||||
servers: Default::default(),
|
||||
containers: Default::default()
|
||||
}),
|
||||
)?;
|
||||
|
||||
containers.iter_mut().for_each(|c| {
|
||||
let Some(server_id) = c.server_id.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(server) = server_map.get(server_id) else {
|
||||
c.server_id = Some(String::from("Unknown"));
|
||||
return;
|
||||
};
|
||||
c.server_id = Some(server.name.clone());
|
||||
});
|
||||
|
||||
let names = [inspect.container.to_string()];
|
||||
let names = parse_wildcards(&names);
|
||||
let servers = parse_wildcards(&inspect.servers);
|
||||
|
||||
let mut containers = containers
|
||||
.into_iter()
|
||||
.filter(|c| {
|
||||
matches_wildcards(&names, &[c.name.as_str()])
|
||||
&& matches_wildcards(
|
||||
&servers,
|
||||
&c.server_id
|
||||
.as_deref()
|
||||
.map(|i| vec![i])
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
})
|
||||
.map(|c| async move {
|
||||
client
|
||||
.read(InspectDockerContainer {
|
||||
container: c.name,
|
||||
server: c.server_id.context("No server...")?,
|
||||
})
|
||||
.await
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
|
||||
containers.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
match containers.len() {
|
||||
0 => {
|
||||
println!(
|
||||
"{}: Did not find any containers matching '{}'",
|
||||
"INFO".green(),
|
||||
inspect.container.bold()
|
||||
);
|
||||
}
|
||||
1 => {
|
||||
println!("{}", serialize_container(inspect, &containers[0])?);
|
||||
}
|
||||
_ => {
|
||||
let containers = containers
|
||||
.iter()
|
||||
.map(|c| serialize_container(inspect, c))
|
||||
.collect::<anyhow::Result<Vec<_>>>()?
|
||||
.join("\n");
|
||||
println!("{containers}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_container(
|
||||
inspect: &InspectContainer,
|
||||
container: &docker::container::Container,
|
||||
) -> anyhow::Result<String> {
|
||||
let res = if inspect.state {
|
||||
serde_json::to_string_pretty(&container.state)
|
||||
} else if inspect.mounts {
|
||||
serde_json::to_string_pretty(&container.mounts)
|
||||
} else if inspect.host_config {
|
||||
serde_json::to_string_pretty(&container.host_config)
|
||||
} else if inspect.config {
|
||||
serde_json::to_string_pretty(&container.config)
|
||||
} else if inspect.network_settings {
|
||||
serde_json::to_string_pretty(&container.network_settings)
|
||||
} else {
|
||||
serde_json::to_string_pretty(container)
|
||||
}
|
||||
.context("Failed to serialize items to JSON")?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
// (Option<Server Name>, Container)
|
||||
impl PrintTable for (Option<&'_ str>, ContainerListItem) {
|
||||
fn header(links: bool) -> &'static [&'static str] {
|
||||
if links {
|
||||
&[
|
||||
"Container",
|
||||
"State",
|
||||
"Server",
|
||||
"Ports",
|
||||
"Networks",
|
||||
"Image",
|
||||
"Link",
|
||||
]
|
||||
} else {
|
||||
&["Container", "State", "Server", "Ports", "Networks", "Image"]
|
||||
}
|
||||
}
|
||||
fn row(self, links: bool) -> Vec<Cell> {
|
||||
let color = match self.1.state {
|
||||
ContainerStateStatusEnum::Running => Color::Green,
|
||||
ContainerStateStatusEnum::Paused => Color::DarkYellow,
|
||||
ContainerStateStatusEnum::Empty => Color::Grey,
|
||||
_ => Color::Red,
|
||||
};
|
||||
let mut networks = HashSet::new();
|
||||
if let Some(network) = self.1.network_mode {
|
||||
networks.insert(network);
|
||||
}
|
||||
for network in self.1.networks {
|
||||
networks.insert(network);
|
||||
}
|
||||
let mut networks = networks.into_iter().collect::<Vec<_>>();
|
||||
networks.sort();
|
||||
let mut ports = self
|
||||
.1
|
||||
.ports
|
||||
.into_iter()
|
||||
.flat_map(|p| p.public_port.map(|p| p.to_string()))
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
ports.sort();
|
||||
let ports = if ports.is_empty() {
|
||||
Cell::new("")
|
||||
} else {
|
||||
Cell::new(format!(":{}", ports.join(", :")))
|
||||
};
|
||||
|
||||
let image = self.1.image.as_deref().unwrap_or("Unknown");
|
||||
let mut res = vec![
|
||||
Cell::new(self.1.name.clone()).add_attribute(Attribute::Bold),
|
||||
Cell::new(self.1.state.to_string())
|
||||
.fg(color)
|
||||
.add_attribute(Attribute::Bold),
|
||||
Cell::new(self.0.unwrap_or("Unknown")),
|
||||
ports,
|
||||
Cell::new(networks.join(", ")),
|
||||
Cell::new(clamp_sha(image)),
|
||||
];
|
||||
if !links {
|
||||
return res;
|
||||
}
|
||||
let link = if let Some(server_id) = self.1.server_id {
|
||||
format!(
|
||||
"{}/servers/{server_id}/container/{}",
|
||||
cli_config().host,
|
||||
self.1.name
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
res.push(Cell::new(link));
|
||||
res
|
||||
}
|
||||
}
|
||||
371
bin/cli/src/command/database.rs
Normal file
371
bin/cli/src/command/database.rs
Normal file
@@ -0,0 +1,371 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Context;
|
||||
use colored::Colorize;
|
||||
use database::mungos::mongodb::bson::{Document, doc};
|
||||
use komodo_client::entities::{
|
||||
config::cli::args::database::DatabaseCommand, optional_string,
|
||||
};
|
||||
|
||||
use crate::{command::sanitize_uri, config::cli_config};
|
||||
|
||||
pub async fn handle(command: &DatabaseCommand) -> anyhow::Result<()> {
|
||||
match command {
|
||||
DatabaseCommand::Backup { yes, .. } => backup(*yes).await,
|
||||
DatabaseCommand::Restore {
|
||||
restore_folder,
|
||||
index,
|
||||
yes,
|
||||
..
|
||||
} => restore(restore_folder.as_deref(), *index, *yes).await,
|
||||
DatabaseCommand::Prune { yes, .. } => prune(*yes).await,
|
||||
DatabaseCommand::Copy { yes, index, .. } => {
|
||||
copy(*index, *yes).await
|
||||
}
|
||||
DatabaseCommand::V1Downgrade { yes } => v1_downgrade(*yes).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn backup(yes: bool) -> anyhow::Result<()> {
|
||||
let config = cli_config();
|
||||
|
||||
println!(
|
||||
"\n🦎 {} Database {} Utility 🦎",
|
||||
"Komodo".bold(),
|
||||
"Backup".green().bold()
|
||||
);
|
||||
println!(
|
||||
"\n{}\n",
|
||||
" - Backup all database contents to gzip compressed files."
|
||||
.dimmed()
|
||||
);
|
||||
if let Some(uri) = optional_string(&config.database.uri) {
|
||||
println!("{}: {}", " - Source URI".dimmed(), sanitize_uri(&uri));
|
||||
}
|
||||
if let Some(address) = optional_string(&config.database.address) {
|
||||
println!("{}: {address}", " - Source Address".dimmed());
|
||||
}
|
||||
if let Some(username) = optional_string(&config.database.username) {
|
||||
println!("{}: {username}", " - Source Username".dimmed());
|
||||
}
|
||||
println!(
|
||||
"{}: {}\n",
|
||||
" - Source Db Name".dimmed(),
|
||||
config.database.db_name,
|
||||
);
|
||||
println!(
|
||||
"{}: {:?}",
|
||||
" - Backups Folder".dimmed(),
|
||||
config.backups_folder
|
||||
);
|
||||
if config.max_backups == 0 {
|
||||
println!(
|
||||
"{}{}",
|
||||
" - Backup pruning".dimmed(),
|
||||
"disabled".red().dimmed()
|
||||
);
|
||||
} else {
|
||||
println!("{}: {}", " - Max Backups".dimmed(), config.max_backups);
|
||||
}
|
||||
|
||||
crate::command::wait_for_enter("start backup", yes)?;
|
||||
|
||||
let db = database::init(&config.database).await?;
|
||||
|
||||
database::utils::backup(&db, &config.backups_folder).await?;
|
||||
|
||||
// Early return if backup pruning disabled
|
||||
if config.max_backups == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Know that new backup was taken successfully at this point,
|
||||
// safe to prune old backup folders
|
||||
|
||||
prune_inner().await
|
||||
}
|
||||
|
||||
async fn restore(
|
||||
restore_folder: Option<&Path>,
|
||||
index: bool,
|
||||
yes: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let config = cli_config();
|
||||
|
||||
println!(
|
||||
"\n🦎 {} Database {} Utility 🦎",
|
||||
"Komodo".bold(),
|
||||
"Restore".purple().bold()
|
||||
);
|
||||
println!(
|
||||
"\n{}\n",
|
||||
" - Restores database contents from gzip compressed files."
|
||||
.dimmed()
|
||||
);
|
||||
if let Some(uri) = optional_string(&config.database_target.uri) {
|
||||
println!("{}: {}", " - Target URI".dimmed(), sanitize_uri(&uri));
|
||||
}
|
||||
if let Some(address) =
|
||||
optional_string(&config.database_target.address)
|
||||
{
|
||||
println!("{}: {address}", " - Target Address".dimmed());
|
||||
}
|
||||
if let Some(username) =
|
||||
optional_string(&config.database_target.username)
|
||||
{
|
||||
println!("{}: {username}", " - Target Username".dimmed());
|
||||
}
|
||||
println!(
|
||||
"{}: {}",
|
||||
" - Target Db Name".dimmed(),
|
||||
config.database_target.db_name,
|
||||
);
|
||||
if !index {
|
||||
println!(
|
||||
"{}: {}",
|
||||
" - Target Db Indexing".dimmed(),
|
||||
"DISABLED".red(),
|
||||
);
|
||||
}
|
||||
println!(
|
||||
"\n{}: {:?}",
|
||||
" - Backups Folder".dimmed(),
|
||||
config.backups_folder
|
||||
);
|
||||
if let Some(restore_folder) = restore_folder {
|
||||
println!("{}: {restore_folder:?}", " - Restore Folder".dimmed());
|
||||
}
|
||||
|
||||
crate::command::wait_for_enter("start restore", yes)?;
|
||||
|
||||
let db = if index {
|
||||
database::Client::new(&config.database_target).await?.db
|
||||
} else {
|
||||
database::init(&config.database_target).await?
|
||||
};
|
||||
|
||||
database::utils::restore(
|
||||
&db,
|
||||
&config.backups_folder,
|
||||
restore_folder,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn prune(yes: bool) -> anyhow::Result<()> {
|
||||
let config = cli_config();
|
||||
|
||||
println!(
|
||||
"\n🦎 {} Database {} Utility 🦎",
|
||||
"Komodo".bold(),
|
||||
"Backup Prune".cyan().bold()
|
||||
);
|
||||
println!(
|
||||
"\n{}\n",
|
||||
" - Prunes database backup folders when greater than the configured amount."
|
||||
.dimmed()
|
||||
);
|
||||
println!(
|
||||
"{}: {:?}",
|
||||
" - Backups Folder".dimmed(),
|
||||
config.backups_folder
|
||||
);
|
||||
if config.max_backups == 0 {
|
||||
println!(
|
||||
"{}{}",
|
||||
" - Backup pruning".dimmed(),
|
||||
"disabled".red().dimmed()
|
||||
);
|
||||
} else {
|
||||
println!("{}: {}", " - Max Backups".dimmed(), config.max_backups);
|
||||
}
|
||||
|
||||
// Early return if backup pruning disabled
|
||||
if config.max_backups == 0 {
|
||||
info!(
|
||||
"Backup pruning is disabled, enabled using 'max_backups' (KOMODO_CLI_MAX_BACKUPS)"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
crate::command::wait_for_enter("start backup prune", yes)?;
|
||||
|
||||
prune_inner().await
|
||||
}
|
||||
|
||||
async fn prune_inner() -> anyhow::Result<()> {
|
||||
let config = cli_config();
|
||||
|
||||
let mut backups_dir =
|
||||
match tokio::fs::read_dir(&config.backups_folder)
|
||||
.await
|
||||
.context("Failed to read backups folder for prune")
|
||||
{
|
||||
Ok(backups_dir) => backups_dir,
|
||||
Err(e) => {
|
||||
warn!("{e:#}");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let mut backup_folders = Vec::new();
|
||||
loop {
|
||||
match backups_dir.next_entry().await {
|
||||
Ok(Some(entry)) => {
|
||||
let Ok(metadata) = entry.metadata().await else {
|
||||
continue;
|
||||
};
|
||||
if metadata.is_dir() {
|
||||
backup_folders.push(entry.path());
|
||||
}
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(_) => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ordered from oldest -> newest
|
||||
backup_folders.sort();
|
||||
|
||||
let max_backups = config.max_backups as usize;
|
||||
let backup_folders_len = backup_folders.len();
|
||||
|
||||
// Early return if under the backup count threshold
|
||||
if backup_folders_len <= max_backups {
|
||||
info!("No backups to prune");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let to_delete =
|
||||
&backup_folders[..(backup_folders_len - max_backups)];
|
||||
|
||||
info!("Pruning old backups: {to_delete:?}");
|
||||
|
||||
for path in to_delete {
|
||||
if let Err(e) =
|
||||
tokio::fs::remove_dir_all(path).await.with_context(|| {
|
||||
format!("Failed to delete backup folder at {path:?}")
|
||||
})
|
||||
{
|
||||
warn!("{e:#}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn copy(index: bool, yes: bool) -> anyhow::Result<()> {
|
||||
let config = cli_config();
|
||||
|
||||
println!(
|
||||
"\n🦎 {} Database {} Utility 🦎",
|
||||
"Komodo".bold(),
|
||||
"Copy".blue().bold()
|
||||
);
|
||||
println!(
|
||||
"\n{}\n",
|
||||
" - Copies database contents to another database.".dimmed()
|
||||
);
|
||||
|
||||
if let Some(uri) = optional_string(&config.database.uri) {
|
||||
println!("{}: {}", " - Source URI".dimmed(), sanitize_uri(&uri));
|
||||
}
|
||||
if let Some(address) = optional_string(&config.database.address) {
|
||||
println!("{}: {address}", " - Source Address".dimmed());
|
||||
}
|
||||
if let Some(username) = optional_string(&config.database.username) {
|
||||
println!("{}: {username}", " - Source Username".dimmed());
|
||||
}
|
||||
println!(
|
||||
"{}: {}\n",
|
||||
" - Source Db Name".dimmed(),
|
||||
config.database.db_name,
|
||||
);
|
||||
|
||||
if let Some(uri) = optional_string(&config.database_target.uri) {
|
||||
println!("{}: {}", " - Target URI".dimmed(), sanitize_uri(&uri));
|
||||
}
|
||||
if let Some(address) =
|
||||
optional_string(&config.database_target.address)
|
||||
{
|
||||
println!("{}: {address}", " - Target Address".dimmed());
|
||||
}
|
||||
if let Some(username) =
|
||||
optional_string(&config.database_target.username)
|
||||
{
|
||||
println!("{}: {username}", " - Target Username".dimmed());
|
||||
}
|
||||
println!(
|
||||
"{}: {}",
|
||||
" - Target Db Name".dimmed(),
|
||||
config.database_target.db_name,
|
||||
);
|
||||
if !index {
|
||||
println!(
|
||||
"{}: {}",
|
||||
" - Target Db Indexing".dimmed(),
|
||||
"DISABLED".red(),
|
||||
);
|
||||
}
|
||||
|
||||
crate::command::wait_for_enter("start copy", yes)?;
|
||||
|
||||
let source_db = database::init(&config.database).await?;
|
||||
let target_db = if index {
|
||||
database::Client::new(&config.database_target).await?.db
|
||||
} else {
|
||||
database::init(&config.database_target).await?
|
||||
};
|
||||
|
||||
database::utils::copy(&source_db, &target_db).await
|
||||
}
|
||||
|
||||
async fn v1_downgrade(yes: bool) -> anyhow::Result<()> {
|
||||
let config = cli_config();
|
||||
|
||||
println!(
|
||||
"\n🦎 {} Database {} 🦎",
|
||||
"Komodo".bold(),
|
||||
"V1 Downgrade".purple().bold()
|
||||
);
|
||||
println!(
|
||||
"\n{}\n",
|
||||
" - Downgrade the database to V1 compatible data structures."
|
||||
.dimmed()
|
||||
);
|
||||
if let Some(uri) = optional_string(&config.database.uri) {
|
||||
println!("{}: {}", " - URI".dimmed(), sanitize_uri(&uri));
|
||||
}
|
||||
if let Some(address) = optional_string(&config.database.address) {
|
||||
println!("{}: {address}", " - Address".dimmed());
|
||||
}
|
||||
if let Some(username) = optional_string(&config.database.username) {
|
||||
println!("{}: {username}", " - Username".dimmed());
|
||||
}
|
||||
println!(
|
||||
"{}: {}\n",
|
||||
" - Db Name".dimmed(),
|
||||
config.database.db_name,
|
||||
);
|
||||
|
||||
crate::command::wait_for_enter("run downgrade", yes)?;
|
||||
|
||||
let db = database::init(&config.database).await?;
|
||||
|
||||
db.collection::<Document>("Server")
|
||||
.update_many(doc! {}, doc! { "$set": { "info": null } })
|
||||
.await
|
||||
.context("Failed to downgrade Server schema")?;
|
||||
|
||||
db.collection::<Document>("Deployment")
|
||||
.update_many(doc! {}, doc! { "$set": { "info": null } })
|
||||
.await
|
||||
.context("Failed to downgrade Deployment schema")?;
|
||||
|
||||
info!(
|
||||
"V1 Downgrade complete. Ready to downgrade to komodo-core:1 ✅"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
649
bin/cli/src/command/execute.rs
Normal file
649
bin/cli/src/command/execute.rs
Normal file
@@ -0,0 +1,649 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use colored::Colorize;
|
||||
use futures_util::{StreamExt, stream::FuturesUnordered};
|
||||
use komodo_client::{
|
||||
api::execute::{
|
||||
BatchExecutionResponse, BatchExecutionResponseItem, Execution,
|
||||
},
|
||||
entities::{resource_link, update::Update},
|
||||
};
|
||||
|
||||
use crate::config::cli_config;
|
||||
|
||||
enum ExecutionResult {
|
||||
Single(Box<Update>),
|
||||
Batch(BatchExecutionResponse),
|
||||
}
|
||||
|
||||
pub async fn handle(
|
||||
execution: &Execution,
|
||||
yes: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
if matches!(execution, Execution::None(_)) {
|
||||
println!("Got 'none' execution. Doing nothing...");
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
println!("Finished doing nothing. Exiting...");
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
println!("\n{}: Execution", "Mode".dimmed());
|
||||
match execution {
|
||||
Execution::None(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RunAction(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::BatchRunAction(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RunProcedure(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::BatchRunProcedure(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RunBuild(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::BatchRunBuild(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::CancelBuild(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::Deploy(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::BatchDeploy(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PullDeployment(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::StartDeployment(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RestartDeployment(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PauseDeployment(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::UnpauseDeployment(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::StopDeployment(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::DestroyDeployment(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::BatchDestroyDeployment(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::CloneRepo(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::BatchCloneRepo(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PullRepo(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::BatchPullRepo(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::BuildRepo(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::BatchBuildRepo(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::CancelRepoBuild(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::StartContainer(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RestartContainer(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PauseContainer(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::UnpauseContainer(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::StopContainer(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::DestroyContainer(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::StartAllContainers(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RestartAllContainers(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PauseAllContainers(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::UnpauseAllContainers(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::StopAllContainers(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PruneContainers(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::DeleteNetwork(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PruneNetworks(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::DeleteImage(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PruneImages(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::DeleteVolume(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PruneVolumes(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PruneDockerBuilders(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PruneBuildx(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PruneSystem(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RunSync(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::CommitSync(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::DeployStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::BatchDeployStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::DeployStackIfChanged(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::BatchDeployStackIfChanged(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PullStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::BatchPullStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::StartStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RestartStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::PauseStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::UnpauseStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::StopStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::DestroyStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::BatchDestroyStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RunStackService(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::TestAlerter(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::SendAlert(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RemoveSwarmNodes(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RemoveSwarmStacks(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RemoveSwarmServices(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::CreateSwarmConfig(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RotateSwarmConfig(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RemoveSwarmConfigs(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::CreateSwarmSecret(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RotateSwarmSecret(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RemoveSwarmSecrets(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::ClearRepoCache(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::BackupCoreDatabase(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::GlobalAutoUpdate(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RotateAllServerKeys(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RotateCoreKeys(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::Sleep(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
}
|
||||
|
||||
super::wait_for_enter("run execution", yes)?;
|
||||
|
||||
info!("Running Execution...");
|
||||
|
||||
let client = super::komodo_client().await?;
|
||||
|
||||
let res = match execution.clone() {
|
||||
Execution::RunAction(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchRunAction(request) => {
|
||||
client.execute(request).await.map(ExecutionResult::Batch)
|
||||
}
|
||||
Execution::RunProcedure(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchRunProcedure(request) => {
|
||||
client.execute(request).await.map(ExecutionResult::Batch)
|
||||
}
|
||||
Execution::RunBuild(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchRunBuild(request) => {
|
||||
client.execute(request).await.map(ExecutionResult::Batch)
|
||||
}
|
||||
Execution::CancelBuild(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::Deploy(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchDeploy(request) => {
|
||||
client.execute(request).await.map(ExecutionResult::Batch)
|
||||
}
|
||||
Execution::PullDeployment(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::StartDeployment(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RestartDeployment(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PauseDeployment(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::UnpauseDeployment(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::StopDeployment(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::DestroyDeployment(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchDestroyDeployment(request) => {
|
||||
client.execute(request).await.map(ExecutionResult::Batch)
|
||||
}
|
||||
Execution::CloneRepo(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchCloneRepo(request) => {
|
||||
client.execute(request).await.map(ExecutionResult::Batch)
|
||||
}
|
||||
Execution::PullRepo(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchPullRepo(request) => {
|
||||
client.execute(request).await.map(ExecutionResult::Batch)
|
||||
}
|
||||
Execution::BuildRepo(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchBuildRepo(request) => {
|
||||
client.execute(request).await.map(ExecutionResult::Batch)
|
||||
}
|
||||
Execution::CancelRepoBuild(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::StartContainer(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RestartContainer(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PauseContainer(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::UnpauseContainer(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::StopContainer(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::DestroyContainer(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::StartAllContainers(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RestartAllContainers(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PauseAllContainers(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::UnpauseAllContainers(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::StopAllContainers(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PruneContainers(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::DeleteNetwork(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PruneNetworks(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::DeleteImage(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PruneImages(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::DeleteVolume(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PruneVolumes(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PruneDockerBuilders(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PruneBuildx(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PruneSystem(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RunSync(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::CommitSync(request) => client
|
||||
.write(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::DeployStack(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchDeployStack(request) => {
|
||||
client.execute(request).await.map(ExecutionResult::Batch)
|
||||
}
|
||||
Execution::DeployStackIfChanged(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchDeployStackIfChanged(request) => {
|
||||
client.execute(request).await.map(ExecutionResult::Batch)
|
||||
}
|
||||
Execution::PullStack(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchPullStack(request) => {
|
||||
client.execute(request).await.map(ExecutionResult::Batch)
|
||||
}
|
||||
Execution::StartStack(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RestartStack(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PauseStack(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::UnpauseStack(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::StopStack(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::DestroyStack(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchDestroyStack(request) => {
|
||||
client.execute(request).await.map(ExecutionResult::Batch)
|
||||
}
|
||||
Execution::RunStackService(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::TestAlerter(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::SendAlert(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RemoveSwarmNodes(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RemoveSwarmStacks(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RemoveSwarmServices(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::CreateSwarmConfig(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RotateSwarmConfig(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RemoveSwarmConfigs(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::CreateSwarmSecret(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RotateSwarmSecret(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RemoveSwarmSecrets(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::ClearRepoCache(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BackupCoreDatabase(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::GlobalAutoUpdate(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RotateAllServerKeys(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RotateCoreKeys(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::Sleep(request) => {
|
||||
let duration =
|
||||
Duration::from_millis(request.duration_ms as u64);
|
||||
tokio::time::sleep(duration).await;
|
||||
println!("Finished sleeping!");
|
||||
std::process::exit(0)
|
||||
}
|
||||
Execution::None(_) => unreachable!(),
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(ExecutionResult::Single(update)) => {
|
||||
poll_update_until_complete(&update).await
|
||||
}
|
||||
Ok(ExecutionResult::Batch(updates)) => {
|
||||
let mut handles = updates
|
||||
.iter()
|
||||
.map(|update| async move {
|
||||
match update {
|
||||
BatchExecutionResponseItem::Ok(update) => {
|
||||
poll_update_until_complete(update).await
|
||||
}
|
||||
BatchExecutionResponseItem::Err(e) => {
|
||||
error!("{e:#?}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
while let Some(res) = handles.next().await {
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
error!("{e:#?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{e:#?}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn poll_update_until_complete(
|
||||
update: &Update,
|
||||
) -> anyhow::Result<()> {
|
||||
let link = if update.id.is_empty() {
|
||||
let (resource_type, id) = update.target.extract_variant_id();
|
||||
resource_link(&cli_config().host, resource_type, id)
|
||||
} else {
|
||||
format!("{}/updates/{}", cli_config().host, update.id)
|
||||
};
|
||||
println!("Link: '{}'", link.bold());
|
||||
|
||||
let client = super::komodo_client().await?;
|
||||
|
||||
let timer = tokio::time::Instant::now();
|
||||
let update = client.poll_update_until_complete(&update.id).await?;
|
||||
if update.success {
|
||||
println!(
|
||||
"FINISHED in {}: {}",
|
||||
format!("{:.1?}", timer.elapsed()).bold(),
|
||||
"EXECUTION SUCCESSFUL".green(),
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
"FINISHED in {}: {}",
|
||||
format!("{:.1?}", timer.elapsed()).bold(),
|
||||
"EXECUTION FAILED".red(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
1217
bin/cli/src/command/list.rs
Normal file
1217
bin/cli/src/command/list.rs
Normal file
File diff suppressed because it is too large
Load Diff
182
bin/cli/src/command/mod.rs
Normal file
182
bin/cli/src/command/mod.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use std::io::Read;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use chrono::TimeZone;
|
||||
use colored::Colorize;
|
||||
use comfy_table::{Attribute, Cell, Table};
|
||||
use komodo_client::{
|
||||
KomodoClient,
|
||||
entities::config::cli::{CliTableBorders, args::CliFormat},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use tokio::sync::OnceCell;
|
||||
use wildcard::Wildcard;
|
||||
|
||||
use crate::config::cli_config;
|
||||
|
||||
pub mod container;
|
||||
pub mod database;
|
||||
pub mod execute;
|
||||
pub mod list;
|
||||
pub mod terminal;
|
||||
pub mod update;
|
||||
|
||||
async fn komodo_client() -> anyhow::Result<&'static KomodoClient> {
|
||||
static KOMODO_CLIENT: OnceCell<KomodoClient> =
|
||||
OnceCell::const_new();
|
||||
KOMODO_CLIENT
|
||||
.get_or_try_init(|| async {
|
||||
let config = cli_config();
|
||||
let (Some(key), Some(secret)) =
|
||||
(&config.cli_key, &config.cli_secret)
|
||||
else {
|
||||
return Err(anyhow!(
|
||||
"Must provide both cli_key and cli_secret"
|
||||
));
|
||||
};
|
||||
KomodoClient::new(&config.host, key, secret)
|
||||
.with_healthcheck()
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
fn wait_for_enter(
|
||||
press_enter_to: &str,
|
||||
skip: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
if skip {
|
||||
println!();
|
||||
return Ok(());
|
||||
}
|
||||
println!(
|
||||
"\nPress {} to {}\n",
|
||||
"ENTER".green(),
|
||||
press_enter_to.bold()
|
||||
);
|
||||
let buffer = &mut [0u8];
|
||||
std::io::stdin()
|
||||
.read_exact(buffer)
|
||||
.context("failed to read ENTER")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sanitizes uris of the form:
|
||||
/// `protocol://username:password@address`
|
||||
fn sanitize_uri(uri: &str) -> String {
|
||||
// protocol: `mongodb`
|
||||
// credentials_address: `username:password@address`
|
||||
let Some((protocol, credentials_address)) = uri.split_once("://")
|
||||
else {
|
||||
// If no protocol, return as-is
|
||||
return uri.to_string();
|
||||
};
|
||||
|
||||
// credentials: `username:password`
|
||||
let Some((credentials, address)) =
|
||||
credentials_address.split_once('@')
|
||||
else {
|
||||
// If no credentials, return as-is
|
||||
return uri.to_string();
|
||||
};
|
||||
|
||||
match credentials.split_once(':') {
|
||||
Some((username, _)) => {
|
||||
format!("{protocol}://{username}:*****@{address}")
|
||||
}
|
||||
None => {
|
||||
format!("{protocol}://*****@{address}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_items<T: PrintTable + Serialize>(
|
||||
items: Vec<T>,
|
||||
format: CliFormat,
|
||||
links: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
match format {
|
||||
CliFormat::Table => {
|
||||
let mut table = Table::new();
|
||||
let preset = {
|
||||
use comfy_table::presets::*;
|
||||
match cli_config().table_borders {
|
||||
None | Some(CliTableBorders::Horizontal) => {
|
||||
UTF8_HORIZONTAL_ONLY
|
||||
}
|
||||
Some(CliTableBorders::Vertical) => UTF8_FULL_CONDENSED,
|
||||
Some(CliTableBorders::Inside) => UTF8_NO_BORDERS,
|
||||
Some(CliTableBorders::Outside) => UTF8_BORDERS_ONLY,
|
||||
Some(CliTableBorders::All) => UTF8_FULL,
|
||||
}
|
||||
};
|
||||
table.load_preset(preset).set_header(
|
||||
T::header(links)
|
||||
.iter()
|
||||
.map(|h| Cell::new(h).add_attribute(Attribute::Bold)),
|
||||
);
|
||||
for item in items {
|
||||
table.add_row(item.row(links));
|
||||
}
|
||||
println!("{table}");
|
||||
}
|
||||
CliFormat::Json => {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&items)
|
||||
.context("Failed to serialize items to JSON")?
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
trait PrintTable {
|
||||
fn header(links: bool) -> &'static [&'static str];
|
||||
fn row(self, links: bool) -> Vec<Cell>;
|
||||
}
|
||||
|
||||
fn parse_wildcards(items: &[String]) -> Vec<Wildcard<'_>> {
|
||||
items
|
||||
.iter()
|
||||
.flat_map(|i| {
|
||||
Wildcard::new(i.as_bytes()).inspect_err(|e| {
|
||||
warn!("Failed to parse wildcard: {i} | {e:?}")
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn matches_wildcards(
|
||||
wildcards: &[Wildcard<'_>],
|
||||
items: &[&str],
|
||||
) -> bool {
|
||||
if wildcards.is_empty() {
|
||||
return true;
|
||||
}
|
||||
items.iter().any(|item| {
|
||||
wildcards.iter().any(|wc| wc.is_match(item.as_bytes()))
|
||||
})
|
||||
}
|
||||
|
||||
fn format_timetamp(ts: i64) -> anyhow::Result<String> {
|
||||
let ts = chrono::Local
|
||||
.timestamp_millis_opt(ts)
|
||||
.single()
|
||||
.context("Invalid ts")?
|
||||
.format("%m/%d %H:%M:%S")
|
||||
.to_string();
|
||||
Ok(ts)
|
||||
}
|
||||
|
||||
fn clamp_sha(maybe_sha: &str) -> String {
|
||||
if maybe_sha.starts_with("sha256:") {
|
||||
maybe_sha[0..20].to_string() + "..."
|
||||
} else {
|
||||
maybe_sha.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// fn text_link(link: &str, text: &str) -> String {
|
||||
// format!("\x1b]8;;{link}\x07{text}\x1b]8;;\x07")
|
||||
// }
|
||||
334
bin/cli/src/command/terminal.rs
Normal file
334
bin/cli/src/command/terminal.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use colored::Colorize;
|
||||
use komodo_client::{
|
||||
api::{
|
||||
read::{ListAllDockerContainers, ListServers},
|
||||
terminal::InitTerminal,
|
||||
},
|
||||
entities::{
|
||||
config::cli::args::terminal::{Attach, Connect, Exec},
|
||||
server::ServerQuery,
|
||||
terminal::{
|
||||
ContainerTerminalMode, TerminalRecreateMode,
|
||||
TerminalResizeMessage, TerminalStdinMessage,
|
||||
},
|
||||
},
|
||||
ws::terminal::TerminalWebsocket,
|
||||
};
|
||||
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub async fn handle_connect(
|
||||
Connect {
|
||||
server,
|
||||
name,
|
||||
command,
|
||||
recreate,
|
||||
}: &Connect,
|
||||
) -> anyhow::Result<()> {
|
||||
handle_terminal_forwarding(async {
|
||||
super::komodo_client()
|
||||
.await?
|
||||
.connect_server_terminal(
|
||||
server.to_string(),
|
||||
Some(name.to_string()),
|
||||
Some(InitTerminal {
|
||||
command: command.clone(),
|
||||
recreate: if *recreate {
|
||||
TerminalRecreateMode::Always
|
||||
} else {
|
||||
TerminalRecreateMode::DifferentCommand
|
||||
},
|
||||
mode: None,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_exec(
|
||||
Exec {
|
||||
server,
|
||||
container,
|
||||
shell,
|
||||
recreate,
|
||||
}: &Exec,
|
||||
) -> anyhow::Result<()> {
|
||||
let server = get_server(server.clone(), container).await?;
|
||||
handle_terminal_forwarding(async {
|
||||
super::komodo_client()
|
||||
.await?
|
||||
.connect_container_terminal(
|
||||
server,
|
||||
container.to_string(),
|
||||
None,
|
||||
Some(InitTerminal {
|
||||
command: Some(shell.to_string()),
|
||||
recreate: if *recreate {
|
||||
TerminalRecreateMode::Always
|
||||
} else {
|
||||
TerminalRecreateMode::DifferentCommand
|
||||
},
|
||||
mode: Some(ContainerTerminalMode::Exec),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_attach(
|
||||
Attach {
|
||||
server,
|
||||
container,
|
||||
recreate,
|
||||
}: &Attach,
|
||||
) -> anyhow::Result<()> {
|
||||
let server = get_server(server.clone(), container).await?;
|
||||
handle_terminal_forwarding(async {
|
||||
super::komodo_client()
|
||||
.await?
|
||||
.connect_container_terminal(
|
||||
server,
|
||||
container.to_string(),
|
||||
None,
|
||||
Some(InitTerminal {
|
||||
command: None,
|
||||
recreate: if *recreate {
|
||||
TerminalRecreateMode::Always
|
||||
} else {
|
||||
TerminalRecreateMode::DifferentCommand
|
||||
},
|
||||
mode: Some(ContainerTerminalMode::Attach),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_server(
|
||||
server: Option<String>,
|
||||
container: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
if let Some(server) = server {
|
||||
return Ok(server);
|
||||
}
|
||||
|
||||
let client = super::komodo_client().await?;
|
||||
|
||||
let mut containers = client
|
||||
.read(ListAllDockerContainers {
|
||||
servers: Default::default(),
|
||||
containers: vec![container.to_string()],
|
||||
})
|
||||
.await?;
|
||||
|
||||
if containers.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"Did not find any container matching {container}"
|
||||
));
|
||||
}
|
||||
|
||||
if containers.len() == 1 {
|
||||
return containers
|
||||
.pop()
|
||||
.context("Shouldn't happen")?
|
||||
.server_id
|
||||
.context("Container doesn't have server_id");
|
||||
}
|
||||
|
||||
let servers = containers
|
||||
.into_iter()
|
||||
.flat_map(|container| container.server_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let servers = client
|
||||
.read(ListServers {
|
||||
query: ServerQuery::builder().names(servers).build(),
|
||||
})
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|server| format!("\t- {}", server.name.bold()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
Err(anyhow!(
|
||||
"Multiple containers matching '{}' on Servers:\n{servers}",
|
||||
container.bold(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn handle_terminal_forwarding<
|
||||
C: Future<Output = anyhow::Result<TerminalWebsocket>>,
|
||||
>(
|
||||
connect: C,
|
||||
) -> anyhow::Result<()> {
|
||||
// Need to forward multiple sources into ws write
|
||||
let (write_tx, mut write_rx) =
|
||||
tokio::sync::mpsc::channel::<TerminalStdinMessage>(1024);
|
||||
|
||||
// ================
|
||||
// SETUP RESIZING
|
||||
// ================
|
||||
|
||||
// Subscribe to SIGWINCH for resize messages
|
||||
let mut sigwinch = tokio::signal::unix::signal(
|
||||
tokio::signal::unix::SignalKind::window_change(),
|
||||
)
|
||||
.context("failed to register SIGWINCH handler")?;
|
||||
|
||||
// Send first resize messsage, bailing if it fails to get the size.
|
||||
write_tx.send(resize_message()?).await?;
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
|
||||
let forward_resize = async {
|
||||
while future_or_cancel(sigwinch.recv(), &cancel)
|
||||
.await
|
||||
.flatten()
|
||||
.is_some()
|
||||
{
|
||||
if let Ok(resize_message) = resize_message()
|
||||
&& write_tx.send(resize_message).await.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
cancel.cancel();
|
||||
};
|
||||
|
||||
let forward_stdin = async {
|
||||
let mut stdin = tokio::io::stdin();
|
||||
let mut buf = [0u8; 8192];
|
||||
while let Some(Ok(n)) =
|
||||
future_or_cancel(stdin.read(&mut buf), &cancel).await
|
||||
{
|
||||
// EOF
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
let bytes = &buf[..n];
|
||||
// Check for disconnect sequence (alt + q)
|
||||
if bytes == [197, 147] {
|
||||
break;
|
||||
}
|
||||
// Forward bytes
|
||||
if write_tx
|
||||
.send(TerminalStdinMessage::Forward(bytes.to_vec()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
};
|
||||
}
|
||||
cancel.cancel();
|
||||
};
|
||||
|
||||
// =====================
|
||||
// CONNECT AND FORWARD
|
||||
// =====================
|
||||
|
||||
let (mut ws_write, mut ws_read) = connect.await?.split();
|
||||
|
||||
let forward_write = async {
|
||||
while let Some(message) =
|
||||
future_or_cancel(write_rx.recv(), &cancel).await.flatten()
|
||||
{
|
||||
if let Err(e) = ws_write.send_stdin_message(message).await {
|
||||
cancel.cancel();
|
||||
return Some(e);
|
||||
};
|
||||
}
|
||||
cancel.cancel();
|
||||
None
|
||||
};
|
||||
|
||||
let forward_read = async {
|
||||
let mut stdout = tokio::io::stdout();
|
||||
while let Some(msg) =
|
||||
future_or_cancel(ws_read.receive_stdout(), &cancel).await
|
||||
{
|
||||
let bytes = match msg {
|
||||
Ok(Some(bytes)) => bytes,
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
cancel.cancel();
|
||||
return Some(e.context("Websocket read error"));
|
||||
}
|
||||
};
|
||||
if let Err(e) = stdout
|
||||
.write_all(&bytes)
|
||||
.await
|
||||
.context("Failed to write text to stdout")
|
||||
{
|
||||
cancel.cancel();
|
||||
return Some(e);
|
||||
}
|
||||
let _ = stdout.flush().await;
|
||||
}
|
||||
cancel.cancel();
|
||||
None
|
||||
};
|
||||
|
||||
let guard = RawModeGuard::enable_raw_mode()?;
|
||||
|
||||
let (_, _, write_error, read_error) = tokio::join!(
|
||||
forward_resize,
|
||||
forward_stdin,
|
||||
forward_write,
|
||||
forward_read
|
||||
);
|
||||
|
||||
drop(guard);
|
||||
|
||||
if let Some(e) = write_error {
|
||||
eprintln!("\nFailed to forward stdin | {e:#}");
|
||||
}
|
||||
|
||||
if let Some(e) = read_error {
|
||||
eprintln!("\nFailed to forward stdout | {e:#}");
|
||||
}
|
||||
|
||||
println!("\n\n{} {}", "connection".bold(), "closed".red().bold());
|
||||
|
||||
// It doesn't seem to exit by itself after the raw mode stuff.
|
||||
std::process::exit(0)
|
||||
}
|
||||
|
||||
fn resize_message() -> anyhow::Result<TerminalStdinMessage> {
|
||||
let (cols, rows) = crossterm::terminal::size()
|
||||
.context("Failed to get terminal size")?;
|
||||
Ok(TerminalStdinMessage::Resize(TerminalResizeMessage {
|
||||
rows,
|
||||
cols,
|
||||
}))
|
||||
}
|
||||
|
||||
struct RawModeGuard;
|
||||
|
||||
impl RawModeGuard {
|
||||
fn enable_raw_mode() -> anyhow::Result<Self> {
|
||||
crossterm::terminal::enable_raw_mode()
|
||||
.context("Failed to enable terminal raw mode")?;
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
impl Drop for RawModeGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = crossterm::terminal::disable_raw_mode() {
|
||||
eprintln!("Failed to disable terminal raw mode | {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn future_or_cancel<T, F: Future<Output = T>>(
|
||||
fut: F,
|
||||
cancel: &CancellationToken,
|
||||
) -> Option<T> {
|
||||
tokio::select! {
|
||||
res = fut => Some(res),
|
||||
_ = cancel.cancelled() => None
|
||||
}
|
||||
}
|
||||
43
bin/cli/src/command/update/mod.rs
Normal file
43
bin/cli/src/command/update/mod.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use komodo_client::entities::{
|
||||
build::PartialBuildConfig,
|
||||
config::cli::args::update::UpdateCommand,
|
||||
deployment::PartialDeploymentConfig, repo::PartialRepoConfig,
|
||||
server::PartialServerConfig, stack::PartialStackConfig,
|
||||
sync::PartialResourceSyncConfig,
|
||||
};
|
||||
|
||||
mod resource;
|
||||
mod user;
|
||||
mod variable;
|
||||
|
||||
pub async fn handle(command: &UpdateCommand) -> anyhow::Result<()> {
|
||||
match command {
|
||||
UpdateCommand::Build(update) => {
|
||||
resource::update::<PartialBuildConfig>(update).await
|
||||
}
|
||||
UpdateCommand::Deployment(update) => {
|
||||
resource::update::<PartialDeploymentConfig>(update).await
|
||||
}
|
||||
UpdateCommand::Repo(update) => {
|
||||
resource::update::<PartialRepoConfig>(update).await
|
||||
}
|
||||
UpdateCommand::Server(update) => {
|
||||
resource::update::<PartialServerConfig>(update).await
|
||||
}
|
||||
UpdateCommand::Stack(update) => {
|
||||
resource::update::<PartialStackConfig>(update).await
|
||||
}
|
||||
UpdateCommand::Sync(update) => {
|
||||
resource::update::<PartialResourceSyncConfig>(update).await
|
||||
}
|
||||
UpdateCommand::Variable {
|
||||
name,
|
||||
value,
|
||||
secret,
|
||||
yes,
|
||||
} => variable::update(name, value, *secret, *yes).await,
|
||||
UpdateCommand::User { username, command } => {
|
||||
user::update(username, command).await
|
||||
}
|
||||
}
|
||||
}
|
||||
152
bin/cli/src/command/update/resource.rs
Normal file
152
bin/cli/src/command/update/resource.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use anyhow::Context;
|
||||
use colored::Colorize;
|
||||
use komodo_client::{
|
||||
api::write::{
|
||||
UpdateBuild, UpdateDeployment, UpdateRepo, UpdateResourceSync,
|
||||
UpdateServer, UpdateStack,
|
||||
},
|
||||
entities::{
|
||||
build::PartialBuildConfig,
|
||||
config::cli::args::update::UpdateResource,
|
||||
deployment::PartialDeploymentConfig, repo::PartialRepoConfig,
|
||||
server::PartialServerConfig, stack::PartialStackConfig,
|
||||
sync::PartialResourceSyncConfig,
|
||||
},
|
||||
};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
pub async fn update<
|
||||
T: std::fmt::Debug + Serialize + DeserializeOwned + ResourceUpdate,
|
||||
>(
|
||||
UpdateResource {
|
||||
resource,
|
||||
update,
|
||||
yes,
|
||||
}: &UpdateResource,
|
||||
) -> anyhow::Result<()> {
|
||||
println!("\n{}: Update {}\n", "Mode".dimmed(), T::resource_type());
|
||||
println!(" - {}: {resource}", "Name".dimmed());
|
||||
|
||||
let config = serde_qs::from_str::<T>(update)
|
||||
.context("Failed to deserialize config")?;
|
||||
|
||||
match serde_json::to_string_pretty(&config) {
|
||||
Ok(config) => {
|
||||
println!(" - {}: {config}", "Update".dimmed());
|
||||
}
|
||||
Err(_) => {
|
||||
println!(" - {}: {config:#?}", "Update".dimmed());
|
||||
}
|
||||
}
|
||||
|
||||
crate::command::wait_for_enter("update resource", *yes)?;
|
||||
|
||||
config.apply(resource).await
|
||||
}
|
||||
|
||||
pub trait ResourceUpdate {
|
||||
fn resource_type() -> &'static str;
|
||||
async fn apply(self, resource: &str) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
impl ResourceUpdate for PartialBuildConfig {
|
||||
fn resource_type() -> &'static str {
|
||||
"Build"
|
||||
}
|
||||
async fn apply(self, resource: &str) -> anyhow::Result<()> {
|
||||
let client = crate::command::komodo_client().await?;
|
||||
client
|
||||
.write(UpdateBuild {
|
||||
id: resource.to_string(),
|
||||
config: self,
|
||||
})
|
||||
.await
|
||||
.context("Failed to update build config")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceUpdate for PartialDeploymentConfig {
|
||||
fn resource_type() -> &'static str {
|
||||
"Deployment"
|
||||
}
|
||||
async fn apply(self, resource: &str) -> anyhow::Result<()> {
|
||||
let client = crate::command::komodo_client().await?;
|
||||
client
|
||||
.write(UpdateDeployment {
|
||||
id: resource.to_string(),
|
||||
config: self,
|
||||
})
|
||||
.await
|
||||
.context("Failed to update deployment config")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceUpdate for PartialRepoConfig {
|
||||
fn resource_type() -> &'static str {
|
||||
"Repo"
|
||||
}
|
||||
async fn apply(self, resource: &str) -> anyhow::Result<()> {
|
||||
let client = crate::command::komodo_client().await?;
|
||||
client
|
||||
.write(UpdateRepo {
|
||||
id: resource.to_string(),
|
||||
config: self,
|
||||
})
|
||||
.await
|
||||
.context("Failed to update repo config")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceUpdate for PartialServerConfig {
|
||||
fn resource_type() -> &'static str {
|
||||
"Server"
|
||||
}
|
||||
async fn apply(self, resource: &str) -> anyhow::Result<()> {
|
||||
let client = crate::command::komodo_client().await?;
|
||||
client
|
||||
.write(UpdateServer {
|
||||
id: resource.to_string(),
|
||||
config: self,
|
||||
})
|
||||
.await
|
||||
.context("Failed to update server config")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceUpdate for PartialStackConfig {
|
||||
fn resource_type() -> &'static str {
|
||||
"Stack"
|
||||
}
|
||||
async fn apply(self, resource: &str) -> anyhow::Result<()> {
|
||||
let client = crate::command::komodo_client().await?;
|
||||
client
|
||||
.write(UpdateStack {
|
||||
id: resource.to_string(),
|
||||
config: self,
|
||||
})
|
||||
.await
|
||||
.context("Failed to update stack config")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceUpdate for PartialResourceSyncConfig {
|
||||
fn resource_type() -> &'static str {
|
||||
"Sync"
|
||||
}
|
||||
async fn apply(self, resource: &str) -> anyhow::Result<()> {
|
||||
let client = crate::command::komodo_client().await?;
|
||||
client
|
||||
.write(UpdateResourceSync {
|
||||
id: resource.to_string(),
|
||||
config: self,
|
||||
})
|
||||
.await
|
||||
.context("Failed to update sync config")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
142
bin/cli/src/command/update/user.rs
Normal file
142
bin/cli/src/command/update/user.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
use anyhow::Context;
|
||||
use colored::Colorize;
|
||||
use database::mungos::mongodb::bson::doc;
|
||||
use komodo_client::entities::{
|
||||
config::{
|
||||
cli::args::{CliEnabled, update::UpdateUserCommand},
|
||||
empty_or_redacted,
|
||||
},
|
||||
optional_string,
|
||||
};
|
||||
|
||||
use crate::{command::sanitize_uri, config::cli_config};
|
||||
|
||||
pub async fn update(
|
||||
username: &str,
|
||||
command: &UpdateUserCommand,
|
||||
) -> anyhow::Result<()> {
|
||||
match command {
|
||||
UpdateUserCommand::Password {
|
||||
password,
|
||||
unsanitized,
|
||||
yes,
|
||||
} => {
|
||||
update_password(username, password, *unsanitized, *yes).await
|
||||
}
|
||||
UpdateUserCommand::SuperAdmin { enabled, yes } => {
|
||||
update_super_admin(username, *enabled, *yes).await
|
||||
}
|
||||
UpdateUserCommand::Clear2fa { yes } => {
|
||||
clear_2fa(username, *yes).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_password(
|
||||
username: &str,
|
||||
password: &str,
|
||||
unsanitized: bool,
|
||||
yes: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
println!("\n{}: Update Password\n", "Mode".dimmed());
|
||||
println!(" - {}: {username}", "Username".dimmed());
|
||||
if unsanitized {
|
||||
println!(" - {}: {password}", "Password".dimmed());
|
||||
} else {
|
||||
println!(
|
||||
" - {}: {}",
|
||||
"Password".dimmed(),
|
||||
empty_or_redacted(password)
|
||||
);
|
||||
}
|
||||
|
||||
crate::command::wait_for_enter("update password", yes)?;
|
||||
|
||||
info!("Updating password...");
|
||||
|
||||
let db = database::Client::new(&cli_config().database).await?;
|
||||
|
||||
let user = db
|
||||
.users
|
||||
.find_one(doc! { "username": username })
|
||||
.await
|
||||
.context("Failed to query database for user")?
|
||||
.context("No user found with given username")?;
|
||||
|
||||
db.set_user_password(&user, password).await?;
|
||||
|
||||
info!("Password updated ✅");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_super_admin(
|
||||
username: &str,
|
||||
super_admin: CliEnabled,
|
||||
yes: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let config = cli_config();
|
||||
|
||||
println!("\n{}: Update Super Admin\n", "Mode".dimmed());
|
||||
println!(" - {}: {username}", "Username".dimmed());
|
||||
println!(" - {}: {super_admin}\n", "Super Admin".dimmed());
|
||||
|
||||
if let Some(uri) = optional_string(&config.database.uri) {
|
||||
println!("{}: {}", " - Source URI".dimmed(), sanitize_uri(&uri));
|
||||
}
|
||||
if let Some(address) = optional_string(&config.database.address) {
|
||||
println!("{}: {address}", " - Source Address".dimmed());
|
||||
}
|
||||
if let Some(username) = optional_string(&config.database.username) {
|
||||
println!("{}: {username}", " - Source Username".dimmed());
|
||||
}
|
||||
println!(
|
||||
"{}: {}",
|
||||
" - Source Db Name".dimmed(),
|
||||
config.database.db_name,
|
||||
);
|
||||
|
||||
crate::command::wait_for_enter("update super admin", yes)?;
|
||||
|
||||
info!("Updating super admin...");
|
||||
|
||||
let db = database::Client::new(&config.database).await?;
|
||||
|
||||
// Make sure the user exists first before saying it is successful.
|
||||
let user = db
|
||||
.users
|
||||
.find_one(doc! { "username": username })
|
||||
.await
|
||||
.context("Failed to query database for user")?
|
||||
.context("No user found with given username")?;
|
||||
|
||||
let super_admin: bool = super_admin.into();
|
||||
db.users
|
||||
.update_one(
|
||||
doc! { "username": user.username },
|
||||
doc! { "$set": { "super_admin": super_admin } },
|
||||
)
|
||||
.await
|
||||
.context("Failed to update user super admin on db")?;
|
||||
|
||||
info!("Super admin updated ✅");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn clear_2fa(username: &str, yes: bool) -> anyhow::Result<()> {
|
||||
println!("\n{}: Clear 2FA Methods\n", "Mode".dimmed());
|
||||
println!(" - {}: {username}", "Username".dimmed());
|
||||
|
||||
crate::command::wait_for_enter("clear user 2FA methods", yes)?;
|
||||
|
||||
info!("Clearing 2FA methods...");
|
||||
|
||||
let db = database::Client::new(&cli_config().database).await?;
|
||||
|
||||
db.clear_user_2fa_methods(username).await?;
|
||||
|
||||
info!("2FA methods cleared ✅");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
70
bin/cli/src/command/update/variable.rs
Normal file
70
bin/cli/src/command/update/variable.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use anyhow::Context;
|
||||
use colored::Colorize;
|
||||
use komodo_client::api::{
|
||||
read::GetVariable,
|
||||
write::{
|
||||
CreateVariable, UpdateVariableIsSecret, UpdateVariableValue,
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn update(
|
||||
name: &str,
|
||||
value: &str,
|
||||
secret: Option<bool>,
|
||||
yes: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
println!("\n{}: Update Variable\n", "Mode".dimmed());
|
||||
println!(" - {}: {name}", "Name".dimmed());
|
||||
println!(" - {}: {value}", "Value".dimmed());
|
||||
if let Some(secret) = secret {
|
||||
println!(" - {}: {secret}", "Is Secret".dimmed());
|
||||
}
|
||||
|
||||
crate::command::wait_for_enter("update variable", yes)?;
|
||||
|
||||
let client = crate::command::komodo_client().await?;
|
||||
|
||||
let Ok(existing) = client
|
||||
.read(GetVariable {
|
||||
name: name.to_string(),
|
||||
})
|
||||
.await
|
||||
else {
|
||||
// Create the variable
|
||||
client
|
||||
.write(CreateVariable {
|
||||
name: name.to_string(),
|
||||
value: value.to_string(),
|
||||
is_secret: secret.unwrap_or_default(),
|
||||
description: Default::default(),
|
||||
})
|
||||
.await
|
||||
.context("Failed to create variable")?;
|
||||
info!("Variable created ✅");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
client
|
||||
.write(UpdateVariableValue {
|
||||
name: name.to_string(),
|
||||
value: value.to_string(),
|
||||
})
|
||||
.await
|
||||
.context("Failed to update variable 'value'")?;
|
||||
info!("Variable 'value' updated ✅");
|
||||
|
||||
let Some(secret) = secret else { return Ok(()) };
|
||||
|
||||
if secret != existing.is_secret {
|
||||
client
|
||||
.write(UpdateVariableIsSecret {
|
||||
name: name.to_string(),
|
||||
is_secret: secret,
|
||||
})
|
||||
.await
|
||||
.context("Failed to update variable 'is_secret'")?;
|
||||
info!("Variable 'is_secret' updated to {secret} ✅");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
280
bin/cli/src/config.rs
Normal file
280
bin/cli/src/config.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
use std::{path::PathBuf, sync::OnceLock};
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
use komodo_client::entities::{
|
||||
config::{
|
||||
DatabaseConfig,
|
||||
cli::{
|
||||
CliConfig, Env,
|
||||
args::{CliArgs, Command, Execute, database::DatabaseCommand},
|
||||
},
|
||||
},
|
||||
logger::LogConfig,
|
||||
};
|
||||
use mogh_secret_file::maybe_read_item_from_file;
|
||||
|
||||
pub fn cli_args() -> &'static CliArgs {
|
||||
static CLI_ARGS: OnceLock<CliArgs> = OnceLock::new();
|
||||
CLI_ARGS.get_or_init(CliArgs::parse)
|
||||
}
|
||||
|
||||
pub fn cli_env() -> &'static Env {
|
||||
static CLI_ARGS: OnceLock<Env> = OnceLock::new();
|
||||
CLI_ARGS.get_or_init(|| {
|
||||
match envy::from_env()
|
||||
.context("Failed to parse Komodo CLI environment")
|
||||
{
|
||||
Ok(env) => env,
|
||||
Err(e) => {
|
||||
panic!("{e:?}")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cli_config() -> &'static CliConfig {
|
||||
static CLI_CONFIG: OnceLock<CliConfig> = OnceLock::new();
|
||||
CLI_CONFIG.get_or_init(|| {
|
||||
let args = cli_args();
|
||||
let env = cli_env().clone();
|
||||
let config_paths = args
|
||||
.config_path
|
||||
.clone()
|
||||
.unwrap_or(env.komodo_cli_config_paths);
|
||||
let debug_startup =
|
||||
args.debug_startup.unwrap_or(env.komodo_cli_debug_startup);
|
||||
|
||||
if debug_startup {
|
||||
println!(
|
||||
"{}: Komodo CLI version: {}",
|
||||
"DEBUG".cyan(),
|
||||
env!("CARGO_PKG_VERSION").blue().bold()
|
||||
);
|
||||
println!(
|
||||
"{}: {}: {config_paths:?}",
|
||||
"DEBUG".cyan(),
|
||||
"Config Paths".dimmed(),
|
||||
);
|
||||
}
|
||||
|
||||
let config_keywords = args
|
||||
.config_keyword
|
||||
.clone()
|
||||
.unwrap_or(env.komodo_cli_config_keywords);
|
||||
let config_keywords = config_keywords
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>();
|
||||
if debug_startup {
|
||||
println!(
|
||||
"{}: {}: {config_keywords:?}",
|
||||
"DEBUG".cyan(),
|
||||
"Config File Keywords".dimmed(),
|
||||
);
|
||||
}
|
||||
let mut unparsed_config = (mogh_config::ConfigLoader {
|
||||
paths: &config_paths
|
||||
.iter()
|
||||
.map(PathBuf::as_path)
|
||||
.collect::<Vec<_>>(),
|
||||
match_wildcards: &config_keywords,
|
||||
include_file_name: ".kminclude",
|
||||
merge_nested: env.komodo_cli_merge_nested_config,
|
||||
extend_array: env.komodo_cli_extend_config_arrays,
|
||||
debug_print: debug_startup,
|
||||
})
|
||||
.load::<serde_json::Map<String, serde_json::Value>>()
|
||||
.expect("failed at parsing config from paths");
|
||||
let init_parsed_config = serde_json::from_value::<CliConfig>(
|
||||
serde_json::Value::Object(unparsed_config.clone()),
|
||||
)
|
||||
.context("Failed to parse config")
|
||||
.unwrap();
|
||||
|
||||
let (host, key, secret) = match &args.command {
|
||||
Command::Execute(Execute {
|
||||
host, key, secret, ..
|
||||
}) => (host.clone(), key.clone(), secret.clone()),
|
||||
_ => (None, None, None),
|
||||
};
|
||||
|
||||
let backups_folder = match &args.command {
|
||||
Command::Database {
|
||||
command: DatabaseCommand::Backup { backups_folder, .. },
|
||||
} => backups_folder.clone(),
|
||||
Command::Database {
|
||||
command: DatabaseCommand::Restore { backups_folder, .. },
|
||||
} => backups_folder.clone(),
|
||||
_ => None,
|
||||
};
|
||||
let (uri, address, username, password, db_name) =
|
||||
match &args.command {
|
||||
Command::Database {
|
||||
command:
|
||||
DatabaseCommand::Copy {
|
||||
uri,
|
||||
address,
|
||||
username,
|
||||
password,
|
||||
db_name,
|
||||
..
|
||||
},
|
||||
} => (
|
||||
uri.clone(),
|
||||
address.clone(),
|
||||
username.clone(),
|
||||
password.clone(),
|
||||
db_name.clone(),
|
||||
),
|
||||
_ => (None, None, None, None, None),
|
||||
};
|
||||
|
||||
let profile = args
|
||||
.profile
|
||||
.as_ref()
|
||||
.or(init_parsed_config.default_profile.as_ref());
|
||||
|
||||
let unparsed_config = if let Some(profile) = profile
|
||||
&& !profile.is_empty()
|
||||
{
|
||||
// Find the profile config,
|
||||
// then merge it with the Default config.
|
||||
let serde_json::Value::Array(profiles) = unparsed_config
|
||||
.remove("profile")
|
||||
.context("Config has no profiles, but a profile is required")
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("`config.profile` is not array");
|
||||
};
|
||||
let Some(profile_config) = profiles.into_iter().find(|p| {
|
||||
let Ok(parsed) =
|
||||
serde_json::from_value::<CliConfig>(p.clone())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
&parsed.config_profile == profile
|
||||
|| parsed
|
||||
.config_aliases
|
||||
.iter()
|
||||
.any(|alias| alias == profile)
|
||||
}) else {
|
||||
panic!("No profile matching '{profile}' was found.");
|
||||
};
|
||||
let serde_json::Value::Object(profile_config) = profile_config
|
||||
else {
|
||||
panic!("Profile config is not Object type.");
|
||||
};
|
||||
mogh_config::merge_config(
|
||||
unparsed_config,
|
||||
profile_config.clone(),
|
||||
env.komodo_cli_merge_nested_config,
|
||||
env.komodo_cli_extend_config_arrays,
|
||||
)
|
||||
.unwrap_or(profile_config)
|
||||
} else {
|
||||
unparsed_config
|
||||
};
|
||||
let config = serde_json::from_value::<CliConfig>(
|
||||
serde_json::Value::Object(unparsed_config),
|
||||
)
|
||||
.context("Failed to parse final config")
|
||||
.unwrap();
|
||||
let config_profile = if config.config_profile.is_empty() {
|
||||
String::from("None")
|
||||
} else {
|
||||
config.config_profile
|
||||
};
|
||||
|
||||
CliConfig {
|
||||
config_profile,
|
||||
config_aliases: config.config_aliases,
|
||||
default_profile: config.default_profile,
|
||||
table_borders: env
|
||||
.komodo_cli_table_borders
|
||||
.or(config.table_borders),
|
||||
host: host
|
||||
.or(env.komodo_cli_host)
|
||||
.or(env.komodo_host)
|
||||
.unwrap_or(config.host),
|
||||
cli_key: key.or(env.komodo_cli_key).or(config.cli_key),
|
||||
cli_secret: secret
|
||||
.or(env.komodo_cli_secret)
|
||||
.or(config.cli_secret),
|
||||
backups_folder: backups_folder
|
||||
.or(env.komodo_cli_backups_folder)
|
||||
.unwrap_or(config.backups_folder),
|
||||
max_backups: env
|
||||
.komodo_cli_max_backups
|
||||
.unwrap_or(config.max_backups),
|
||||
database_target: DatabaseConfig {
|
||||
uri: uri
|
||||
.or(env.komodo_cli_database_target_uri)
|
||||
.unwrap_or(config.database_target.uri),
|
||||
address: address
|
||||
.or(env.komodo_cli_database_target_address)
|
||||
.unwrap_or(config.database_target.address),
|
||||
username: username
|
||||
.or(env.komodo_cli_database_target_username)
|
||||
.unwrap_or(config.database_target.username),
|
||||
password: password
|
||||
.or(env.komodo_cli_database_target_password)
|
||||
.unwrap_or(config.database_target.password),
|
||||
db_name: db_name
|
||||
.or(env.komodo_cli_database_target_db_name)
|
||||
.unwrap_or(config.database_target.db_name),
|
||||
app_name: config.database_target.app_name,
|
||||
},
|
||||
database: DatabaseConfig {
|
||||
uri: maybe_read_item_from_file(
|
||||
env.komodo_database_uri_file,
|
||||
env.komodo_database_uri,
|
||||
)
|
||||
.unwrap_or(config.database.uri),
|
||||
address: env
|
||||
.komodo_database_address
|
||||
.unwrap_or(config.database.address),
|
||||
username: maybe_read_item_from_file(
|
||||
env.komodo_database_username_file,
|
||||
env.komodo_database_username,
|
||||
)
|
||||
.unwrap_or(config.database.username),
|
||||
password: maybe_read_item_from_file(
|
||||
env.komodo_database_password_file,
|
||||
env.komodo_database_password,
|
||||
)
|
||||
.unwrap_or(config.database.password),
|
||||
db_name: env
|
||||
.komodo_database_db_name
|
||||
.unwrap_or(config.database.db_name),
|
||||
app_name: config.database.app_name,
|
||||
},
|
||||
cli_logging: LogConfig {
|
||||
level: env
|
||||
.komodo_cli_logging_level
|
||||
.unwrap_or(config.cli_logging.level),
|
||||
stdio: env
|
||||
.komodo_cli_logging_stdio
|
||||
.unwrap_or(config.cli_logging.stdio),
|
||||
pretty: env
|
||||
.komodo_cli_logging_pretty
|
||||
.unwrap_or(config.cli_logging.pretty),
|
||||
location: false,
|
||||
ansi: env
|
||||
.komodo_cli_logging_ansi
|
||||
.unwrap_or(config.cli_logging.ansi),
|
||||
otlp_endpoint: env
|
||||
.komodo_cli_logging_otlp_endpoint
|
||||
.unwrap_or(config.cli_logging.otlp_endpoint),
|
||||
opentelemetry_service_name: env
|
||||
.komodo_cli_logging_opentelemetry_service_name
|
||||
.unwrap_or(config.cli_logging.opentelemetry_service_name),
|
||||
opentelemetry_scope_name: env
|
||||
.komodo_cli_logging_opentelemetry_scope_name
|
||||
.unwrap_or(config.cli_logging.opentelemetry_scope_name),
|
||||
},
|
||||
profile: config.profile,
|
||||
}
|
||||
})
|
||||
}
|
||||
96
bin/cli/src/main.rs
Normal file
96
bin/cli/src/main.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
use anyhow::Context;
|
||||
use colored::Colorize;
|
||||
use komodo_client::entities::config::cli::args;
|
||||
|
||||
use crate::config::cli_config;
|
||||
|
||||
mod command;
|
||||
mod config;
|
||||
|
||||
async fn app() -> anyhow::Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
mogh_logger::init(&config::cli_config().cli_logging)?;
|
||||
let args = config::cli_args();
|
||||
let env = config::cli_env();
|
||||
let debug_load =
|
||||
args.debug_startup.unwrap_or(env.komodo_cli_debug_startup);
|
||||
|
||||
match &args.command {
|
||||
args::Command::Config {
|
||||
all_profiles,
|
||||
unsanitized,
|
||||
} => {
|
||||
let mut config = if *unsanitized {
|
||||
cli_config().clone()
|
||||
} else {
|
||||
cli_config().sanitized()
|
||||
};
|
||||
if !*all_profiles {
|
||||
config.profile = Default::default();
|
||||
}
|
||||
if debug_load {
|
||||
println!("\n{config:#?}");
|
||||
} else {
|
||||
println!(
|
||||
"\nCLI Config {}",
|
||||
serde_json::to_string_pretty(&config)
|
||||
.context("Failed to serialize config for pretty print")?
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
args::Command::Container(container) => {
|
||||
command::container::handle(container).await
|
||||
}
|
||||
args::Command::Inspect(inspect) => {
|
||||
command::container::inspect_container(inspect).await
|
||||
}
|
||||
args::Command::List(list) => command::list::handle(list).await,
|
||||
args::Command::Execute(args) => {
|
||||
command::execute::handle(&args.execution, args.yes).await
|
||||
}
|
||||
args::Command::Update { command } => {
|
||||
command::update::handle(command).await
|
||||
}
|
||||
args::Command::Connect(connect) => {
|
||||
command::terminal::handle_connect(connect).await
|
||||
}
|
||||
args::Command::Exec(exec) => {
|
||||
command::terminal::handle_exec(exec).await
|
||||
}
|
||||
args::Command::Attach(attach) => {
|
||||
command::terminal::handle_attach(attach).await
|
||||
}
|
||||
args::Command::Key { command } => {
|
||||
mogh_pki::cli::handle(command, mogh_pki::PkiKind::Mutual).await
|
||||
}
|
||||
args::Command::Database { command } => {
|
||||
command::database::handle(command).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let mut term_signal = tokio::signal::unix::signal(
|
||||
tokio::signal::unix::SignalKind::terminate(),
|
||||
)?;
|
||||
tokio::select! {
|
||||
res = tokio::spawn(app()) => match res {
|
||||
Ok(Err(e)) => {
|
||||
eprintln!("{}: {e}", "ERROR".red());
|
||||
std::process::exit(1)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}: {e}", "ERROR".red());
|
||||
std::process::exit(1)
|
||||
},
|
||||
Ok(_) => {}
|
||||
},
|
||||
_ = term_signal.recv() => {},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
86
bin/core/Cargo.toml
Normal file
86
bin/core/Cargo.toml
Normal file
@@ -0,0 +1,86 @@
|
||||
[package]
|
||||
name = "komodo_core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "core"
|
||||
path = "src/main.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# local
|
||||
komodo_client = { workspace = true, features = ["core"] }
|
||||
periphery_client.workspace = true
|
||||
mogh_validations.workspace = true
|
||||
interpolate.workspace = true
|
||||
mogh_secret_file.workspace = true
|
||||
formatting.workspace = true
|
||||
mogh_rate_limit.workspace = true
|
||||
transport.workspace = true
|
||||
database.workspace = true
|
||||
encoding.workspace = true
|
||||
command.workspace = true
|
||||
mogh_config.workspace = true
|
||||
mogh_logger.workspace = true
|
||||
mogh_cache.workspace = true
|
||||
mogh_pki.workspace = true
|
||||
git.workspace = true
|
||||
# mogh
|
||||
mogh_error = { workspace = true, features = ["axum"] }
|
||||
mogh_auth_client = { workspace = true, features = ["utoipa"] }
|
||||
mogh_auth_server.workspace = true
|
||||
async_timing_util.workspace = true
|
||||
partial_derive2.workspace = true
|
||||
mogh_resolver.workspace = true
|
||||
mogh_server.workspace = true
|
||||
toml_pretty.workspace = true
|
||||
slack.workspace = true
|
||||
svi.workspace = true
|
||||
# external
|
||||
aws-credential-types.workspace = true
|
||||
english-to-cron.workspace = true
|
||||
data-encoding.workspace = true
|
||||
serde_yaml_ng.workspace = true
|
||||
utoipa-scalar.workspace = true
|
||||
futures-util.workspace = true
|
||||
aws-sdk-ec2.workspace = true
|
||||
urlencoding.workspace = true
|
||||
aws-config.workspace = true
|
||||
tokio-util.workspace = true
|
||||
axum-extra.workspace = true
|
||||
serde_json.workspace = true
|
||||
typeshare.workspace = true
|
||||
chrono-tz.workspace = true
|
||||
indexmap.workspace = true
|
||||
wildcard.workspace = true
|
||||
arc-swap.workspace = true
|
||||
serde_qs.workspace = true
|
||||
colored.workspace = true
|
||||
tracing.workspace = true
|
||||
reqwest.workspace = true
|
||||
dotenvy.workspace = true
|
||||
anyhow.workspace = true
|
||||
bcrypt.workspace = true
|
||||
croner.workspace = true
|
||||
chrono.workspace = true
|
||||
rustls.workspace = true
|
||||
utoipa.workspace = true
|
||||
bytes.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
strum.workspace = true
|
||||
regex.workspace = true
|
||||
axum.workspace = true
|
||||
toml.workspace = true
|
||||
uuid.workspace = true
|
||||
envy.workspace = true
|
||||
hmac.workspace = true
|
||||
sha2.workspace = true
|
||||
hex.workspace = true
|
||||
url.workspace = true
|
||||
68
bin/core/aio.Dockerfile
Normal file
68
bin/core/aio.Dockerfile
Normal file
@@ -0,0 +1,68 @@
|
||||
## All in one, multi stage compile + runtime Docker build for your architecture.
|
||||
|
||||
# Build Core
|
||||
FROM rust:1.93.1-trixie AS core-builder
|
||||
RUN cargo install cargo-strip
|
||||
|
||||
WORKDIR /builder
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY ./lib ./lib
|
||||
COPY ./client/core/rs ./client/core/rs
|
||||
COPY ./client/periphery ./client/periphery
|
||||
COPY ./bin/core ./bin/core
|
||||
COPY ./bin/cli ./bin/cli
|
||||
|
||||
# Compile app
|
||||
RUN cargo build -p komodo_core --release && \
|
||||
cargo build -p komodo_cli --release && \
|
||||
cargo strip
|
||||
|
||||
# Build UI
|
||||
FROM node:22.12-alpine AS ui-builder
|
||||
WORKDIR /builder
|
||||
COPY ./ui ./ui
|
||||
COPY ./client/core/ts ./client
|
||||
RUN cd client && yarn && yarn build && yarn link
|
||||
RUN cd ui && yarn link komodo_client && yarn && yarn build
|
||||
|
||||
# Final Image
|
||||
FROM debian:trixie-slim
|
||||
|
||||
COPY ./bin/core/starship.toml /starship.toml
|
||||
COPY ./bin/core/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
# Setup an application directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy
|
||||
COPY ./config/core.config.toml /config/.default.config.toml
|
||||
COPY --from=ui-builder /builder/ui/dist /app/ui
|
||||
COPY --from=core-builder /builder/target/release/core /usr/local/bin/core
|
||||
COPY --from=core-builder /builder/target/release/km /usr/local/bin/km
|
||||
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
|
||||
|
||||
# Set $DENO_DIR and preload external Deno deps
|
||||
ENV DENO_DIR=/action-cache/deno
|
||||
RUN mkdir /action-cache && \
|
||||
cd /action-cache && \
|
||||
deno install jsr:@std/yaml jsr:@std/toml
|
||||
|
||||
COPY ./bin/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
# Hint at the port
|
||||
EXPOSE 9120
|
||||
|
||||
ENV KOMODO_CLI_CONFIG_PATHS="/config"
|
||||
# This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*`
|
||||
ENV KOMODO_CLI_CONFIG_KEYWORDS="*config.*,*komodo.cli*.*"
|
||||
|
||||
CMD [ "/bin/bash", "-c", "update-ca-certificates && core" ]
|
||||
|
||||
# Label to prevent Komodo from stopping with StopAllContainers
|
||||
LABEL komodo.skip="true"
|
||||
# Label for Ghcr
|
||||
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
|
||||
LABEL org.opencontainers.image.description="Komodo Core"
|
||||
LABEL org.opencontainers.image.licenses="GPL-3.0"
|
||||
14
bin/core/debian-deps.sh
Normal file
14
bin/core/debian-deps.sh
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
## Core deps installer
|
||||
|
||||
apt-get update
|
||||
apt-get install -y git curl ca-certificates iproute2
|
||||
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Starship prompt
|
||||
curl -sS https://starship.rs/install.sh | sh -s -- --yes --bin-dir /usr/local/bin
|
||||
echo 'export STARSHIP_CONFIG=/starship.toml' >> /root/.bashrc
|
||||
echo 'eval "$(starship init bash)"' >> /root/.bashrc
|
||||
|
||||
65
bin/core/multi-arch.Dockerfile
Normal file
65
bin/core/multi-arch.Dockerfile
Normal file
@@ -0,0 +1,65 @@
|
||||
## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile).
|
||||
## Sets up the necessary runtime container dependencies for Komodo Core.
|
||||
## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.
|
||||
|
||||
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
|
||||
ARG UI_IMAGE=ghcr.io/moghtech/komodo-ui:latest
|
||||
ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64
|
||||
ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64
|
||||
|
||||
# This is required to work with COPY --from
|
||||
FROM ${X86_64_BINARIES} AS x86_64
|
||||
FROM ${AARCH64_BINARIES} AS aarch64
|
||||
FROM ${UI_IMAGE} AS ui
|
||||
|
||||
# Final Image
|
||||
FROM debian:trixie-slim
|
||||
|
||||
COPY ./bin/core/starship.toml /starship.toml
|
||||
COPY ./bin/core/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM.
|
||||
COPY --from=x86_64 /core /app/core/linux/amd64
|
||||
COPY --from=aarch64 /core /app/core/linux/arm64
|
||||
RUN mv /app/core/${TARGETPLATFORM} /usr/local/bin/core && rm -r /app/core
|
||||
|
||||
# Same for km
|
||||
COPY --from=x86_64 /km /app/km/linux/amd64
|
||||
COPY --from=aarch64 /km /app/km/linux/arm64
|
||||
RUN mv /app/km/${TARGETPLATFORM} /usr/local/bin/km && rm -r /app/km
|
||||
|
||||
# Copy default config / static ui / deno binary
|
||||
COPY ./config/core.config.toml /config/.default.config.toml
|
||||
COPY --from=ui /ui /app/ui
|
||||
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
|
||||
|
||||
# Set $DENO_DIR and preload external Deno deps
|
||||
ENV DENO_DIR=/action-cache/deno
|
||||
RUN mkdir /action-cache && \
|
||||
cd /action-cache && \
|
||||
deno install jsr:@std/yaml jsr:@std/toml
|
||||
|
||||
COPY ./bin/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
# Hint at the port
|
||||
EXPOSE 9120
|
||||
|
||||
ENV KOMODO_CLI_CONFIG_PATHS="/config"
|
||||
# This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*`
|
||||
ENV KOMODO_CLI_CONFIG_KEYWORDS="*config.*,*komodo.cli*.*"
|
||||
|
||||
ENTRYPOINT [ "entrypoint.sh" ]
|
||||
CMD [ "core" ]
|
||||
|
||||
# Label to prevent Komodo from stopping with StopAllContainers
|
||||
LABEL komodo.skip="true"
|
||||
# Label for Ghcr
|
||||
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
|
||||
LABEL org.opencontainers.image.description="Komodo Core"
|
||||
LABEL org.opencontainers.image.licenses="GPL-3.0"
|
||||
54
bin/core/single-arch.Dockerfile
Normal file
54
bin/core/single-arch.Dockerfile
Normal file
@@ -0,0 +1,54 @@
|
||||
## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).
|
||||
## Sets up the necessary runtime container dependencies for Komodo Core.
|
||||
|
||||
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
|
||||
|
||||
# This is required to work with COPY --from
|
||||
FROM ${BINARIES_IMAGE} AS binaries
|
||||
|
||||
# Build UI
|
||||
FROM node:22.12-alpine AS ui-builder
|
||||
WORKDIR /builder
|
||||
COPY ./ui ./ui
|
||||
COPY ./client/core/ts ./client
|
||||
RUN cd client && yarn && yarn build && yarn link
|
||||
RUN cd ui && yarn link komodo_client && yarn && yarn build
|
||||
|
||||
FROM debian:trixie-slim
|
||||
|
||||
COPY ./bin/core/starship.toml /starship.toml
|
||||
COPY ./bin/core/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
# Copy
|
||||
COPY ./config/core.config.toml /config/.default.config.toml
|
||||
COPY --from=ui-builder /builder/ui/dist /app/ui
|
||||
COPY --from=binaries /core /usr/local/bin/core
|
||||
COPY --from=binaries /km /usr/local/bin/km
|
||||
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
|
||||
|
||||
# Set $DENO_DIR and preload external Deno deps
|
||||
ENV DENO_DIR=/action-cache/deno
|
||||
RUN mkdir /action-cache && \
|
||||
cd /action-cache && \
|
||||
deno install jsr:@std/yaml jsr:@std/toml
|
||||
|
||||
COPY ./bin/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
# Hint at the port
|
||||
EXPOSE 9120
|
||||
|
||||
ENV KOMODO_CLI_CONFIG_PATHS="/config"
|
||||
# This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*`
|
||||
ENV KOMODO_CLI_CONFIG_KEYWORDS="*config.*,*komodo.cli*.*"
|
||||
|
||||
ENTRYPOINT [ "entrypoint.sh" ]
|
||||
CMD [ "core" ]
|
||||
|
||||
# Label to prevent Komodo from stopping with StopAllContainers
|
||||
LABEL komodo.skip="true"
|
||||
# Label for Ghcr
|
||||
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
|
||||
LABEL org.opencontainers.image.description="Komodo Core"
|
||||
LABEL org.opencontainers.image.licenses="GPL-3.0"
|
||||
379
bin/core/src/alert/discord.rs
Normal file
379
bin/core/src/alert/discord.rs
Normal file
@@ -0,0 +1,379 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub async fn send_alert(
|
||||
url: &str,
|
||||
alert: &Alert,
|
||||
) -> anyhow::Result<()> {
|
||||
let level = fmt_level(alert.level);
|
||||
let content = match &alert.data {
|
||||
AlertData::Test { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Alerter, id);
|
||||
format!(
|
||||
"{level} | If you see this message, then Alerter **{name}** is **working**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::SwarmUnhealthy { id, name, err } => {
|
||||
let link = resource_link(ResourceTargetVariant::Swarm, id);
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
format!("{level} | Swarm **{name}** is now **healthy**\n{link}")
|
||||
}
|
||||
SeverityLevel::Critical => {
|
||||
let err = err
|
||||
.as_ref()
|
||||
.map(|e| format!("\n**error**: {e}"))
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"{level} | Swarm **{name}** is **unhealthy** ❌\n{link}{err}"
|
||||
)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
AlertData::ServerVersionMismatch {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
server_version,
|
||||
core_version,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
format!(
|
||||
"{level} | **{name}**{region} | Periphery version now matches Core version ✅\n{link}"
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
format!(
|
||||
"{level} | **{name}**{region} | Version mismatch detected ⚠️\nPeriphery: **{server_version}** | Core: **{core_version}**\n{link}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
AlertData::ServerUnreachable {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
err,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
format!(
|
||||
"{level} | **{name}**{region} is now **connected**\n{link}"
|
||||
)
|
||||
}
|
||||
SeverityLevel::Critical => {
|
||||
let err = err
|
||||
.as_ref()
|
||||
.map(|e| format!("\n**error**: {e:#?}"))
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"{level} | **{name}**{region} is **unreachable** ❌\n{link}{err}"
|
||||
)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
AlertData::ServerCpu {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
percentage,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
format!(
|
||||
"{level} | **{name}**{region} cpu usage at **{percentage:.1}%**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::ServerMem {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
used_gb,
|
||||
total_gb,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
format!(
|
||||
"{level} | **{name}**{region} memory usage at **{percentage:.1}%** 💾\n\nUsing **{used_gb:.1} GiB** / **{total_gb:.1} GiB**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::ServerDisk {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
path,
|
||||
used_gb,
|
||||
total_gb,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
format!(
|
||||
"{level} | **{name}**{region} disk usage at **{percentage:.1}%** 💿\nmount point: `{path:?}`\nusing **{used_gb:.1} GiB** / **{total_gb:.1} GiB**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::ContainerStateChange {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
let to = fmt_docker_container_state(to);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: **{swarm}**")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: **{server}**")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"📦 Deployment **{name}** is now **{to}**{target}\nprevious: **{from}**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::DeploymentImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: **{swarm}**")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: **{server}**")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Deployment **{name}** has an update available{target}\nimage: **{image}**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::DeploymentAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: **{swarm}**")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: **{server}**")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Deployment **{name}** was updated automatically ⏫{target}\nimage: **{image}**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::StackStateChange {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
let to = fmt_stack_state(to);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: **{swarm}**")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: **{server}**")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"🥞 Stack **{name}** is now {to}{target}\nprevious: **{from}**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::StackImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
service,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: **{swarm}**")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: **{server}**")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Stack **{name}** has an update available{target}\nservice: **{service}**\nimage: **{image}**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::StackAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
images,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
let images_label =
|
||||
if images.len() > 1 { "images" } else { "image" };
|
||||
let images = images.join(", ");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: **{swarm}**")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: **{server}**")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Stack **{name}** was updated automatically ⏫{target}\n{images_label}: **{images}**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::AwsBuilderTerminationFailed {
|
||||
instance_id,
|
||||
message,
|
||||
} => {
|
||||
format!(
|
||||
"{level} | Failed to terminated AWS builder instance\ninstance id: **{instance_id}**\n{message}"
|
||||
)
|
||||
}
|
||||
AlertData::ResourceSyncPendingUpdates { id, name } => {
|
||||
let link =
|
||||
resource_link(ResourceTargetVariant::ResourceSync, id);
|
||||
format!(
|
||||
"{level} | Pending resource sync updates on **{name}**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::BuildFailed { id, name, version } => {
|
||||
let link = resource_link(ResourceTargetVariant::Build, id);
|
||||
format!(
|
||||
"{level} | Build **{name}** failed\nversion: **v{version}**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::RepoBuildFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Repo, id);
|
||||
format!("{level} | Repo build for **{name}** failed\n{link}")
|
||||
}
|
||||
AlertData::ProcedureFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Procedure, id);
|
||||
format!("{level} | Procedure **{name}** failed\n{link}")
|
||||
}
|
||||
AlertData::ActionFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Action, id);
|
||||
format!("{level} | Action **{name}** failed\n{link}")
|
||||
}
|
||||
AlertData::ScheduleRun {
|
||||
resource_type,
|
||||
id,
|
||||
name,
|
||||
} => {
|
||||
let link = resource_link(*resource_type, id);
|
||||
format!(
|
||||
"{level} | **{name}** ({resource_type}) | Scheduled run started 🕝\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::Custom { message, details } => {
|
||||
format!(
|
||||
"{level} | {message}{}",
|
||||
if details.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("\n{details}")
|
||||
}
|
||||
)
|
||||
}
|
||||
AlertData::None {} => Default::default(),
|
||||
};
|
||||
|
||||
if content.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let VariablesAndSecrets { variables, secrets } =
|
||||
get_variables_and_secrets().await?;
|
||||
let mut url_interpolated = url.to_string();
|
||||
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolator.interpolate_string(&mut url_interpolated)?;
|
||||
|
||||
send_message(&url_interpolated, &content)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let replacers = interpolator
|
||||
.secret_replacers
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let sanitized_error =
|
||||
svi::replace_in_string(&format!("{e:?}"), &replacers);
|
||||
anyhow::Error::msg(format!(
|
||||
"Error with request to Discord: {sanitized_error}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
async fn send_message(
|
||||
url: &str,
|
||||
content: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let body = DiscordMessageBody { content };
|
||||
|
||||
let response = http_client()
|
||||
.post(url)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send message")?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
if status.is_success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let text = response.text().await.with_context(|| {
|
||||
format!("Failed to send message to Discord | {status} | failed to get response text")
|
||||
})?;
|
||||
Err(anyhow::anyhow!(
|
||||
"Failed to send message to Discord | {status} | {text}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn http_client() -> &'static reqwest::Client {
|
||||
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
|
||||
CLIENT.get_or_init(reqwest::Client::new)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DiscordMessageBody<'a> {
|
||||
content: &'a str,
|
||||
}
|
||||
546
bin/core/src/alert/mod.rs
Normal file
546
bin/core/src/alert/mod.rs
Normal file
@@ -0,0 +1,546 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use futures_util::future::join_all;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::entities::{
|
||||
ResourceTargetVariant,
|
||||
alert::{Alert, AlertData, AlertDataVariant, SeverityLevel},
|
||||
alerter::*,
|
||||
deployment::DeploymentState,
|
||||
komodo_timestamp,
|
||||
stack::StackState,
|
||||
};
|
||||
|
||||
use crate::helpers::query::get_variables_and_secrets;
|
||||
use crate::helpers::{
|
||||
maintenance::is_in_maintenance, query::VariablesAndSecrets,
|
||||
};
|
||||
use crate::{config::core_config, state::db_client};
|
||||
|
||||
mod discord;
|
||||
mod ntfy;
|
||||
mod pushover;
|
||||
mod slack;
|
||||
|
||||
pub async fn send_alerts(alerts: &[Alert]) {
|
||||
if alerts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(alerters) = find_collect(
|
||||
&db_client().alerters,
|
||||
doc! { "config.enabled": true },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
error!(
|
||||
"ERROR sending alerts | failed to get alerters from db | {e:#}"
|
||||
)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let handles = alerts
|
||||
.iter()
|
||||
.map(|alert| send_alert_to_alerters(&alerters, alert));
|
||||
|
||||
join_all(handles).await;
|
||||
}
|
||||
|
||||
async fn send_alert_to_alerters(alerters: &[Alerter], alert: &Alert) {
|
||||
if alerters.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let handles = alerters
|
||||
.iter()
|
||||
.map(|alerter| send_alert_to_alerter(alerter, alert));
|
||||
|
||||
join_all(handles)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|res| res.err())
|
||||
.for_each(|e| error!("{e:#}"));
|
||||
}
|
||||
|
||||
pub async fn send_alert_to_alerter(
|
||||
alerter: &Alerter,
|
||||
alert: &Alert,
|
||||
) -> anyhow::Result<()> {
|
||||
// Don't send if not enabled
|
||||
if !alerter.config.enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if is_in_maintenance(
|
||||
&alerter.config.maintenance_windows,
|
||||
komodo_timestamp(),
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let alert_variant: AlertDataVariant = (&alert.data).into();
|
||||
|
||||
// In the test case, we don't want the filters inside this
|
||||
// block to stop the test from being sent to the alerting endpoint.
|
||||
if alert_variant != AlertDataVariant::Test {
|
||||
// Don't send if alert type not configured on the alerter
|
||||
if !alerter.config.alert_types.is_empty()
|
||||
&& !alerter.config.alert_types.contains(&alert_variant)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Don't send if resource is in the blacklist
|
||||
if alerter.config.except_resources.contains(&alert.target) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Don't send if whitelist configured and target is not included
|
||||
if !alerter.config.resources.is_empty()
|
||||
&& !alerter.config.resources.contains(&alert.target)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
match &alerter.config.endpoint {
|
||||
AlerterEndpoint::Custom(CustomAlerterEndpoint { url }) => {
|
||||
send_custom_alert(url, alert).await.with_context(|| {
|
||||
format!(
|
||||
"Failed to send alert to Custom Alerter {}",
|
||||
alerter.name
|
||||
)
|
||||
})
|
||||
}
|
||||
AlerterEndpoint::Slack(SlackAlerterEndpoint { url }) => {
|
||||
slack::send_alert(url, alert).await.with_context(|| {
|
||||
format!(
|
||||
"Failed to send alert to Slack Alerter {}",
|
||||
alerter.name
|
||||
)
|
||||
})
|
||||
}
|
||||
AlerterEndpoint::Discord(DiscordAlerterEndpoint { url }) => {
|
||||
discord::send_alert(url, alert).await.with_context(|| {
|
||||
format!(
|
||||
"Failed to send alert to Discord Alerter {}",
|
||||
alerter.name
|
||||
)
|
||||
})
|
||||
}
|
||||
AlerterEndpoint::Ntfy(NtfyAlerterEndpoint { url, email }) => {
|
||||
ntfy::send_alert(url, email.as_deref(), alert)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to send alert to ntfy Alerter {}",
|
||||
alerter.name
|
||||
)
|
||||
})
|
||||
}
|
||||
AlerterEndpoint::Pushover(PushoverAlerterEndpoint { url }) => {
|
||||
pushover::send_alert(url, alert).await.with_context(|| {
|
||||
format!(
|
||||
"Failed to send alert to Pushover Alerter {}",
|
||||
alerter.name
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_custom_alert(
|
||||
url: &str,
|
||||
alert: &Alert,
|
||||
) -> anyhow::Result<()> {
|
||||
let VariablesAndSecrets { variables, secrets } =
|
||||
get_variables_and_secrets().await?;
|
||||
let mut url_interpolated = url.to_string();
|
||||
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolator.interpolate_string(&mut url_interpolated)?;
|
||||
|
||||
let res = reqwest::Client::new()
|
||||
.post(url_interpolated)
|
||||
.json(alert)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let replacers = interpolator
|
||||
.secret_replacers
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let sanitized_error =
|
||||
svi::replace_in_string(&format!("{e:?}"), &replacers);
|
||||
anyhow::Error::msg(format!(
|
||||
"Error with request: {sanitized_error}"
|
||||
))
|
||||
})
|
||||
.context("failed at post request to alerter")?;
|
||||
let status = res.status();
|
||||
if !status.is_success() {
|
||||
let text = res
|
||||
.text()
|
||||
.await
|
||||
.context("failed to get response text on alerter response")?;
|
||||
return Err(anyhow!(
|
||||
"post to alerter failed | {status} | {text}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fmt_region(region: &Option<String>) -> String {
|
||||
match region {
|
||||
Some(region) => format!(" ({region})"),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_docker_container_state(state: &DeploymentState) -> String {
|
||||
match state {
|
||||
DeploymentState::Running => String::from("Running ▶️"),
|
||||
DeploymentState::Exited => String::from("Exited 🛑"),
|
||||
DeploymentState::Restarting => String::from("Restarting 🔄"),
|
||||
DeploymentState::NotDeployed => String::from("Not Deployed"),
|
||||
_ => state.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_stack_state(state: &StackState) -> String {
|
||||
match state {
|
||||
StackState::Running => String::from("Running ▶️"),
|
||||
StackState::Stopped => String::from("Stopped 🛑"),
|
||||
StackState::Restarting => String::from("Restarting 🔄"),
|
||||
StackState::Down => String::from("Down ⬇️"),
|
||||
_ => state.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_level(level: SeverityLevel) -> &'static str {
|
||||
match level {
|
||||
SeverityLevel::Critical => "CRITICAL 🚨",
|
||||
SeverityLevel::Warning => "WARNING ‼️",
|
||||
SeverityLevel::Ok => "OK ✅",
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_link(
|
||||
resource_type: ResourceTargetVariant,
|
||||
id: &str,
|
||||
) -> String {
|
||||
komodo_client::entities::resource_link(
|
||||
&core_config().host,
|
||||
resource_type,
|
||||
id,
|
||||
)
|
||||
}
|
||||
|
||||
/// Standard message content format
|
||||
/// used by Ntfy, Pushover.
|
||||
fn standard_alert_content(alert: &Alert) -> String {
|
||||
let level = fmt_level(alert.level);
|
||||
match &alert.data {
|
||||
AlertData::Test { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Alerter, id);
|
||||
format!(
|
||||
"{level} | If you see this message, then Alerter {name} is working\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::SwarmUnhealthy { id, name, err } => {
|
||||
let link = resource_link(ResourceTargetVariant::Swarm, id);
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
format!("{level} | Swarm {name} is now healthy\n{link}")
|
||||
}
|
||||
SeverityLevel::Critical => {
|
||||
let err = err
|
||||
.as_ref()
|
||||
.map(|e| format!("\nerror: {e}"))
|
||||
.unwrap_or_default();
|
||||
format!("{level} | Swarm {name} is unhealthy ❌\n{link}{err}")
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
AlertData::ServerVersionMismatch {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
server_version,
|
||||
core_version,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
format!(
|
||||
"{level} | {name}{region} | Periphery version now matches Core version ✅\n{link}"
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
format!(
|
||||
"{level} | {name}{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}\n{link}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
AlertData::ServerUnreachable {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
err,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
format!("{level} | {name}{region} is now connected\n{link}")
|
||||
}
|
||||
SeverityLevel::Critical => {
|
||||
let err = err
|
||||
.as_ref()
|
||||
.map(|e| format!("\nerror: {e:#?}"))
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"{level} | {name}{region} is unreachable ❌\n{link}{err}"
|
||||
)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
AlertData::ServerCpu {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
percentage,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
format!(
|
||||
"{level} | {name}{region} cpu usage at {percentage:.1}%\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::ServerMem {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
used_gb,
|
||||
total_gb,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
format!(
|
||||
"{level} | {name}{region} memory usage at {percentage:.1}%💾\n\nUsing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::ServerDisk {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
path,
|
||||
used_gb,
|
||||
total_gb,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
format!(
|
||||
"{level} | {name}{region} disk usage at {percentage:.1}%💿\nmount point: {path:?}\nusing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::ContainerStateChange {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
let to_state = fmt_docker_container_state(to);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: {swarm}")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: {server}")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"📦Deployment {name} is now {to_state}{target}\nprevious: {from}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::DeploymentImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: {swarm}")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: {server}")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Deployment {name} has an update available{target}\nimage: {image}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::DeploymentAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: {swarm}")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: {server}")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Deployment {name} was updated automatically{target}\nimage: {image}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::StackStateChange {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
let to_state = fmt_stack_state(to);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: {swarm}")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: {server}")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"🥞 Stack {name} is now {to_state}{target}\nprevious: {from}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::StackImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
service,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: {swarm}")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: {server}")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Stack {name} has an update available{target}\nservice: {service}\nimage: {image}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::StackAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
images,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
let images_label =
|
||||
if images.len() > 1 { "images" } else { "image" };
|
||||
let images_str = images.join(", ");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: {swarm}")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: {server}")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Stack {name} was updated automatically ⏫{target}\n{images_label}: {images_str}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::AwsBuilderTerminationFailed {
|
||||
instance_id,
|
||||
message,
|
||||
} => {
|
||||
format!(
|
||||
"{level} | Failed to terminate AWS builder instance\ninstance id: {instance_id}\n{message}",
|
||||
)
|
||||
}
|
||||
AlertData::ResourceSyncPendingUpdates { id, name } => {
|
||||
let link =
|
||||
resource_link(ResourceTargetVariant::ResourceSync, id);
|
||||
format!(
|
||||
"{level} | Pending resource sync updates on {name}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::BuildFailed { id, name, version } => {
|
||||
let link = resource_link(ResourceTargetVariant::Build, id);
|
||||
format!(
|
||||
"{level} | Build {name} failed\nversion: v{version}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::RepoBuildFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Repo, id);
|
||||
format!("{level} | Repo build for {name} failed\n{link}",)
|
||||
}
|
||||
AlertData::ProcedureFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Procedure, id);
|
||||
format!("{level} | Procedure {name} failed\n{link}")
|
||||
}
|
||||
AlertData::ActionFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Action, id);
|
||||
format!("{level} | Action {name} failed\n{link}")
|
||||
}
|
||||
AlertData::ScheduleRun {
|
||||
resource_type,
|
||||
id,
|
||||
name,
|
||||
} => {
|
||||
let link = resource_link(*resource_type, id);
|
||||
format!(
|
||||
"{level} | {name} ({resource_type}) | Scheduled run started 🕝\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::Custom { message, details } => {
|
||||
format!(
|
||||
"{level} | {message}{}",
|
||||
if details.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("\n{details}")
|
||||
}
|
||||
)
|
||||
}
|
||||
AlertData::None {} => Default::default(),
|
||||
}
|
||||
}
|
||||
75
bin/core/src/alert/ntfy.rs
Normal file
75
bin/core/src/alert/ntfy.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub async fn send_alert(
|
||||
url: &str,
|
||||
email: Option<&str>,
|
||||
alert: &Alert,
|
||||
) -> anyhow::Result<()> {
|
||||
let content = standard_alert_content(alert);
|
||||
if content.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let VariablesAndSecrets { variables, secrets } =
|
||||
get_variables_and_secrets().await?;
|
||||
let mut url_interpolated = url.to_string();
|
||||
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolator.interpolate_string(&mut url_interpolated)?;
|
||||
|
||||
send_message(&url_interpolated, email, content)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let replacers = interpolator
|
||||
.secret_replacers
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let sanitized_error =
|
||||
svi::replace_in_string(&format!("{e:?}"), &replacers);
|
||||
anyhow::Error::msg(format!(
|
||||
"Error with request to Ntfy: {sanitized_error}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
async fn send_message(
|
||||
url: &str,
|
||||
email: Option<&str>,
|
||||
content: String,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut request = http_client()
|
||||
.post(url)
|
||||
.header("Title", "Komodo Alert")
|
||||
.body(content);
|
||||
|
||||
if let Some(email) = email {
|
||||
request = request.header("X-Email", email);
|
||||
}
|
||||
|
||||
let response =
|
||||
request.send().await.context("Failed to send message")?;
|
||||
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
debug!("ntfy alert sent successfully: {}", status);
|
||||
Ok(())
|
||||
} else {
|
||||
let text = response.text().await.with_context(|| {
|
||||
format!(
|
||||
"Failed to send message to ntfy | {status} | failed to get response text"
|
||||
)
|
||||
})?;
|
||||
Err(anyhow!(
|
||||
"Failed to send message to ntfy | {status} | {text}",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn http_client() -> &'static reqwest::Client {
|
||||
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
|
||||
CLIENT.get_or_init(reqwest::Client::new)
|
||||
}
|
||||
74
bin/core/src/alert/pushover.rs
Normal file
74
bin/core/src/alert/pushover.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub async fn send_alert(
|
||||
url: &str,
|
||||
alert: &Alert,
|
||||
) -> anyhow::Result<()> {
|
||||
let content = standard_alert_content(alert);
|
||||
if content.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let VariablesAndSecrets { variables, secrets } =
|
||||
get_variables_and_secrets().await?;
|
||||
let mut url_interpolated = url.to_string();
|
||||
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolator.interpolate_string(&mut url_interpolated)?;
|
||||
|
||||
send_message(&url_interpolated, content).await.map_err(|e| {
|
||||
let replacers = interpolator
|
||||
.secret_replacers
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let sanitized_error =
|
||||
svi::replace_in_string(&format!("{e:?}"), &replacers);
|
||||
anyhow::Error::msg(format!(
|
||||
"Error with request to Pushover: {sanitized_error}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
async fn send_message(
|
||||
url: &str,
|
||||
content: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// pushover needs all information to be encoded in the URL. At minimum they need
|
||||
// the user key, the application token, and the message (url encoded).
|
||||
// other optional params here: https://pushover.net/api (just add them to the
|
||||
// webhook url along with the application token and the user key).
|
||||
let content = [("message", content)];
|
||||
|
||||
let response = http_client()
|
||||
.post(url)
|
||||
.form(&content)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send message")?;
|
||||
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
debug!("pushover alert sent successfully: {}", status);
|
||||
Ok(())
|
||||
} else {
|
||||
let text = response.text().await.with_context(|| {
|
||||
format!(
|
||||
"Failed to send message to pushover | {status} | failed to get response text"
|
||||
)
|
||||
})?;
|
||||
Err(anyhow!(
|
||||
"Failed to send message to pushover | {} | {}",
|
||||
status,
|
||||
text
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn http_client() -> &'static reqwest::Client {
|
||||
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
|
||||
CLIENT.get_or_init(reqwest::Client::new)
|
||||
}
|
||||
579
bin/core/src/alert/slack.rs
Normal file
579
bin/core/src/alert/slack.rs
Normal file
@@ -0,0 +1,579 @@
|
||||
use ::slack::types::OwnedBlock as Block;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub async fn send_alert(
|
||||
url: &str,
|
||||
alert: &Alert,
|
||||
) -> anyhow::Result<()> {
|
||||
let level = fmt_level(alert.level);
|
||||
let (text, blocks): (_, Option<_>) = match &alert.data {
|
||||
AlertData::Test { id, name } => {
|
||||
let text = format!(
|
||||
"{level} | If you see this message, then Alerter *{name}* is *working*"
|
||||
);
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"If you see this message, then Alerter *{name}* is *working*"
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Alerter,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::SwarmUnhealthy { id, name, err } => {
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
let text =
|
||||
format!("{level} | Swarm *{name}* is now *healthy*");
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"Swarm *{name}* is now *healthy*"
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
SeverityLevel::Critical => {
|
||||
let text =
|
||||
format!("{level} | Swarm *{name}* is *unhealthy* ❌");
|
||||
let err = err
|
||||
.as_ref()
|
||||
.map(|e| format!("\nerror: {e}"))
|
||||
.unwrap_or_default();
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"Swarm *{name}* is *unhealthy* ❌{err}"
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Server,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
AlertData::ServerVersionMismatch {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
server_version,
|
||||
core_version,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let text = match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
format!(
|
||||
"{level} | *{name}*{region} | Periphery version now matches Core version ✅"
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
format!(
|
||||
"{level} | *{name}*{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}"
|
||||
)
|
||||
}
|
||||
};
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Server,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::ServerUnreachable {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
err,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
let text =
|
||||
format!("{level} | *{name}*{region} is now *connected*");
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} is now *connected*"
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
SeverityLevel::Critical => {
|
||||
let text =
|
||||
format!("{level} | *{name}*{region} is *unreachable* ❌");
|
||||
let err = err
|
||||
.as_ref()
|
||||
.map(|e| format!("\nerror: {e:#?}"))
|
||||
.unwrap_or_default();
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} is *unreachable* ❌{err}"
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Server,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
AlertData::ServerCpu {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
percentage,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
let text = format!(
|
||||
"{level} | *{name}*{region} cpu usage at *{percentage:.1}%*"
|
||||
);
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} cpu usage at *{percentage:.1}%*"
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Server,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
_ => {
|
||||
let text = format!(
|
||||
"{level} | *{name}*{region} cpu usage at *{percentage:.1}%* 📈"
|
||||
);
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} cpu usage at *{percentage:.1}%* 📈"
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Server,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
AlertData::ServerMem {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
used_gb,
|
||||
total_gb,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
let text = format!(
|
||||
"{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾"
|
||||
);
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} memory usage at *{percentage:.1}%* 💾"
|
||||
)),
|
||||
Block::section(format!(
|
||||
"using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Server,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
_ => {
|
||||
let text = format!(
|
||||
"{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾"
|
||||
);
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} memory usage at *{percentage:.1}%* 💾"
|
||||
)),
|
||||
Block::section(format!(
|
||||
"using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Server,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
AlertData::ServerDisk {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
path,
|
||||
used_gb,
|
||||
total_gb,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
let text = format!(
|
||||
"{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿"
|
||||
);
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} disk usage at *{percentage:.1}%* 💿"
|
||||
)),
|
||||
Block::section(format!(
|
||||
"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Server,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
_ => {
|
||||
let text = format!(
|
||||
"{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿"
|
||||
);
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} disk usage at *{percentage:.1}%* 💿"
|
||||
)),
|
||||
Block::section(format!(
|
||||
"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Server,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
AlertData::ContainerStateChange {
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
id,
|
||||
} => {
|
||||
let to = fmt_docker_container_state(to);
|
||||
let text = format!("📦 Container *{name}* is now *{to}*");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("swarm: *{swarm}*\n")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("server: *{server}*\n")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!("{target}previous: {from}",)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Deployment,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::DeploymentImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let text =
|
||||
format!("⬆ Deployment *{name}* has an update available");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("swarm: *{swarm}*\n")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("server: *{server}*\n")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!("{target}image: *{image}*",)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Deployment,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::DeploymentAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let text =
|
||||
format!("⬆ Deployment *{name}* was updated automatically ⏫");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("swarm: *{swarm}*\n")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("server: *{server}*\n")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!("{target}image: *{image}*",)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Deployment,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::StackStateChange {
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
id,
|
||||
} => {
|
||||
let to = fmt_stack_state(to);
|
||||
let text = format!("🥞 Stack *{name}* is now *{to}*");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("swarm: *{swarm}*\n")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("server: *{server}*\n")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!("{target}previous: *{from}*",)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Stack,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::StackImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
service,
|
||||
image,
|
||||
} => {
|
||||
let text = format!("⬆ Stack *{name}* has an update available");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("swarm: *{swarm}*\n")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("server: *{server}*\n")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"{target}service: *{service}*\nimage: *{image}*",
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Stack,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::StackAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
images,
|
||||
} => {
|
||||
let text =
|
||||
format!("⬆ Stack *{name}* was updated automatically ⏫");
|
||||
let images_label =
|
||||
if images.len() > 1 { "images" } else { "image" };
|
||||
let images = images.join(", ");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("swarm: *{swarm}*\n")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("server: *{server}*\n")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(
|
||||
format!("{target}{images_label}: *{images}*",),
|
||||
),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Stack,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::AwsBuilderTerminationFailed {
|
||||
instance_id,
|
||||
message,
|
||||
} => {
|
||||
let text = format!(
|
||||
"{level} | Failed to terminated AWS builder instance "
|
||||
);
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"instance id: *{instance_id}*\n{message}"
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::ResourceSyncPendingUpdates { id, name } => {
|
||||
let text = format!(
|
||||
"{level} | Pending resource sync updates on *{name}*"
|
||||
);
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"sync id: *{id}*\nsync name: *{name}*",
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::ResourceSync,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::BuildFailed { id, name, version } => {
|
||||
let text = format!("{level} | Build {name} has failed");
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!("version: *v{version}*",)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Build,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::RepoBuildFailed { id, name } => {
|
||||
let text =
|
||||
format!("{level} | Repo build for *{name}* has *failed*");
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Repo,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::ProcedureFailed { id, name } => {
|
||||
let text = format!("{level} | Procedure *{name}* has *failed*");
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Procedure,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::ActionFailed { id, name } => {
|
||||
let text = format!("{level} | Action *{name}* has *failed*");
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Action,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::ScheduleRun {
|
||||
resource_type,
|
||||
id,
|
||||
name,
|
||||
} => {
|
||||
let text = format!(
|
||||
"{level} | *{name}* ({resource_type}) | Scheduled run started 🕝"
|
||||
);
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(resource_link(*resource_type, id)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::Custom { message, details } => {
|
||||
let text = format!("{level} | {message}");
|
||||
let blocks =
|
||||
vec![Block::header(text.clone()), Block::section(details)];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::None {} => Default::default(),
|
||||
};
|
||||
if text.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let VariablesAndSecrets { variables, secrets } =
|
||||
get_variables_and_secrets().await?;
|
||||
let mut url_interpolated = url.to_string();
|
||||
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolator.interpolate_string(&mut url_interpolated)?;
|
||||
|
||||
let slack = ::slack::Client::new(url_interpolated);
|
||||
slack
|
||||
.send_owned_message_single(&text, None, blocks.as_deref())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let replacers = interpolator
|
||||
.secret_replacers
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let sanitized_error =
|
||||
svi::replace_in_string(&format!("{e:?}"), &replacers);
|
||||
anyhow::Error::msg(format!(
|
||||
"Error with request to Slack: {sanitized_error}"
|
||||
))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
457
bin/core/src/api/execute/action.rs
Normal file
457
bin/core/src/api/execute/action.rs
Normal file
@@ -0,0 +1,457 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use command::run_komodo_standard_command;
|
||||
use database::mungos::{
|
||||
by_id::update_one_by_id, mongodb::bson::to_document,
|
||||
};
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::execute::{BatchExecutionResponse, BatchRunAction, RunAction},
|
||||
entities::{
|
||||
FileFormat, JsonObject,
|
||||
action::Action,
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
config::core::CoreConfig,
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
random_string,
|
||||
update::Update,
|
||||
user::action_user,
|
||||
},
|
||||
parsers::parse_key_value_list,
|
||||
};
|
||||
use mogh_auth_client::api::manage::{
|
||||
CreateApiKey, CreateApiKeyResponse,
|
||||
};
|
||||
use mogh_auth_server::api::manage::api_key::{
|
||||
create_api_key, delete_api_key,
|
||||
};
|
||||
use mogh_config::merge_objects;
|
||||
use mogh_resolver::Resolve;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
api::execute::ExecuteRequest,
|
||||
auth::KomodoAuthImpl,
|
||||
config::core_config,
|
||||
helpers::{
|
||||
query::{VariablesAndSecrets, get_variables_and_secrets},
|
||||
update::update_update,
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource::refresh_action_state_cache,
|
||||
state::{action_states, db_client},
|
||||
};
|
||||
|
||||
use super::ExecuteArgs;
|
||||
|
||||
impl super::BatchExecute for BatchRunAction {
|
||||
type Resource = Action;
|
||||
fn single_request(action: String) -> ExecuteRequest {
|
||||
ExecuteRequest::RunAction(RunAction {
|
||||
action,
|
||||
args: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for BatchRunAction {
|
||||
#[instrument(
|
||||
"BatchRunAction",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
pattern = self.pattern,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchRunAction>(&self.pattern, user)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RunAction {
|
||||
#[instrument(
|
||||
"RunAction",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
action = self.action,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let mut action = get_check_permissions::<Action>(
|
||||
&self.action,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// get the action state for the action (or insert default).
|
||||
let action_state = action_states()
|
||||
.action
|
||||
.get_or_insert_default(&action.id)
|
||||
.await;
|
||||
|
||||
// This will set action state back to default when dropped.
|
||||
// Will also check to ensure action not already busy before updating.
|
||||
let _action_guard = action_state.update_custom(
|
||||
|state| state.running += 1,
|
||||
|state| state.running -= 1,
|
||||
false,
|
||||
)?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let default_args = parse_action_arguments(
|
||||
&action.config.arguments,
|
||||
action.config.arguments_format,
|
||||
)
|
||||
.context("Failed to parse default Action arguments")?;
|
||||
|
||||
let args = merge_objects(
|
||||
default_args,
|
||||
self.args.unwrap_or_default(),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.context("Failed to merge request args with default args")?;
|
||||
|
||||
let args = serde_json::to_string(&args)
|
||||
.context("Failed to serialize action run arguments")?;
|
||||
|
||||
let CreateApiKeyResponse { key, secret } = create_api_key(
|
||||
&KomodoAuthImpl,
|
||||
action_user().id.clone(),
|
||||
CreateApiKey {
|
||||
name: update.id.clone(),
|
||||
expires: 0,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Do next steps in seperate error handling block,
|
||||
// and delete the API key before unwrapping the error.
|
||||
// If Komodo shuts down during these steps, there will
|
||||
// be a dangling api key in the DB with user_id: "000000000000000000000002".
|
||||
// These need to be
|
||||
let res = async {
|
||||
let contents = &mut action.config.file_contents;
|
||||
|
||||
// Wrap the file contents in the execution context.
|
||||
*contents = full_contents(contents, &args, &key, &secret);
|
||||
|
||||
let replacers = interpolate(
|
||||
contents,
|
||||
&mut update,
|
||||
key.clone(),
|
||||
secret.clone(),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let file = format!("{}.ts", random_string(10));
|
||||
let path = core_config().action_directory.join(&file);
|
||||
|
||||
mogh_secret_file::write_async(&path, contents)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Failed to write action file to {path:?}")
|
||||
})?;
|
||||
|
||||
let CoreConfig { ssl_enabled, .. } = core_config();
|
||||
|
||||
let https_cert_flag = if *ssl_enabled {
|
||||
" --unsafely-ignore-certificate-errors=localhost"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let reload = if action.config.reload_deno_deps {
|
||||
" --reload"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let mut res = run_komodo_standard_command(
|
||||
// Keep this stage name as is, the UI will find the latest update log by matching the stage name
|
||||
"Execute Action",
|
||||
None,
|
||||
format!(
|
||||
"deno run --allow-all{https_cert_flag}{reload} {}",
|
||||
path.display()
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
res.stdout = svi::replace_in_string(&res.stdout, &replacers)
|
||||
.replace(&key, "<ACTION_API_KEY>");
|
||||
res.stderr = svi::replace_in_string(&res.stderr, &replacers)
|
||||
.replace(&secret, "<ACTION_API_SECRET>");
|
||||
|
||||
cleanup_run(file + ".js", &path).await;
|
||||
|
||||
update.logs.push(res);
|
||||
update.finalize();
|
||||
|
||||
mogh_error::Ok(update)
|
||||
}
|
||||
.await;
|
||||
|
||||
if let Err(e) =
|
||||
delete_api_key(&KomodoAuthImpl, &action_user().id, key).await
|
||||
{
|
||||
warn!(
|
||||
"Failed to delete API key after action execution | {:#}",
|
||||
e.error
|
||||
);
|
||||
};
|
||||
|
||||
let update = res?;
|
||||
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with update_update.
|
||||
// The Err case of to_document should be unreachable,
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db_client().updates,
|
||||
&update.id,
|
||||
database::mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
refresh_action_state_cache().await;
|
||||
}
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
if !update.success && action.config.failure_alert {
|
||||
let target = update.target.clone();
|
||||
tokio::spawn(async move {
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
target,
|
||||
ts: komodo_timestamp(),
|
||||
resolved_ts: Some(komodo_timestamp()),
|
||||
resolved: true,
|
||||
level: SeverityLevel::Warning,
|
||||
data: AlertData::ActionFailed {
|
||||
id: action.id,
|
||||
name: action.name,
|
||||
},
|
||||
};
|
||||
send_alerts(&[alert]).await
|
||||
});
|
||||
}
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument("Interpolate", skip(contents, update, secret))]
|
||||
async fn interpolate(
|
||||
contents: &mut String,
|
||||
update: &mut Update,
|
||||
key: String,
|
||||
secret: String,
|
||||
) -> mogh_error::Result<HashSet<(String, String)>> {
|
||||
let VariablesAndSecrets {
|
||||
variables,
|
||||
mut secrets,
|
||||
} = get_variables_and_secrets().await?;
|
||||
|
||||
secrets.insert(String::from("ACTION_API_KEY"), key);
|
||||
secrets.insert(String::from("ACTION_API_SECRET"), secret);
|
||||
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolator
|
||||
.interpolate_string(contents)?
|
||||
.push_logs(&mut update.logs);
|
||||
|
||||
Ok(interpolator.secret_replacers)
|
||||
}
|
||||
|
||||
fn full_contents(
|
||||
contents: &str,
|
||||
// Pre-serialized to JSON string.
|
||||
args: &str,
|
||||
key: &str,
|
||||
secret: &str,
|
||||
) -> String {
|
||||
let CoreConfig {
|
||||
port, ssl_enabled, ..
|
||||
} = core_config();
|
||||
let protocol = if *ssl_enabled { "https" } else { "http" };
|
||||
let base_url = format!("{protocol}://localhost:{port}");
|
||||
format!(
|
||||
"import {{ KomodoClient, Types }} from '{base_url}/client/lib.js';
|
||||
import * as __YAML__ from 'jsr:@std/yaml';
|
||||
import * as __TOML__ from 'jsr:@std/toml';
|
||||
|
||||
const YAML = {{
|
||||
stringify: __YAML__.stringify,
|
||||
parse: __YAML__.parse,
|
||||
parseAll: __YAML__.parseAll,
|
||||
parseDockerCompose: __YAML__.parse,
|
||||
}}
|
||||
|
||||
const TOML = {{
|
||||
stringify: __TOML__.stringify,
|
||||
parse: __TOML__.parse,
|
||||
parseResourceToml: __TOML__.parse,
|
||||
parseCargoToml: __TOML__.parse,
|
||||
}}
|
||||
|
||||
const ARGS = {args};
|
||||
|
||||
const komodo = KomodoClient('{base_url}', {{
|
||||
type: 'api-key',
|
||||
params: {{ key: '{key}', secret: '{secret}' }}
|
||||
}});
|
||||
|
||||
async function main() {{
|
||||
{contents}
|
||||
|
||||
console.log('🦎 Action completed successfully 🦎');
|
||||
}}
|
||||
|
||||
main()
|
||||
.catch(error => {{
|
||||
console.error('🚨 Action exited early with errors 🚨')
|
||||
if (error.status !== undefined && error.result !== undefined) {{
|
||||
console.error('Status:', error.status);
|
||||
console.error(JSON.stringify(error.result, null, 2));
|
||||
}} else {{
|
||||
console.error(error);
|
||||
}}
|
||||
Deno.exit(1)
|
||||
}});"
|
||||
)
|
||||
}
|
||||
|
||||
/// Cleans up file at given path.
|
||||
/// ALSO if $DENO_DIR is set,
|
||||
/// will clean up the generated file matching "file"
|
||||
#[instrument("CleanupRun")]
|
||||
async fn cleanup_run(file: String, path: &Path) {
|
||||
if let Err(e) = fs::remove_file(path).await {
|
||||
warn!(
|
||||
"Failed to delete action file after action execution | {e:#}"
|
||||
);
|
||||
}
|
||||
// If $DENO_DIR is set (will be in container),
|
||||
// will clean up the generated file matching "file" (NOT under path)
|
||||
let Some(deno_dir) = deno_dir() else {
|
||||
return;
|
||||
};
|
||||
delete_file(deno_dir.join("gen/file"), file).await;
|
||||
}
|
||||
|
||||
fn deno_dir() -> Option<&'static Path> {
|
||||
static DENO_DIR: OnceLock<Option<PathBuf>> = OnceLock::new();
|
||||
DENO_DIR
|
||||
.get_or_init(|| {
|
||||
let deno_dir = std::env::var("DENO_DIR").ok()?;
|
||||
Some(PathBuf::from(&deno_dir))
|
||||
})
|
||||
.as_deref()
|
||||
}
|
||||
|
||||
/// file is just the terminating file path,
|
||||
/// it may be nested multiple folder under path,
|
||||
/// this will find the nested file and delete it.
|
||||
/// Assumes the file is only there once.
|
||||
fn delete_file(
|
||||
dir: PathBuf,
|
||||
file: String,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send>>
|
||||
{
|
||||
Box::pin(async move {
|
||||
let Ok(mut dir) = fs::read_dir(dir).await else {
|
||||
return false;
|
||||
};
|
||||
// Collect the nested folders for recursing
|
||||
// only after checking all the files in directory.
|
||||
let mut folders = Vec::<PathBuf>::new();
|
||||
|
||||
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||
let Ok(meta) = entry.metadata().await else {
|
||||
continue;
|
||||
};
|
||||
if meta.is_file() {
|
||||
let Ok(name) = entry.file_name().into_string() else {
|
||||
continue;
|
||||
};
|
||||
if name == file {
|
||||
if let Err(e) = fs::remove_file(entry.path()).await {
|
||||
warn!(
|
||||
"Failed to clean up generated file after action execution | {e:#}"
|
||||
);
|
||||
};
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
folders.push(entry.path());
|
||||
}
|
||||
}
|
||||
|
||||
if folders.len() == 1 {
|
||||
// unwrap ok, folders definitely is not empty
|
||||
let folder = folders.pop().unwrap();
|
||||
delete_file(folder, file).await
|
||||
} else {
|
||||
// Check folders with file.clone
|
||||
for folder in folders {
|
||||
if delete_file(folder, file.clone()).await {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_action_arguments(
|
||||
args: &str,
|
||||
format: FileFormat,
|
||||
) -> anyhow::Result<JsonObject> {
|
||||
match format {
|
||||
FileFormat::KeyValue => {
|
||||
let args = parse_key_value_list(args)
|
||||
.context("Failed to parse args as key value list")?
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, serde_json::Value::String(v)))
|
||||
.collect();
|
||||
Ok(args)
|
||||
}
|
||||
FileFormat::Toml => toml::from_str(args)
|
||||
.context("Failed to parse Toml to Action args"),
|
||||
FileFormat::Yaml => serde_yaml_ng::from_str(args)
|
||||
.context("Failed to parse Yaml to action args"),
|
||||
FileFormat::Json => serde_json::from_str(args)
|
||||
.context("Failed to parse Json to action args"),
|
||||
}
|
||||
}
|
||||
191
bin/core/src/api/execute/alerter.rs
Normal file
191
bin/core/src/api/execute/alerter.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use formatting::format_serror;
|
||||
use futures_util::{
|
||||
StreamExt, TryStreamExt, stream::FuturesUnordered,
|
||||
};
|
||||
use komodo_client::{
|
||||
api::execute::{SendAlert, TestAlerter},
|
||||
entities::{
|
||||
alert::{Alert, AlertData, AlertDataVariant, SeverityLevel},
|
||||
alerter::Alerter,
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCodeError;
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
use crate::{
|
||||
alert::send_alert_to_alerter, helpers::update::update_update,
|
||||
permission::get_check_permissions, resource::list_full_for_user,
|
||||
};
|
||||
|
||||
use super::ExecuteArgs;
|
||||
|
||||
impl Resolve<ExecuteArgs> for TestAlerter {
|
||||
#[instrument(
|
||||
"TestAlerter",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
alerter = self.alerter,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let alerter = get_check_permissions::<Alerter>(
|
||||
&self.alerter,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
if !alerter.config.enabled {
|
||||
update.push_error_log(
|
||||
"Test Alerter",
|
||||
String::from(
|
||||
"Alerter is disabled. Enable the Alerter to send alerts.",
|
||||
),
|
||||
);
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
return Ok(update);
|
||||
}
|
||||
|
||||
let ts = komodo_timestamp();
|
||||
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: true,
|
||||
level: SeverityLevel::Ok,
|
||||
target: update.target.clone(),
|
||||
data: AlertData::Test {
|
||||
id: alerter.id.clone(),
|
||||
name: alerter.name.clone(),
|
||||
},
|
||||
resolved_ts: Some(ts),
|
||||
};
|
||||
|
||||
if let Err(e) = send_alert_to_alerter(&alerter, &alert).await {
|
||||
update.push_error_log("Test Alerter", format_serror(&e.into()));
|
||||
} else {
|
||||
update.push_simple_log("Test Alerter", String::from("Alert sent successfully. It should be visible at your alerting destination."));
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<ExecuteArgs> for SendAlert {
|
||||
#[instrument(
|
||||
"SendAlert",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
request = format!("{self:?}"),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let alerters = list_full_for_user::<Alerter>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|a| {
|
||||
a.config.enabled
|
||||
&& (self.alerters.is_empty()
|
||||
|| self.alerters.contains(&a.name)
|
||||
|| self.alerters.contains(&a.id))
|
||||
&& (a.config.alert_types.is_empty()
|
||||
|| a.config.alert_types.contains(&AlertDataVariant::Custom))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let alerters = if user.admin {
|
||||
alerters
|
||||
} else {
|
||||
// Only keep alerters with execute permissions
|
||||
alerters
|
||||
.into_iter()
|
||||
.map(|alerter| async move {
|
||||
get_check_permissions::<Alerter>(
|
||||
&alerter.id,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect()
|
||||
};
|
||||
|
||||
if alerters.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"Could not find any valid alerters to send to, this required Execute permissions on the Alerter"
|
||||
).status_code(StatusCode::BAD_REQUEST));
|
||||
}
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
let ts = komodo_timestamp();
|
||||
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: true,
|
||||
level: self.level,
|
||||
target: update.target.clone(),
|
||||
data: AlertData::Custom {
|
||||
message: self.message,
|
||||
details: self.details,
|
||||
},
|
||||
resolved_ts: Some(ts),
|
||||
};
|
||||
|
||||
update.push_simple_log(
|
||||
"Send alert",
|
||||
serde_json::to_string_pretty(&alert)
|
||||
.context("Failed to serialize alert to JSON")?,
|
||||
);
|
||||
|
||||
if let Err(e) = alerters
|
||||
.iter()
|
||||
.map(|alerter| send_alert_to_alerter(alerter, &alert))
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
{
|
||||
update.push_error_log("Send Error", format_serror(&e.into()));
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
701
bin/core/src/api/execute/build.rs
Normal file
701
bin/core/src/api/execute/build.rs
Normal file
@@ -0,0 +1,701 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
future::IntoFuture,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::{
|
||||
by_id::update_one_by_id,
|
||||
find::find_collect,
|
||||
mongodb::{
|
||||
bson::{doc, to_bson, to_document},
|
||||
options::FindOneOptions,
|
||||
},
|
||||
};
|
||||
use formatting::format_serror;
|
||||
use futures_util::future::join_all;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::{
|
||||
execute::{
|
||||
BatchExecutionResponse, BatchRunBuild, CancelBuild, Deploy,
|
||||
RunBuild,
|
||||
},
|
||||
write::RefreshBuildCache,
|
||||
},
|
||||
entities::{
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
all_logs_success,
|
||||
build::{Build, BuildConfig},
|
||||
builder::{Builder, BuilderConfig},
|
||||
deployment::DeploymentState,
|
||||
komodo_timestamp, optional_string,
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
update::{Log, Update},
|
||||
user::auto_redeploy_user,
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
api::write::WriteArgs,
|
||||
helpers::{
|
||||
build_git_token,
|
||||
builder::{cleanup_builder_instance, connect_builder_periphery},
|
||||
channel::build_cancel_channel,
|
||||
query::{
|
||||
VariablesAndSecrets, get_deployment_state,
|
||||
get_variables_and_secrets,
|
||||
},
|
||||
registry_token,
|
||||
update::{init_execution_update, update_update},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource::{self, refresh_build_state_cache},
|
||||
state::{action_states, db_client},
|
||||
};
|
||||
|
||||
use super::{ExecuteArgs, ExecuteRequest};
|
||||
|
||||
impl super::BatchExecute for BatchRunBuild {
|
||||
type Resource = Build;
|
||||
fn single_request(build: String) -> ExecuteRequest {
|
||||
ExecuteRequest::RunBuild(RunBuild { build })
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for BatchRunBuild {
|
||||
#[instrument(
|
||||
"BatchRunBuild",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
pattern = self.pattern,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchRunBuild>(&self.pattern, user)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RunBuild {
|
||||
#[instrument(
|
||||
"RunBuild",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
build = self.build,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let mut build = get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut repo = if !build.config.files_on_host
|
||||
&& !build.config.linked_repo.is_empty()
|
||||
{
|
||||
crate::resource::get::<Repo>(&build.config.linked_repo)
|
||||
.await?
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let VariablesAndSecrets {
|
||||
mut variables,
|
||||
secrets,
|
||||
} = get_variables_and_secrets().await?;
|
||||
|
||||
// Add the $VERSION to variables. Use with [[$VERSION]]
|
||||
variables.insert(
|
||||
String::from("$VERSION"),
|
||||
build.config.version.to_string(),
|
||||
);
|
||||
|
||||
if build.config.builder_id.is_empty() {
|
||||
return Err(anyhow!("Must attach builder to RunBuild").into());
|
||||
}
|
||||
|
||||
// get the action state for the build (or insert default).
|
||||
let action_state =
|
||||
action_states().build.get_or_insert_default(&build.id).await;
|
||||
|
||||
// This will set action state back to default when dropped.
|
||||
// Will also check to ensure build not already busy before updating.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.building = true)?;
|
||||
|
||||
if build.config.auto_increment_version {
|
||||
build.config.version.increment();
|
||||
}
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
update.version = build.config.version;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let git_token =
|
||||
build_git_token(&mut build, repo.as_mut()).await?;
|
||||
|
||||
let registry_tokens =
|
||||
validate_account_extract_registry_tokens(&build).await?;
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
let cancel_clone = cancel.clone();
|
||||
let mut cancel_recv =
|
||||
build_cancel_channel().receiver.resubscribe();
|
||||
let build_id = build.id.clone();
|
||||
|
||||
let builder =
|
||||
resource::get::<Builder>(&build.config.builder_id).await?;
|
||||
|
||||
let is_server_builder =
|
||||
matches!(&builder.config, BuilderConfig::Server(_));
|
||||
|
||||
tokio::spawn(async move {
|
||||
let poll = async {
|
||||
loop {
|
||||
let (incoming_build_id, mut update) = tokio::select! {
|
||||
_ = cancel_clone.cancelled() => return Ok(()),
|
||||
id = cancel_recv.recv() => id?
|
||||
};
|
||||
if incoming_build_id == build_id {
|
||||
if is_server_builder {
|
||||
update.push_error_log("Cancel acknowledged", "Build cancellation is not possible on server builders at this time. Use an AWS builder to enable this feature.");
|
||||
} else {
|
||||
update.push_simple_log("Cancel acknowledged", "The build cancellation has been queued, it may still take some time.");
|
||||
}
|
||||
update.finalize();
|
||||
let id = update.id.clone();
|
||||
if let Err(e) = update_update(update).await {
|
||||
warn!("Failed to modify Update {id} on db | {e:#}");
|
||||
}
|
||||
if !is_server_builder {
|
||||
cancel_clone.cancel();
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
#[allow(unreachable_code)]
|
||||
anyhow::Ok(())
|
||||
};
|
||||
tokio::select! {
|
||||
_ = cancel_clone.cancelled() => {}
|
||||
_ = poll => {}
|
||||
}
|
||||
});
|
||||
|
||||
// GET BUILDER PERIPHERY
|
||||
let (periphery, cleanup_data) = match connect_builder_periphery(
|
||||
build.name.clone(),
|
||||
Some(build.config.version),
|
||||
builder,
|
||||
&mut update,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(builder) => builder,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to get Builder for Build {} | {e:#}",
|
||||
build.name
|
||||
);
|
||||
update.logs.push(Log::error(
|
||||
"Get Builder",
|
||||
format_serror(&e.context("Failed to get Builder").into()),
|
||||
));
|
||||
return handle_early_return(
|
||||
update, build.id, build.name, false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
};
|
||||
|
||||
// INTERPOLATE VARIABLES
|
||||
let secret_replacers = if !build.config.skip_secret_interp {
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolator.interpolate_build(&mut build)?;
|
||||
|
||||
if let Some(repo) = repo.as_mut() {
|
||||
interpolator.interpolate_repo(repo)?;
|
||||
}
|
||||
|
||||
interpolator.push_logs(&mut update.logs);
|
||||
|
||||
interpolator.secret_replacers
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let commit_message = if !build.config.files_on_host
|
||||
&& (!build.config.repo.is_empty()
|
||||
|| !build.config.linked_repo.is_empty())
|
||||
{
|
||||
// PULL OR CLONE REPO
|
||||
let res = tokio::select! {
|
||||
res = periphery
|
||||
.request(api::git::PullOrCloneRepo {
|
||||
args: repo.as_ref().map(Into::into).unwrap_or((&build).into()),
|
||||
git_token,
|
||||
environment: Default::default(),
|
||||
env_file_path: Default::default(),
|
||||
on_clone: None,
|
||||
on_pull: None,
|
||||
skip_secret_interp: Default::default(),
|
||||
replacers: Default::default(),
|
||||
}) => res,
|
||||
_ = cancel.cancelled() => {
|
||||
debug!("Build cancelled during clone, cleaning up builder");
|
||||
update.push_error_log("Build cancelled", String::from("user cancelled build during repo clone"));
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
info!("Builder cleaned up");
|
||||
return handle_early_return(update, build.id, build.name, true).await
|
||||
},
|
||||
};
|
||||
|
||||
let commit_message = match res {
|
||||
Ok(res) => {
|
||||
debug!("Finished repo clone");
|
||||
update.logs.extend(res.res.logs);
|
||||
update.commit_hash =
|
||||
res.res.commit_hash.unwrap_or_default().to_string();
|
||||
res.res.commit_message.unwrap_or_default()
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed build at clone repo | {e:#}");
|
||||
update.push_error_log(
|
||||
"Clone Repo",
|
||||
format_serror(&e.context("Failed to clone repo").into()),
|
||||
);
|
||||
Default::default()
|
||||
}
|
||||
};
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Some(commit_message)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if all_logs_success(&update.logs) {
|
||||
// RUN BUILD
|
||||
let res = tokio::select! {
|
||||
res = periphery
|
||||
.request(api::build::Build {
|
||||
build: build.clone(),
|
||||
repo,
|
||||
registry_tokens,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
// To push a commit hash tagged image
|
||||
commit_hash: optional_string(&update.commit_hash),
|
||||
// Unused for now
|
||||
additional_tags: Default::default(),
|
||||
}) => res.context("Failed at call to Periphery to build"),
|
||||
_ = cancel.cancelled() => {
|
||||
info!("Build cancelled during build, cleaning up builder");
|
||||
update.push_error_log("Build cancelled", String::from("User cancelled build during docker build"));
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
return handle_early_return(update, build.id, build.name, true).await
|
||||
},
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(logs) => {
|
||||
debug!("finished build");
|
||||
update.logs.extend(logs);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error in build | {e:#}");
|
||||
update.push_error_log(
|
||||
"Build Error",
|
||||
format_serror(&e.context("Failed to build").into()),
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
|
||||
let db = db_client();
|
||||
|
||||
if update.success {
|
||||
let _ = db
|
||||
.builds
|
||||
.update_one(
|
||||
doc! { "name": &build.name },
|
||||
doc! { "$set": {
|
||||
"config.version": to_bson(&build.config.version)
|
||||
.context("failed at converting version to bson")?,
|
||||
"info.last_built_at": komodo_timestamp(),
|
||||
"info.built_hash": &update.commit_hash,
|
||||
"info.built_message": commit_message
|
||||
}},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// stop the cancel listening task from going forever
|
||||
cancel.cancel();
|
||||
|
||||
// If building on temporary cloud server (AWS),
|
||||
// this will terminate the server.
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
// The Err case of to_document should be unreachable,
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db.updates,
|
||||
&update.id,
|
||||
database::mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
refresh_build_state_cache().await;
|
||||
}
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let Build { id, name, .. } = build;
|
||||
|
||||
if update.success {
|
||||
// don't hold response up for user
|
||||
tokio::spawn(async move {
|
||||
handle_post_build_redeploy(&id).await;
|
||||
});
|
||||
} else {
|
||||
let name = name.clone();
|
||||
let target = update.target.clone();
|
||||
let version = update.version;
|
||||
tokio::spawn(async move {
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
target,
|
||||
ts: komodo_timestamp(),
|
||||
resolved_ts: Some(komodo_timestamp()),
|
||||
resolved: true,
|
||||
level: SeverityLevel::Warning,
|
||||
data: AlertData::BuildFailed { id, name, version },
|
||||
};
|
||||
send_alerts(&[alert]).await
|
||||
});
|
||||
}
|
||||
|
||||
if let Err(e) = (RefreshBuildCache { build: name })
|
||||
.resolve(&WriteArgs { user: user.clone() })
|
||||
.await
|
||||
{
|
||||
update.push_error_log(
|
||||
"Refresh build cache",
|
||||
format_serror(&e.error.into()),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(update.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument("HandleEarlyReturn", skip(update))]
|
||||
async fn handle_early_return(
|
||||
mut update: Update,
|
||||
build_id: String,
|
||||
build_name: String,
|
||||
is_cancel: bool,
|
||||
) -> mogh_error::Result<Update> {
|
||||
update.finalize();
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
// The Err case of to_document should be unreachable,
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db_client().updates,
|
||||
&update.id,
|
||||
database::mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
refresh_build_state_cache().await;
|
||||
}
|
||||
update_update(update.clone()).await?;
|
||||
if !update.success && !is_cancel {
|
||||
let target = update.target.clone();
|
||||
let version = update.version;
|
||||
tokio::spawn(async move {
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
target,
|
||||
ts: komodo_timestamp(),
|
||||
resolved_ts: Some(komodo_timestamp()),
|
||||
resolved: true,
|
||||
level: SeverityLevel::Warning,
|
||||
data: AlertData::BuildFailed {
|
||||
id: build_id,
|
||||
name: build_name,
|
||||
version,
|
||||
},
|
||||
};
|
||||
send_alerts(&[alert]).await
|
||||
});
|
||||
}
|
||||
Ok(update.clone())
|
||||
}
|
||||
|
||||
pub async fn validate_cancel_build(
|
||||
request: &ExecuteRequest,
|
||||
) -> anyhow::Result<()> {
|
||||
if let ExecuteRequest::CancelBuild(req) = request {
|
||||
let build = resource::get::<Build>(&req.build).await?;
|
||||
|
||||
let db = db_client();
|
||||
|
||||
let (latest_build, latest_cancel) = tokio::try_join!(
|
||||
db.updates
|
||||
.find_one(doc! {
|
||||
"operation": "RunBuild",
|
||||
"target.id": &build.id,
|
||||
},)
|
||||
.with_options(
|
||||
FindOneOptions::builder()
|
||||
.sort(doc! { "start_ts": -1 })
|
||||
.build()
|
||||
)
|
||||
.into_future(),
|
||||
db.updates
|
||||
.find_one(doc! {
|
||||
"operation": "CancelBuild",
|
||||
"target.id": &build.id,
|
||||
},)
|
||||
.with_options(
|
||||
FindOneOptions::builder()
|
||||
.sort(doc! { "start_ts": -1 })
|
||||
.build()
|
||||
)
|
||||
.into_future()
|
||||
)?;
|
||||
|
||||
match (latest_build, latest_cancel) {
|
||||
(Some(build), Some(cancel)) => {
|
||||
if cancel.start_ts > build.start_ts {
|
||||
return Err(anyhow!("Build has already been cancelled"));
|
||||
}
|
||||
}
|
||||
(None, _) => return Err(anyhow!("No build in progress")),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for CancelBuild {
|
||||
#[instrument(
|
||||
"CancelBuild",
|
||||
skip(user, update),
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
build = self.build,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let build = get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// make sure the build is building
|
||||
if !action_states()
|
||||
.build
|
||||
.get(&build.id)
|
||||
.await
|
||||
.and_then(|s| s.get().ok().map(|s| s.building))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
return Err(anyhow!("Build is not building.").into());
|
||||
}
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
update.push_simple_log(
|
||||
"cancel triggered",
|
||||
"the build cancel has been triggered",
|
||||
);
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
build_cancel_channel()
|
||||
.sender
|
||||
.lock()
|
||||
.await
|
||||
.send((build.id, update.clone()))?;
|
||||
|
||||
// Make sure cancel is set to complete after some time in case
|
||||
// no reciever is there to do it. Prevents update stuck in InProgress.
|
||||
let update_id = update.id.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
if let Err(e) = update_one_by_id(
|
||||
&db_client().updates,
|
||||
&update_id,
|
||||
doc! { "$set": { "status": "Complete" } },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Failed to set CancelBuild Update status Complete after timeout | {e:#}"
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument("PostBuildRedeploy")]
|
||||
async fn handle_post_build_redeploy(build_id: &str) {
|
||||
let Ok(redeploy_deployments) = find_collect(
|
||||
&db_client().deployments,
|
||||
doc! {
|
||||
"config.image.params.build_id": build_id,
|
||||
"config.redeploy_on_build": true
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let futures =
|
||||
redeploy_deployments
|
||||
.into_iter()
|
||||
.map(|deployment| async move {
|
||||
let state = get_deployment_state(&deployment.id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if state == DeploymentState::Running {
|
||||
let req = super::ExecuteRequest::Deploy(Deploy {
|
||||
deployment: deployment.id.clone(),
|
||||
stop_signal: None,
|
||||
stop_time: None,
|
||||
});
|
||||
let user = auto_redeploy_user().to_owned();
|
||||
let res = async {
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
Deploy {
|
||||
deployment: deployment.id.clone(),
|
||||
stop_signal: None,
|
||||
stop_time: None,
|
||||
}
|
||||
.resolve(&ExecuteArgs {
|
||||
user,
|
||||
update,
|
||||
id: Uuid::new_v4(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
.await;
|
||||
Some((deployment.id.clone(), res))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
for res in join_all(futures).await {
|
||||
let Some((id, res)) = res else {
|
||||
continue;
|
||||
};
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"failed post build redeploy for deployment {id}: {:#}",
|
||||
e.error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This will make sure that a build with non-none image registry has an account attached,
|
||||
/// and will check the core config for a token matching requirements.
|
||||
/// Otherwise it is left to periphery.
|
||||
#[instrument("ValidateRegistryTokens")]
|
||||
async fn validate_account_extract_registry_tokens(
|
||||
Build {
|
||||
config: BuildConfig { image_registry, .. },
|
||||
..
|
||||
}: &Build,
|
||||
// Maps (domain, account) -> token
|
||||
) -> mogh_error::Result<Vec<(String, String, String)>> {
|
||||
let mut res = HashMap::with_capacity(image_registry.capacity());
|
||||
|
||||
for (domain, account) in image_registry
|
||||
.iter()
|
||||
.map(|r| (r.domain.as_str(), r.account.as_str()))
|
||||
// This ensures uniqueness / prevents redundant logins
|
||||
.collect::<HashSet<_>>()
|
||||
{
|
||||
if domain.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if account.is_empty() {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"Must attach account to use registry provider {domain}"
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let Some(registry_token) = registry_token(domain, account).await.with_context(
|
||||
|| format!("Failed to get registry token in call to db. Stopping run. | {domain} | {account}"),
|
||||
)? else {
|
||||
continue;
|
||||
};
|
||||
|
||||
res.insert(
|
||||
(domain.to_string(), account.to_string()),
|
||||
registry_token,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(
|
||||
res
|
||||
.into_iter()
|
||||
.map(|((domain, account), token)| (domain, account, token))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
961
bin/core/src/api/execute/deployment.rs
Normal file
961
bin/core/src/api/execute/deployment.rs
Normal file
@@ -0,0 +1,961 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use formatting::format_serror;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
SwarmOrServer, Version,
|
||||
build::{Build, ImageRegistryConfig},
|
||||
deployment::{
|
||||
Deployment, DeploymentImage, DeploymentInfo,
|
||||
extract_registry_domain,
|
||||
},
|
||||
komodo_timestamp, optional_string,
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
update::{Log, Update},
|
||||
},
|
||||
};
|
||||
use mogh_cache::TimeoutCache;
|
||||
use mogh_error::AddStatusCodeError;
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::{VariablesAndSecrets, get_variables_and_secrets},
|
||||
registry_token,
|
||||
swarm::swarm_request,
|
||||
update::update_update,
|
||||
},
|
||||
monitor::{update_cache_for_server, update_cache_for_swarm},
|
||||
resource::{self, setup_deployment_execution},
|
||||
state::action_states,
|
||||
};
|
||||
|
||||
use super::{ExecuteArgs, ExecuteRequest};
|
||||
|
||||
impl super::BatchExecute for BatchDeploy {
|
||||
type Resource = Deployment;
|
||||
fn single_request(deployment: String) -> ExecuteRequest {
|
||||
ExecuteRequest::Deploy(Deploy {
|
||||
deployment,
|
||||
stop_signal: None,
|
||||
stop_time: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for BatchDeploy {
|
||||
#[instrument(
|
||||
"BatchDeploy",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
pattern = self.pattern,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchDeploy>(&self.pattern, user)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for Deploy {
|
||||
#[instrument(
|
||||
"Deploy",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
deployment = self.deployment,
|
||||
stop_signal = format!("{:?}", self.stop_signal),
|
||||
stop_time = self.stop_time,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (mut deployment, swarm_or_server) =
|
||||
setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.deploying = true)?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
// Send update after setting action state, this way UI gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
// This block resolves the attached Build to an actual versioned image
|
||||
let (version, registry_token) = match &deployment.config.image {
|
||||
DeploymentImage::Build { build_id, version } => {
|
||||
let build = resource::get::<Build>(build_id).await?;
|
||||
let image_names = build.get_image_names();
|
||||
let image_name = image_names
|
||||
.first()
|
||||
.context("No image name could be created")
|
||||
.context("Failed to create image name")?;
|
||||
let version = if version.is_none() {
|
||||
build.config.version
|
||||
} else {
|
||||
*version
|
||||
};
|
||||
let version_str = version.to_string();
|
||||
// Potentially add the build image_tag postfix
|
||||
let version_str = if build.config.image_tag.is_empty() {
|
||||
version_str
|
||||
} else {
|
||||
format!("{version_str}-{}", build.config.image_tag)
|
||||
};
|
||||
// replace image with corresponding build image.
|
||||
deployment.config.image = DeploymentImage::Image {
|
||||
image: format!("{image_name}:{version_str}"),
|
||||
};
|
||||
let first_registry = build
|
||||
.config
|
||||
.image_registry
|
||||
.first()
|
||||
.unwrap_or(ImageRegistryConfig::static_default());
|
||||
if first_registry.domain.is_empty() {
|
||||
(version, None)
|
||||
} else {
|
||||
let ImageRegistryConfig {
|
||||
domain, account, ..
|
||||
} = first_registry;
|
||||
if deployment.config.image_registry_account.is_empty() {
|
||||
deployment.config.image_registry_account =
|
||||
account.to_string();
|
||||
}
|
||||
let token = if !deployment
|
||||
.config
|
||||
.image_registry_account
|
||||
.is_empty()
|
||||
{
|
||||
registry_token(domain, &deployment.config.image_registry_account).await.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {domain} | {}", deployment.config.image_registry_account),
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(version, token)
|
||||
}
|
||||
}
|
||||
DeploymentImage::Image { image } => {
|
||||
let domain = extract_registry_domain(image)?;
|
||||
let token = if !deployment
|
||||
.config
|
||||
.image_registry_account
|
||||
.is_empty()
|
||||
{
|
||||
registry_token(&domain, &deployment.config.image_registry_account).await.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {domain} | {}", deployment.config.image_registry_account),
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(Version::default(), token)
|
||||
}
|
||||
};
|
||||
|
||||
// interpolate variables / secrets, returning the sanitizing replacers to send to
|
||||
// periphery so it may sanitize the final command for safe logging (avoids exposing secret values)
|
||||
let secret_replacers = if !deployment.config.skip_secret_interp {
|
||||
let VariablesAndSecrets { variables, secrets } =
|
||||
get_variables_and_secrets().await?;
|
||||
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolator
|
||||
.interpolate_deployment(&mut deployment)?
|
||||
.push_logs(&mut update.logs);
|
||||
|
||||
interpolator.secret_replacers
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
update.version = version;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let deployment_id = deployment.id.clone();
|
||||
|
||||
match swarm_or_server {
|
||||
SwarmOrServer::None => unreachable!(),
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
api::swarm::CreateSwarmService {
|
||||
deployment,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(logs) => {
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
update.logs.extend(logs)
|
||||
}
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Create Swarm Service",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
SwarmOrServer::Server(server) => {
|
||||
match periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::RunContainer {
|
||||
deployment,
|
||||
stop_signal: self.stop_signal,
|
||||
stop_time: self.stop_time,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => {
|
||||
update_cache_for_server(&server, true).await;
|
||||
update.logs.push(log)
|
||||
}
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Deploy Container",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = resource::update_info::<Deployment>(
|
||||
&deployment_id,
|
||||
&DeploymentInfo {
|
||||
latest_image_digest: Default::default(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Failed to update deployment {} info after deploy | {e:#}",
|
||||
deployment_id
|
||||
);
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait this long after a pull to allow another pull through
|
||||
const PULL_TIMEOUT: i64 = 5_000;
|
||||
type ServerId = String;
|
||||
type Image = String;
|
||||
type PullCache = TimeoutCache<(ServerId, Image), Log>;
|
||||
|
||||
fn pull_cache() -> &'static PullCache {
|
||||
static PULL_CACHE: OnceLock<PullCache> = OnceLock::new();
|
||||
PULL_CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
"PullDeploymentInner",
|
||||
skip_all,
|
||||
fields(
|
||||
deployment = deployment.id,
|
||||
server = server.id
|
||||
)
|
||||
)]
|
||||
pub async fn pull_deployment_inner(
|
||||
deployment: Deployment,
|
||||
server: &Server,
|
||||
) -> anyhow::Result<Log> {
|
||||
let (image, account, token) = match deployment.config.image {
|
||||
DeploymentImage::Build { build_id, version } => {
|
||||
let build = resource::get::<Build>(&build_id).await?;
|
||||
let image_names = build.get_image_names();
|
||||
let image_name = image_names
|
||||
.first()
|
||||
.context("No image name could be created")
|
||||
.context("Failed to create image name")?;
|
||||
let version = if version.is_none() {
|
||||
build.config.version.to_string()
|
||||
} else {
|
||||
version.to_string()
|
||||
};
|
||||
// Potentially add the build image_tag postfix
|
||||
let version = if build.config.image_tag.is_empty() {
|
||||
version
|
||||
} else {
|
||||
format!("{version}-{}", build.config.image_tag)
|
||||
};
|
||||
// replace image with corresponding build image.
|
||||
let image = format!("{image_name}:{version}");
|
||||
let first_registry = build
|
||||
.config
|
||||
.image_registry
|
||||
.first()
|
||||
.unwrap_or(ImageRegistryConfig::static_default());
|
||||
if first_registry.domain.is_empty() {
|
||||
(image, None, None)
|
||||
} else {
|
||||
let ImageRegistryConfig {
|
||||
domain, account, ..
|
||||
} = first_registry;
|
||||
let account =
|
||||
if deployment.config.image_registry_account.is_empty() {
|
||||
account
|
||||
} else {
|
||||
&deployment.config.image_registry_account
|
||||
};
|
||||
let token = if !account.is_empty() {
|
||||
registry_token(domain, account).await.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {domain} | {account}"),
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(image, optional_string(account), token)
|
||||
}
|
||||
}
|
||||
DeploymentImage::Image { image } => {
|
||||
let domain = extract_registry_domain(&image)?;
|
||||
let token = if !deployment
|
||||
.config
|
||||
.image_registry_account
|
||||
.is_empty()
|
||||
{
|
||||
registry_token(&domain, &deployment.config.image_registry_account).await.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {domain} | {}", deployment.config.image_registry_account),
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(
|
||||
image,
|
||||
optional_string(&deployment.config.image_registry_account),
|
||||
token,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// Acquire the pull lock for this image on the server
|
||||
let lock = pull_cache()
|
||||
.get_lock((server.id.clone(), image.clone()))
|
||||
.await;
|
||||
|
||||
// Lock the path lock, prevents simultaneous pulls by
|
||||
// ensuring simultaneous pulls will wait for first to finish
|
||||
// and checking cached results.
|
||||
let mut locked = lock.lock().await;
|
||||
|
||||
// Early return from cache if lasted pulled with PULL_TIMEOUT
|
||||
if locked.last_ts + PULL_TIMEOUT > komodo_timestamp() {
|
||||
return locked.clone_res();
|
||||
}
|
||||
|
||||
let res = async {
|
||||
let log = match periphery_client(server)
|
||||
.await?
|
||||
.request(api::docker::PullImage {
|
||||
name: image,
|
||||
account,
|
||||
token,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error("Pull image", format_serror(&e.into())),
|
||||
};
|
||||
|
||||
update_cache_for_server(server, true).await;
|
||||
anyhow::Ok(log)
|
||||
}
|
||||
.await;
|
||||
|
||||
// Set the cache with results. Any other calls waiting on the lock will
|
||||
// then immediately also use this same result.
|
||||
locked.set(&res, komodo_timestamp());
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for PullDeployment {
|
||||
#[instrument(
|
||||
"PullDeployment",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
deployment = self.deployment,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("PullDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.pulling = true)?;
|
||||
|
||||
let mut update = update.clone();
|
||||
// Send update after setting action state, this way UI gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let log = pull_deployment_inner(deployment, &server).await?;
|
||||
|
||||
update.logs.push(log);
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for StartDeployment {
|
||||
#[instrument(
|
||||
"StartDeployment",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
deployment = self.deployment,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("StartDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.starting = true)?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
// Send update after setting action state, this way UI gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let log = match periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::StartContainer {
|
||||
name: deployment.name,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"start container",
|
||||
format_serror(&e.context("failed to start container").into()),
|
||||
),
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update_cache_for_server(&server, true).await;
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RestartDeployment {
|
||||
#[instrument(
|
||||
"RestartDeployment",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
deployment = self.deployment,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("RestartDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.restarting = true)?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
// Send update after setting action state, this way UI gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let log = match periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::RestartContainer {
|
||||
name: deployment.name,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"restart container",
|
||||
format_serror(
|
||||
&e.context("failed to restart container").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update_cache_for_server(&server, true).await;
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for PauseDeployment {
|
||||
#[instrument(
|
||||
"PauseDeployment",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
deployment = self.deployment,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("PauseDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.pausing = true)?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
// Send update after setting action state, this way UI gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let log = match periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::PauseContainer {
|
||||
name: deployment.name,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"pause container",
|
||||
format_serror(&e.context("failed to pause container").into()),
|
||||
),
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update_cache_for_server(&server, true).await;
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for UnpauseDeployment {
|
||||
#[instrument(
|
||||
"UnpauseDeployment",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
deployment = self.deployment,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("UnpauseDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.unpausing = true)?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
// Send update after setting action state, this way UI gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let log = match periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::UnpauseContainer {
|
||||
name: deployment.name,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"unpause container",
|
||||
format_serror(
|
||||
&e.context("failed to unpause container").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update_cache_for_server(&server, true).await;
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for StopDeployment {
|
||||
#[instrument(
|
||||
"StopDeployment",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
deployment = self.deployment,
|
||||
signal = format!("{:?}", self.signal),
|
||||
time = self.time,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("StopDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.stopping = true)?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
// Send update after setting action state, this way UI gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let log = match periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::StopContainer {
|
||||
name: deployment.name,
|
||||
signal: self
|
||||
.signal
|
||||
.unwrap_or(deployment.config.termination_signal)
|
||||
.into(),
|
||||
time: self
|
||||
.time
|
||||
.unwrap_or(deployment.config.termination_timeout)
|
||||
.into(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"stop container",
|
||||
format_serror(&e.context("failed to stop container").into()),
|
||||
),
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update_cache_for_server(&server, true).await;
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl super::BatchExecute for BatchDestroyDeployment {
|
||||
type Resource = Deployment;
|
||||
fn single_request(deployment: String) -> ExecuteRequest {
|
||||
ExecuteRequest::DestroyDeployment(DestroyDeployment {
|
||||
deployment,
|
||||
signal: None,
|
||||
time: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for BatchDestroyDeployment {
|
||||
#[instrument(
|
||||
"BatchDestroyDeployment",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
pattern = self.pattern,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchDestroyDeployment>(
|
||||
&self.pattern,
|
||||
user,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for DestroyDeployment {
|
||||
#[instrument(
|
||||
"DestroyDeployment",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
deployment = self.deployment,
|
||||
signal = format!("{:?}", self.signal),
|
||||
time = self.time,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.destroying = true)?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
// Send update after setting action state, this way UI gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::None => unreachable!(),
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
api::swarm::RemoveSwarmServices {
|
||||
services: vec![deployment.name],
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => {
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
log
|
||||
}
|
||||
Err(e) => Log::error(
|
||||
"Remove Swarm Service",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm service").into(),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
SwarmOrServer::Server(server) => {
|
||||
match periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::RemoveContainer {
|
||||
name: deployment.name,
|
||||
signal: self
|
||||
.signal
|
||||
.unwrap_or(deployment.config.termination_signal)
|
||||
.into(),
|
||||
time: self
|
||||
.time
|
||||
.unwrap_or(deployment.config.termination_timeout)
|
||||
.into(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => {
|
||||
update_cache_for_server(&server, true).await;
|
||||
log
|
||||
}
|
||||
Err(e) => Log::error(
|
||||
"Destroy Container",
|
||||
format_serror(
|
||||
&e.context("Failed to destroy container").into(),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update.finalize();
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
609
bin/core/src/api/execute/maintenance.rs
Normal file
609
bin/core/src/api/execute/maintenance.rs
Normal file
@@ -0,0 +1,609 @@
|
||||
use std::{fmt::Write as _, sync::OnceLock};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use command::run_komodo_standard_command;
|
||||
use database::{
|
||||
bson::{Document, doc},
|
||||
mungos::find::find_collect,
|
||||
};
|
||||
use formatting::{bold, format_serror};
|
||||
use futures_util::{StreamExt, stream::FuturesOrdered};
|
||||
use komodo_client::{
|
||||
api::execute::{
|
||||
BackupCoreDatabase, ClearRepoCache, GlobalAutoUpdate,
|
||||
RotateAllServerKeys, RotateCoreKeys,
|
||||
},
|
||||
entities::{
|
||||
SwarmOrServer, deployment::DeploymentState, server::ServerState,
|
||||
stack::StackState,
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCodeError;
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api;
|
||||
use reqwest::StatusCode;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
execute::ExecuteArgs,
|
||||
write::{
|
||||
check_deployment_for_update_inner, check_stack_for_update_inner,
|
||||
},
|
||||
},
|
||||
config::{core_config, core_keys},
|
||||
helpers::{
|
||||
periphery_client, query::find_swarm_or_server,
|
||||
update::update_update,
|
||||
},
|
||||
resource::rotate_server_keys,
|
||||
state::{
|
||||
db_client, deployment_status_cache, server_status_cache,
|
||||
stack_status_cache,
|
||||
},
|
||||
};
|
||||
|
||||
/// Makes sure the method can only be called once at a time
|
||||
fn clear_repo_cache_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for ClearRepoCache {
|
||||
#[instrument(
|
||||
"ClearRepoCache",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("This method is admin only.")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let _lock = clear_repo_cache_lock()
|
||||
.try_lock()
|
||||
.context("Clear already in progress...")?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
let mut contents =
|
||||
tokio::fs::read_dir(&core_config().repo_directory)
|
||||
.await
|
||||
.context("Failed to read repo cache directory")?;
|
||||
|
||||
loop {
|
||||
let path = match contents
|
||||
.next_entry()
|
||||
.await
|
||||
.context("Failed to read contents at path")
|
||||
{
|
||||
Ok(Some(contents)) => contents.path(),
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Read Directory",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if path.is_dir() {
|
||||
match tokio::fs::remove_dir_all(&path)
|
||||
.await
|
||||
.context("Failed to clear contents at path")
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Clear Directory",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Makes sure the method can only be called once at a time
|
||||
fn backup_database_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for BackupCoreDatabase {
|
||||
#[instrument(
|
||||
"BackupCoreDatabase",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("This method is admin only.")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let _lock = backup_database_lock()
|
||||
.try_lock()
|
||||
.context("Backup already in progress...")?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let res = run_komodo_standard_command(
|
||||
"Backup Core Database",
|
||||
None,
|
||||
"km database backup --yes",
|
||||
)
|
||||
.await;
|
||||
|
||||
update.logs.push(res);
|
||||
update.finalize();
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Makes sure the method can only be called once at a time
|
||||
fn global_update_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
|
||||
#[instrument(
|
||||
"GlobalAutoUpdate",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("This method is admin only.")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let _lock = global_update_lock()
|
||||
.try_lock()
|
||||
.context("Global update already in progress...")?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
// This is all done in sequence because there is no rush,
|
||||
// the pulls / deploys happen spaced out to ease the load on system.
|
||||
let servers = find_collect(&db_client().servers, None, None)
|
||||
.await
|
||||
.context("Failed to query for servers from database")?;
|
||||
let swarms = find_collect(&db_client().swarms, None, None)
|
||||
.await
|
||||
.context("Failed to query for swarms from database")?;
|
||||
|
||||
let query = doc! {
|
||||
"$or": [
|
||||
{ "config.poll_for_updates": true },
|
||||
{ "config.auto_update": true }
|
||||
]
|
||||
};
|
||||
|
||||
let stacks =
|
||||
find_collect(&db_client().stacks, query.clone(), None)
|
||||
.await
|
||||
.context("Failed to query for stacks from database")?;
|
||||
|
||||
let server_status_cache = server_status_cache();
|
||||
let stack_status_cache = stack_status_cache();
|
||||
|
||||
// Will be edited later at update.logs[0]
|
||||
update.push_simple_log("Auto Pull", String::new());
|
||||
|
||||
for stack in stacks {
|
||||
let Some(status) = stack_status_cache.get(&stack.id).await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Only pull running stacks.
|
||||
if !matches!(status.curr.state, StackState::Running) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let swarm_or_server = find_swarm_or_server(
|
||||
&stack.config.swarm_id,
|
||||
&swarms,
|
||||
&stack.config.server_id,
|
||||
&servers,
|
||||
)?;
|
||||
|
||||
if let SwarmOrServer::None = &swarm_or_server {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(server) =
|
||||
servers.iter().find(|s| s.id == stack.config.server_id)
|
||||
// This check is probably redundant along with running check
|
||||
// but shouldn't hurt
|
||||
&& server_status_cache
|
||||
.get(&server.id)
|
||||
.await
|
||||
.map(|s| matches!(s.state, ServerState::Ok))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if let Err(e) = check_stack_for_update_inner(
|
||||
stack.id,
|
||||
&swarm_or_server,
|
||||
self.skip_auto_update,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
update.push_error_log(
|
||||
&format!("Check Stack {}", stack.name),
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
} else {
|
||||
if !update.logs[0].stdout.is_empty() {
|
||||
update.logs[0].stdout.push('\n');
|
||||
}
|
||||
|
||||
update.logs[0].stdout.push_str(&format!(
|
||||
"Checked Stack {} ✅",
|
||||
bold(&stack.name)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let deployment_status_cache = deployment_status_cache();
|
||||
let deployments =
|
||||
find_collect(&db_client().deployments, query, None)
|
||||
.await
|
||||
.context("Failed to query for deployments from database")?;
|
||||
|
||||
for deployment in deployments {
|
||||
let Some(status) =
|
||||
deployment_status_cache.get(&deployment.id).await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Only pull running deployments.
|
||||
if !matches!(status.curr.state, DeploymentState::Running) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let swarm_or_server = find_swarm_or_server(
|
||||
&deployment.config.swarm_id,
|
||||
&swarms,
|
||||
&deployment.config.server_id,
|
||||
&servers,
|
||||
)?;
|
||||
|
||||
if let SwarmOrServer::None = &swarm_or_server {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = deployment.name.clone();
|
||||
|
||||
if let Err(e) = check_deployment_for_update_inner(
|
||||
deployment,
|
||||
&swarm_or_server,
|
||||
self.skip_auto_update,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
update.push_error_log(
|
||||
&format!("Check Deployment {name}"),
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
} else {
|
||||
if !update.logs[0].stdout.is_empty() {
|
||||
update.logs[0].stdout.push('\n');
|
||||
}
|
||||
update.logs[0]
|
||||
.stdout
|
||||
.push_str(&format!("Checked Deployment {} ✅", bold(name)));
|
||||
}
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Makes sure the method can only be called once at a time
|
||||
fn global_rotate_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RotateAllServerKeys {
|
||||
#[instrument(
|
||||
"RotateAllServerKeys",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("This method is admin only.")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let _lock = global_rotate_lock()
|
||||
.try_lock()
|
||||
.context("Key rotation already in progress...")?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut servers = db_client()
|
||||
.servers
|
||||
.find(Document::new())
|
||||
.await
|
||||
.context("Failed to query servers from database")?;
|
||||
|
||||
let server_status_cache = server_status_cache();
|
||||
|
||||
let mut log = String::new();
|
||||
|
||||
while let Some(server) = servers.next().await {
|
||||
let server = match server {
|
||||
Ok(server) => server,
|
||||
Err(e) => {
|
||||
warn!("Failed to parse Server | {e:#}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if !server.config.auto_rotate_keys {
|
||||
let _ = write!(
|
||||
&mut log,
|
||||
"\nSkipping {}: Key Rotation Disabled ⚙️",
|
||||
bold(&server.name)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let Some(status) = server_status_cache.get(&server.id).await
|
||||
else {
|
||||
let _ = write!(
|
||||
&mut log,
|
||||
"\nSkipping {}: No Status ⚠️",
|
||||
bold(&server.name)
|
||||
);
|
||||
continue;
|
||||
};
|
||||
match status.state {
|
||||
ServerState::Disabled => {
|
||||
let _ = write!(
|
||||
&mut log,
|
||||
"\nSkipping {}: Server Disabled ⚙️",
|
||||
bold(&server.name)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
ServerState::NotOk => {
|
||||
let _ = write!(
|
||||
&mut log,
|
||||
"\nSkipping {}: Server Not Ok ⚠️",
|
||||
bold(&server.name)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
match rotate_server_keys(&server).await {
|
||||
Ok(_) => {
|
||||
let _ = write!(
|
||||
&mut log,
|
||||
"\nRotated keys for {} ✅",
|
||||
bold(&server.name)
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Key Rotation Failure",
|
||||
format_serror(
|
||||
&e.context(format!(
|
||||
"Failed to rotate {} keys",
|
||||
bold(&server.name)
|
||||
))
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update.push_simple_log("Rotate Server Keys", log);
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RotateCoreKeys {
|
||||
#[instrument(
|
||||
"RotateCoreKeys",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
force = self.force,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("This method is admin only.")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let _lock = global_rotate_lock()
|
||||
.try_lock()
|
||||
.context("Key rotation already in progress...")?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let core_keys = core_keys();
|
||||
|
||||
if !core_keys.rotatable() {
|
||||
return Err(anyhow!("Core `private_key` must be pointing to file, for example 'file:/config/keys/core.key'").into());
|
||||
};
|
||||
|
||||
let server_status_cache = server_status_cache();
|
||||
let servers =
|
||||
find_collect(&db_client().servers, Document::new(), None)
|
||||
.await
|
||||
.context("Failed to query servers from database")?
|
||||
.into_iter()
|
||||
.map(|server| async move {
|
||||
let state = server_status_cache
|
||||
.get(&server.id)
|
||||
.await
|
||||
.map(|s| s.state)
|
||||
.unwrap_or(ServerState::NotOk);
|
||||
(server, state)
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
if !self.force
|
||||
&& let Some((server, _)) = servers
|
||||
.iter()
|
||||
.find(|(_, state)| matches!(state, ServerState::NotOk))
|
||||
{
|
||||
return Err(
|
||||
anyhow!("Server {} is NotOk, stopping key rotation. Pass `force: true` to continue anyways.", server.name).into(),
|
||||
);
|
||||
}
|
||||
|
||||
let public_key = core_keys
|
||||
.rotate(mogh_pki::PkiKind::Mutual)
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
info!("New Public Key: {public_key}");
|
||||
|
||||
let mut log = format!("New Public Key: {public_key}\n");
|
||||
|
||||
for (server, state) in servers {
|
||||
match state {
|
||||
ServerState::Disabled => {
|
||||
let _ = write!(
|
||||
&mut log,
|
||||
"\nSkipping {}: Server Disabled ⚙️",
|
||||
bold(&server.name)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
ServerState::NotOk => {
|
||||
// Shouldn't be reached unless 'force: true'
|
||||
let _ = write!(
|
||||
&mut log,
|
||||
"\nSkipping {}: Server Not Ok ⚠️",
|
||||
bold(&server.name)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let periphery = periphery_client(&server).await?;
|
||||
let res = periphery
|
||||
.request(api::keys::RotateCorePublicKey {
|
||||
public_key: public_key.clone(),
|
||||
})
|
||||
.await;
|
||||
match res {
|
||||
Ok(_) => {
|
||||
let _ = write!(
|
||||
&mut log,
|
||||
"\nRotated key for {} ✅",
|
||||
bold(&server.name)
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Key Rotation Failure",
|
||||
format_serror(
|
||||
&e.context(format!(
|
||||
"Failed to rotate for {}. The new Core public key will have to be added manually.",
|
||||
bold(&server.name)
|
||||
))
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update.push_simple_log("Rotate Core Keys", log);
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
358
bin/core/src/api/execute/mod.rs
Normal file
358
bin/core/src/api/execute/mod.rs
Normal file
@@ -0,0 +1,358 @@
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
Extension, Router, extract::Path, middleware, routing::post,
|
||||
};
|
||||
use axum_extra::{TypedHeader, headers::ContentType};
|
||||
use database::mungos::by_id::find_one_by_id;
|
||||
use formatting::format_serror;
|
||||
use futures_util::future::join_all;
|
||||
use komodo_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
Operation,
|
||||
permission::PermissionLevel,
|
||||
update::{Log, Update},
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mogh_auth_server::middleware::authenticate_request;
|
||||
use mogh_error::Json;
|
||||
use mogh_error::JsonString;
|
||||
use mogh_resolver::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use strum::{Display, EnumDiscriminants};
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
auth::KomodoAuthImpl,
|
||||
helpers::update::{init_execution_update, update_update},
|
||||
resource::{KomodoResource, list_full_for_user_using_pattern},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
mod action;
|
||||
mod alerter;
|
||||
mod build;
|
||||
mod deployment;
|
||||
mod maintenance;
|
||||
mod procedure;
|
||||
mod repo;
|
||||
mod server;
|
||||
mod stack;
|
||||
mod swarm;
|
||||
mod sync;
|
||||
|
||||
use super::Variant;
|
||||
|
||||
pub struct ExecuteArgs {
|
||||
/// The execution id.
|
||||
/// Unique for every /execute call.
|
||||
pub id: Uuid,
|
||||
pub user: User,
|
||||
pub update: Update,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants,
|
||||
)]
|
||||
#[strum_discriminants(name(ExecuteRequestVariant), derive(Display))]
|
||||
#[args(ExecuteArgs)]
|
||||
#[response(JsonString)]
|
||||
#[error(mogh_error::Error)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
pub enum ExecuteRequest {
|
||||
// ==== STACK ====
|
||||
DeployStack(DeployStack),
|
||||
BatchDeployStack(BatchDeployStack),
|
||||
DeployStackIfChanged(DeployStackIfChanged),
|
||||
BatchDeployStackIfChanged(BatchDeployStackIfChanged),
|
||||
PullStack(PullStack),
|
||||
BatchPullStack(BatchPullStack),
|
||||
StartStack(StartStack),
|
||||
RestartStack(RestartStack),
|
||||
StopStack(StopStack),
|
||||
PauseStack(PauseStack),
|
||||
UnpauseStack(UnpauseStack),
|
||||
DestroyStack(DestroyStack),
|
||||
BatchDestroyStack(BatchDestroyStack),
|
||||
RunStackService(RunStackService),
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
Deploy(Deploy),
|
||||
BatchDeploy(BatchDeploy),
|
||||
PullDeployment(PullDeployment),
|
||||
StartDeployment(StartDeployment),
|
||||
RestartDeployment(RestartDeployment),
|
||||
PauseDeployment(PauseDeployment),
|
||||
UnpauseDeployment(UnpauseDeployment),
|
||||
StopDeployment(StopDeployment),
|
||||
DestroyDeployment(DestroyDeployment),
|
||||
BatchDestroyDeployment(BatchDestroyDeployment),
|
||||
|
||||
// ==== BUILD ====
|
||||
RunBuild(RunBuild),
|
||||
BatchRunBuild(BatchRunBuild),
|
||||
CancelBuild(CancelBuild),
|
||||
|
||||
// ==== REPO ====
|
||||
CloneRepo(CloneRepo),
|
||||
BatchCloneRepo(BatchCloneRepo),
|
||||
PullRepo(PullRepo),
|
||||
BatchPullRepo(BatchPullRepo),
|
||||
BuildRepo(BuildRepo),
|
||||
BatchBuildRepo(BatchBuildRepo),
|
||||
CancelRepoBuild(CancelRepoBuild),
|
||||
|
||||
// ==== PROCEDURE ====
|
||||
RunProcedure(RunProcedure),
|
||||
BatchRunProcedure(BatchRunProcedure),
|
||||
|
||||
// ==== ACTION ====
|
||||
RunAction(RunAction),
|
||||
BatchRunAction(BatchRunAction),
|
||||
|
||||
// ==== SYNC ====
|
||||
RunSync(RunSync),
|
||||
|
||||
// ==== ALERTER ====
|
||||
TestAlerter(TestAlerter),
|
||||
SendAlert(SendAlert),
|
||||
|
||||
// ==== SERVER ====
|
||||
StartContainer(StartContainer),
|
||||
RestartContainer(RestartContainer),
|
||||
PauseContainer(PauseContainer),
|
||||
UnpauseContainer(UnpauseContainer),
|
||||
StopContainer(StopContainer),
|
||||
DestroyContainer(DestroyContainer),
|
||||
StartAllContainers(StartAllContainers),
|
||||
RestartAllContainers(RestartAllContainers),
|
||||
PauseAllContainers(PauseAllContainers),
|
||||
UnpauseAllContainers(UnpauseAllContainers),
|
||||
StopAllContainers(StopAllContainers),
|
||||
PruneContainers(PruneContainers),
|
||||
DeleteNetwork(DeleteNetwork),
|
||||
PruneNetworks(PruneNetworks),
|
||||
DeleteImage(DeleteImage),
|
||||
PruneImages(PruneImages),
|
||||
DeleteVolume(DeleteVolume),
|
||||
PruneVolumes(PruneVolumes),
|
||||
PruneDockerBuilders(PruneDockerBuilders),
|
||||
PruneBuildx(PruneBuildx),
|
||||
PruneSystem(PruneSystem),
|
||||
|
||||
// ==== SWARM ====
|
||||
RemoveSwarmNodes(RemoveSwarmNodes),
|
||||
RemoveSwarmStacks(RemoveSwarmStacks),
|
||||
RemoveSwarmServices(RemoveSwarmServices),
|
||||
CreateSwarmConfig(CreateSwarmConfig),
|
||||
RotateSwarmConfig(RotateSwarmConfig),
|
||||
RemoveSwarmConfigs(RemoveSwarmConfigs),
|
||||
CreateSwarmSecret(CreateSwarmSecret),
|
||||
RotateSwarmSecret(RotateSwarmSecret),
|
||||
RemoveSwarmSecrets(RemoveSwarmSecrets),
|
||||
|
||||
// ==== MAINTENANCE ====
|
||||
ClearRepoCache(ClearRepoCache),
|
||||
BackupCoreDatabase(BackupCoreDatabase),
|
||||
GlobalAutoUpdate(GlobalAutoUpdate),
|
||||
RotateAllServerKeys(RotateAllServerKeys),
|
||||
RotateCoreKeys(RotateCoreKeys),
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.route("/{variant}", post(variant_handler))
|
||||
.layer(middleware::from_fn(
|
||||
authenticate_request::<KomodoAuthImpl, true>,
|
||||
))
|
||||
}
|
||||
|
||||
async fn variant_handler(
|
||||
user: Extension<User>,
|
||||
Path(Variant { variant }): Path<Variant>,
|
||||
Json(params): Json<serde_json::Value>,
|
||||
) -> mogh_error::Result<(TypedHeader<ContentType>, String)> {
|
||||
let req: ExecuteRequest = serde_json::from_value(json!({
|
||||
"type": variant,
|
||||
"params": params,
|
||||
}))?;
|
||||
handler(user, Json(req)).await
|
||||
}
|
||||
|
||||
async fn handler(
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<ExecuteRequest>,
|
||||
) -> mogh_error::Result<(TypedHeader<ContentType>, String)> {
|
||||
let res = match inner_handler(request, user).await? {
|
||||
ExecutionResult::Single(update) => serde_json::to_string(&update)
|
||||
.context("Failed to serialize Update")?,
|
||||
ExecutionResult::Batch(res) => res,
|
||||
};
|
||||
Ok((TypedHeader(ContentType::json()), res))
|
||||
}
|
||||
|
||||
#[typeshare(serialized_as = "Update")]
|
||||
type BoxUpdate = Box<Update>;
|
||||
|
||||
pub enum ExecutionResult {
|
||||
Single(BoxUpdate),
|
||||
/// The batch contents will be pre serialized here
|
||||
Batch(String),
|
||||
}
|
||||
|
||||
pub fn inner_handler(
|
||||
request: ExecuteRequest,
|
||||
user: User,
|
||||
) -> Pin<
|
||||
Box<
|
||||
dyn std::future::Future<Output = anyhow::Result<ExecutionResult>>
|
||||
+ Send,
|
||||
>,
|
||||
> {
|
||||
Box::pin(async move {
|
||||
let task_id = Uuid::new_v4();
|
||||
|
||||
// Need to validate no cancel is active before any update is created.
|
||||
// This ensures no double update created if Cancel is called more than once for the same request.
|
||||
build::validate_cancel_build(&request).await?;
|
||||
repo::validate_cancel_repo_build(&request).await?;
|
||||
|
||||
let update = init_execution_update(&request, &user).await?;
|
||||
|
||||
// This will be the case for the Batch exections,
|
||||
// they don't have their own updates.
|
||||
// The batch calls also call "inner_handler" themselves,
|
||||
// and in their case will spawn tasks, so that isn't necessary
|
||||
// here either.
|
||||
if update.operation == Operation::None {
|
||||
return Ok(ExecutionResult::Batch(
|
||||
task(task_id, request, user, update).await?,
|
||||
));
|
||||
}
|
||||
|
||||
// Spawn a task for the execution which continues
|
||||
// running after this method returns.
|
||||
let handle =
|
||||
tokio::spawn(task(task_id, request, user, update.clone()));
|
||||
|
||||
// Spawns another task to monitor the first for failures,
|
||||
// and add the log to Update about it (which primary task can't do because it errored out)
|
||||
tokio::spawn({
|
||||
let update_id = update.id.clone();
|
||||
async move {
|
||||
let log = match handle.await {
|
||||
Ok(Err(e)) => {
|
||||
warn!("/execute request {task_id} task error: {e:#}",);
|
||||
Log::error("Task Error", format_serror(&e.into()))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("/execute request {task_id} spawn error: {e:?}",);
|
||||
Log::error("Spawn Error", format!("{e:#?}"))
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
let res = async {
|
||||
// Nothing to do if update was never actually created,
|
||||
// which is the case when the id is empty.
|
||||
if update_id.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut update =
|
||||
find_one_by_id(&db_client().updates, &update_id)
|
||||
.await
|
||||
.context("Failed to query to db")?
|
||||
.context("No update exists with given id")?;
|
||||
update.logs.push(log);
|
||||
update.finalize();
|
||||
update_update(update).await
|
||||
}
|
||||
.await;
|
||||
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed to update update with task error log | {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(ExecutionResult::Single(update.into()))
|
||||
})
|
||||
}
|
||||
|
||||
async fn task(
|
||||
id: Uuid,
|
||||
request: ExecuteRequest,
|
||||
user: User,
|
||||
update: Update,
|
||||
) -> anyhow::Result<String> {
|
||||
let variant: ExecuteRequestVariant = (&request).into();
|
||||
|
||||
info!(
|
||||
"EXECUTE REQUEST {id} | METHOD: {variant} | USER: {} ({})",
|
||||
user.username, user.id
|
||||
);
|
||||
|
||||
let res =
|
||||
match request.resolve(&ExecuteArgs { user, update, id }).await {
|
||||
Err(e) => Err(e.error),
|
||||
Ok(JsonString::Err(e)) => Err(
|
||||
anyhow::Error::from(e)
|
||||
.context("failed to serialize response"),
|
||||
),
|
||||
Ok(JsonString::Ok(res)) => Ok(res),
|
||||
};
|
||||
|
||||
if let Err(e) = &res {
|
||||
warn!("EXECUTE REQUEST {id} | METHOD: {variant} | ERROR: {e:#}");
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
trait BatchExecute {
|
||||
type Resource: KomodoResource;
|
||||
fn single_request(name: String) -> ExecuteRequest;
|
||||
}
|
||||
|
||||
#[instrument("BatchExecute", skip(user))]
|
||||
async fn batch_execute<E: BatchExecute>(
|
||||
pattern: &str,
|
||||
user: &User,
|
||||
) -> anyhow::Result<BatchExecutionResponse> {
|
||||
let resources = list_full_for_user_using_pattern::<E::Resource>(
|
||||
pattern,
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let futures = resources.into_iter().map(|resource| {
|
||||
let user = user.clone();
|
||||
async move {
|
||||
inner_handler(E::single_request(resource.name.clone()), user)
|
||||
.await
|
||||
.map(|r| {
|
||||
let ExecutionResult::Single(update) = r else {
|
||||
unreachable!()
|
||||
};
|
||||
update
|
||||
})
|
||||
.map_err(|e| BatchExecutionResponseItemErr {
|
||||
name: resource.name,
|
||||
error: e.into(),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
});
|
||||
Ok(join_all(futures).await)
|
||||
}
|
||||
182
bin/core/src/api/execute/procedure.rs
Normal file
182
bin/core/src/api/execute/procedure.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use std::pin::Pin;
|
||||
|
||||
use database::mungos::{
|
||||
by_id::update_one_by_id, mongodb::bson::to_document,
|
||||
};
|
||||
use formatting::{Color, bold, colored, format_serror, muted};
|
||||
use komodo_client::{
|
||||
api::execute::{
|
||||
BatchExecutionResponse, BatchRunProcedure, RunProcedure,
|
||||
},
|
||||
entities::{
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
procedure::Procedure,
|
||||
update::Update,
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
helpers::{procedure::execute_procedure, update::update_update},
|
||||
permission::get_check_permissions,
|
||||
resource::refresh_procedure_state_cache,
|
||||
state::{action_states, db_client},
|
||||
};
|
||||
|
||||
use super::{ExecuteArgs, ExecuteRequest};
|
||||
|
||||
impl super::BatchExecute for BatchRunProcedure {
|
||||
type Resource = Procedure;
|
||||
fn single_request(procedure: String) -> ExecuteRequest {
|
||||
ExecuteRequest::RunProcedure(RunProcedure { procedure })
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for BatchRunProcedure {
|
||||
#[instrument(
|
||||
"BatchRunProcedure",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, .. }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchRunProcedure>(&self.pattern, user)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RunProcedure {
|
||||
#[instrument(
|
||||
"RunProcedure",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
procedure = self.procedure,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
Ok(
|
||||
resolve_inner(self.procedure, user.clone(), update.clone())
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_inner(
|
||||
procedure: String,
|
||||
user: User,
|
||||
mut update: Update,
|
||||
) -> Pin<
|
||||
Box<
|
||||
dyn std::future::Future<Output = anyhow::Result<Update>> + Send,
|
||||
>,
|
||||
> {
|
||||
Box::pin(async move {
|
||||
let procedure = get_check_permissions::<Procedure>(
|
||||
&procedure,
|
||||
&user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Need to push the initial log, as execute_procedure
|
||||
// assumes first log is already created
|
||||
// and will panic otherwise.
|
||||
update.push_simple_log(
|
||||
"Execute procedure",
|
||||
format!(
|
||||
"{}: executing procedure '{}'",
|
||||
muted("INFO"),
|
||||
bold(&procedure.name)
|
||||
),
|
||||
);
|
||||
|
||||
// get the action state for the procedure (or insert default).
|
||||
let action_state = action_states()
|
||||
.procedure
|
||||
.get_or_insert_default(&procedure.id)
|
||||
.await;
|
||||
|
||||
// This will set action state back to default when dropped.
|
||||
// Will also check to ensure procedure not already busy before updating.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.running = true)?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let update = Mutex::new(update);
|
||||
|
||||
let res = execute_procedure(&procedure, &update).await;
|
||||
|
||||
let mut update = update.into_inner();
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
update.push_simple_log(
|
||||
"Execution ok",
|
||||
format!(
|
||||
"{}: The procedure has {} with no errors",
|
||||
muted("INFO"),
|
||||
colored("completed", Color::Green)
|
||||
),
|
||||
);
|
||||
}
|
||||
Err(e) => update
|
||||
.push_error_log("execution error", format_serror(&e.into())),
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
// The Err case of to_document should be unreachable,
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db_client().updates,
|
||||
&update.id,
|
||||
database::mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
refresh_procedure_state_cache().await;
|
||||
}
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
if !update.success && procedure.config.failure_alert {
|
||||
let target = update.target.clone();
|
||||
tokio::spawn(async move {
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
target,
|
||||
ts: komodo_timestamp(),
|
||||
resolved_ts: Some(komodo_timestamp()),
|
||||
resolved: true,
|
||||
level: SeverityLevel::Warning,
|
||||
data: AlertData::ProcedureFailed {
|
||||
id: procedure.id,
|
||||
name: procedure.name,
|
||||
},
|
||||
};
|
||||
send_alerts(&[alert]).await
|
||||
});
|
||||
}
|
||||
|
||||
Ok(update)
|
||||
})
|
||||
}
|
||||
799
bin/core/src/api/execute/repo.rs
Normal file
799
bin/core/src/api/execute/repo.rs
Normal file
@@ -0,0 +1,799 @@
|
||||
use std::{collections::HashSet, future::IntoFuture, time::Duration};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::{
|
||||
by_id::update_one_by_id,
|
||||
mongodb::{
|
||||
bson::{doc, to_document},
|
||||
options::FindOneOptions,
|
||||
},
|
||||
};
|
||||
use formatting::format_serror;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::{execute::*, write::RefreshRepoCache},
|
||||
entities::{
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
builder::{Builder, BuilderConfig},
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
update::{Log, Update},
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
api::write::WriteArgs,
|
||||
helpers::{
|
||||
builder::{cleanup_builder_instance, connect_builder_periphery},
|
||||
channel::repo_cancel_channel,
|
||||
git_token, periphery_client,
|
||||
query::{VariablesAndSecrets, get_variables_and_secrets},
|
||||
update::update_update,
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource::{self, refresh_repo_state_cache},
|
||||
state::{action_states, db_client},
|
||||
};
|
||||
|
||||
use super::{ExecuteArgs, ExecuteRequest};
|
||||
|
||||
impl super::BatchExecute for BatchCloneRepo {
|
||||
type Resource = Repo;
|
||||
fn single_request(repo: String) -> ExecuteRequest {
|
||||
ExecuteRequest::CloneRepo(CloneRepo { repo })
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for BatchCloneRepo {
|
||||
#[instrument(
|
||||
"BatchCloneRepo",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
pattern = self.pattern,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchCloneRepo>(&self.pattern, user)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for CloneRepo {
|
||||
#[instrument(
|
||||
"CloneRepo",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
repo = self.repo,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let mut repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// get the action state for the repo (or insert default).
|
||||
let action_state =
|
||||
action_states().repo.get_or_insert_default(&repo.id).await;
|
||||
|
||||
// This will set action state back to default when dropped.
|
||||
// Will also check to ensure repo not already busy before updating.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.cloning = true)?;
|
||||
|
||||
let mut update = update.clone();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
if repo.config.server_id.is_empty() {
|
||||
return Err(anyhow!("repo has no server attached").into());
|
||||
}
|
||||
|
||||
let git_token = git_token(
|
||||
&repo.config.git_provider,
|
||||
&repo.config.git_account,
|
||||
|https| repo.config.git_https = https,
|
||||
)
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. This is a database error, not a token exisitence error. Stopping run. | {} | {}", repo.config.git_provider, repo.config.git_account),
|
||||
)?;
|
||||
|
||||
let server =
|
||||
resource::get::<Server>(&repo.config.server_id).await?;
|
||||
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
// interpolate variables / secrets, returning the sanitizing replacers to send to
|
||||
// periphery so it may sanitize the final command for safe logging (avoids exposing secret values)
|
||||
let secret_replacers =
|
||||
interpolate(&mut repo, &mut update).await?;
|
||||
|
||||
let logs = match periphery
|
||||
.request(api::git::CloneRepo {
|
||||
args: (&repo).into(),
|
||||
git_token,
|
||||
environment: repo.config.env_vars()?,
|
||||
env_file_path: repo.config.env_file_path,
|
||||
on_clone: repo.config.on_clone.into(),
|
||||
on_pull: repo.config.on_pull.into(),
|
||||
skip_secret_interp: repo.config.skip_secret_interp,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(res) => res.res.logs,
|
||||
Err(e) => {
|
||||
vec![Log::error(
|
||||
"Clone Repo",
|
||||
format_serror(&e.context("Failed to clone repo").into()),
|
||||
)]
|
||||
}
|
||||
};
|
||||
|
||||
update.logs.extend(logs);
|
||||
update.finalize();
|
||||
|
||||
if update.success {
|
||||
update_last_pulled_time(&repo.name).await;
|
||||
}
|
||||
|
||||
if let Err(e) = (RefreshRepoCache { repo: repo.id })
|
||||
.resolve(&WriteArgs { user: user.clone() })
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed to refresh repo cache")
|
||||
{
|
||||
update.push_error_log(
|
||||
"Refresh Repo cache",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
};
|
||||
|
||||
handle_repo_update_return(update).await
|
||||
}
|
||||
}
|
||||
|
||||
impl super::BatchExecute for BatchPullRepo {
|
||||
type Resource = Repo;
|
||||
fn single_request(repo: String) -> ExecuteRequest {
|
||||
ExecuteRequest::PullRepo(PullRepo { repo })
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for BatchPullRepo {
|
||||
#[instrument(
|
||||
"BatchPullRepo",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
pattern = self.pattern
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchPullRepo>(&self.pattern, user)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for PullRepo {
|
||||
#[instrument(
|
||||
"PullRepo",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
repo = self.repo,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let mut repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// get the action state for the repo (or insert default).
|
||||
let action_state =
|
||||
action_states().repo.get_or_insert_default(&repo.id).await;
|
||||
|
||||
// This will set action state back to default when dropped.
|
||||
// Will also check to ensure repo not already busy before updating.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.pulling = true)?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
if repo.config.server_id.is_empty() {
|
||||
return Err(anyhow!("repo has no server attached").into());
|
||||
}
|
||||
|
||||
let git_token = git_token(
|
||||
&repo.config.git_provider,
|
||||
&repo.config.git_account,
|
||||
|https| repo.config.git_https = https,
|
||||
)
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. This is a database error, not a token exisitence error. Stopping run. | {} | {}", repo.config.git_provider, repo.config.git_account),
|
||||
)?;
|
||||
|
||||
let server =
|
||||
resource::get::<Server>(&repo.config.server_id).await?;
|
||||
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
// interpolate variables / secrets, returning the sanitizing replacers to send to
|
||||
// periphery so it may sanitize the final command for safe logging (avoids exposing secret values)
|
||||
let secret_replacers =
|
||||
interpolate(&mut repo, &mut update).await?;
|
||||
|
||||
let logs = match periphery
|
||||
.request(api::git::PullRepo {
|
||||
args: (&repo).into(),
|
||||
git_token,
|
||||
environment: repo.config.env_vars()?,
|
||||
env_file_path: repo.config.env_file_path,
|
||||
on_pull: repo.config.on_pull.into(),
|
||||
skip_secret_interp: repo.config.skip_secret_interp,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(res) => {
|
||||
update.commit_hash = res.res.commit_hash.unwrap_or_default();
|
||||
res.res.logs
|
||||
}
|
||||
Err(e) => {
|
||||
vec![Log::error(
|
||||
"pull repo",
|
||||
format_serror(&e.context("failed to pull repo").into()),
|
||||
)]
|
||||
}
|
||||
};
|
||||
|
||||
update.logs.extend(logs);
|
||||
|
||||
update.finalize();
|
||||
|
||||
if update.success {
|
||||
update_last_pulled_time(&repo.name).await;
|
||||
}
|
||||
|
||||
if let Err(e) = (RefreshRepoCache { repo: repo.id })
|
||||
.resolve(&WriteArgs { user: user.clone() })
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed to refresh repo cache")
|
||||
{
|
||||
update.push_error_log(
|
||||
"Refresh Repo cache",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
};
|
||||
|
||||
handle_repo_update_return(update).await
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
"HandleRepoEarlyReturn",
|
||||
skip_all,
|
||||
fields(update_id = update.id)
|
||||
)]
|
||||
async fn handle_repo_update_return(
|
||||
update: Update,
|
||||
) -> mogh_error::Result<Update> {
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
// The Err case of to_document should be unreachable,
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db_client().updates,
|
||||
&update.id,
|
||||
database::mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
refresh_repo_state_cache().await;
|
||||
}
|
||||
update_update(update.clone()).await?;
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
#[instrument("UpdateLastPulledTime")]
|
||||
async fn update_last_pulled_time(repo_name: &str) {
|
||||
let res = db_client()
|
||||
.repos
|
||||
.update_one(
|
||||
doc! { "name": repo_name },
|
||||
doc! { "$set": { "info.last_pulled_at": komodo_timestamp() } },
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"failed to update repo last_pulled_at | repo: {repo_name} | {e:#}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl super::BatchExecute for BatchBuildRepo {
|
||||
type Resource = Repo;
|
||||
fn single_request(repo: String) -> ExecuteRequest {
|
||||
ExecuteRequest::CloneRepo(CloneRepo { repo })
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for BatchBuildRepo {
|
||||
#[instrument(
|
||||
"BatchBuildRepo",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
pattern = self.pattern,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchBuildRepo>(&self.pattern, user)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for BuildRepo {
|
||||
#[instrument(
|
||||
"BuildRepo",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
repo = self.repo,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let mut repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if repo.config.builder_id.is_empty() {
|
||||
return Err(anyhow!("Must attach builder to BuildRepo").into());
|
||||
}
|
||||
|
||||
// get the action state for the repo (or insert default).
|
||||
let action_state =
|
||||
action_states().repo.get_or_insert_default(&repo.id).await;
|
||||
|
||||
// This will set action state back to default when dropped.
|
||||
// Will also check to ensure repo not already busy before updating.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.building = true)?;
|
||||
|
||||
let mut update = update.clone();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let git_token = git_token(
|
||||
&repo.config.git_provider,
|
||||
&repo.config.git_account,
|
||||
|https| repo.config.git_https = https,
|
||||
)
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. This is a database error, not a token exisitence error. Stopping run. | {} | {}", repo.config.git_provider, repo.config.git_account),
|
||||
)?;
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
let cancel_clone = cancel.clone();
|
||||
let mut cancel_recv =
|
||||
repo_cancel_channel().receiver.resubscribe();
|
||||
let repo_id = repo.id.clone();
|
||||
|
||||
let builder =
|
||||
resource::get::<Builder>(&repo.config.builder_id).await?;
|
||||
|
||||
let is_server_builder =
|
||||
matches!(&builder.config, BuilderConfig::Server(_));
|
||||
|
||||
tokio::spawn(async move {
|
||||
let poll = async {
|
||||
loop {
|
||||
let (incoming_repo_id, mut update) = tokio::select! {
|
||||
_ = cancel_clone.cancelled() => return Ok(()),
|
||||
id = cancel_recv.recv() => id?
|
||||
};
|
||||
if incoming_repo_id == repo_id {
|
||||
if is_server_builder {
|
||||
update.push_error_log("Cancel acknowledged", "Repo Build cancellation is not possible on server builders at this time. Use an AWS builder to enable this feature.");
|
||||
} else {
|
||||
update.push_simple_log("Cancel acknowledged", "The repo build cancellation has been queued, it may still take some time.");
|
||||
}
|
||||
update.finalize();
|
||||
let id = update.id.clone();
|
||||
if let Err(e) = update_update(update).await {
|
||||
warn!("failed to modify Update {id} on db | {e:#}");
|
||||
}
|
||||
if !is_server_builder {
|
||||
cancel_clone.cancel();
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
#[allow(unreachable_code)]
|
||||
anyhow::Ok(())
|
||||
};
|
||||
tokio::select! {
|
||||
_ = cancel_clone.cancelled() => {}
|
||||
_ = poll => {}
|
||||
}
|
||||
});
|
||||
|
||||
// GET BUILDER PERIPHERY
|
||||
|
||||
let (periphery, cleanup_data) = match connect_builder_periphery(
|
||||
repo.name.clone(),
|
||||
None,
|
||||
builder,
|
||||
&mut update,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(builder) => builder,
|
||||
Err(e) => {
|
||||
warn!("failed to get builder for repo {} | {e:#}", repo.name);
|
||||
update.logs.push(Log::error(
|
||||
"get builder",
|
||||
format_serror(&e.context("failed to get builder").into()),
|
||||
));
|
||||
return handle_builder_early_return(
|
||||
update, repo.id, repo.name, false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
};
|
||||
|
||||
// CLONE REPO
|
||||
|
||||
// interpolate variables / secrets, returning the sanitizing replacers to send to
|
||||
// periphery so it may sanitize the final command for safe logging (avoids exposing secret values)
|
||||
let secret_replacers =
|
||||
interpolate(&mut repo, &mut update).await?;
|
||||
|
||||
let res = tokio::select! {
|
||||
res = periphery
|
||||
.request(api::git::CloneRepo {
|
||||
args: (&repo).into(),
|
||||
git_token,
|
||||
environment: repo.config.env_vars()?,
|
||||
env_file_path: repo.config.env_file_path,
|
||||
on_clone: repo.config.on_clone.into(),
|
||||
on_pull: repo.config.on_pull.into(),
|
||||
skip_secret_interp: repo.config.skip_secret_interp,
|
||||
replacers: secret_replacers.into_iter().collect()
|
||||
}) => res,
|
||||
_ = cancel.cancelled() => {
|
||||
debug!("build cancelled during clone, cleaning up builder");
|
||||
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
info!("builder cleaned up");
|
||||
return handle_builder_early_return(update, repo.id, repo.name, true).await
|
||||
},
|
||||
};
|
||||
|
||||
let commit_message = match res {
|
||||
Ok(res) => {
|
||||
debug!("finished repo clone");
|
||||
update.logs.extend(res.res.logs);
|
||||
update.commit_hash = res.res.commit_hash.unwrap_or_default();
|
||||
|
||||
res.res.commit_message.unwrap_or_default()
|
||||
}
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Clone Repo",
|
||||
format_serror(&e.context("Failed to clone repo").into()),
|
||||
);
|
||||
Default::default()
|
||||
}
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
|
||||
let db = db_client();
|
||||
|
||||
if update.success {
|
||||
let _ = db
|
||||
.repos
|
||||
.update_one(
|
||||
doc! { "name": &repo.name },
|
||||
doc! { "$set": {
|
||||
"info.last_built_at": komodo_timestamp(),
|
||||
"info.built_hash": &update.commit_hash,
|
||||
"info.built_message": commit_message
|
||||
}},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// stop the cancel listening task from going forever
|
||||
cancel.cancel();
|
||||
|
||||
// If building on temporary cloud server (AWS),
|
||||
// this will terminate the server.
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
// The Err case of to_document should be unreachable,
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db.updates,
|
||||
&update.id,
|
||||
database::mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
refresh_repo_state_cache().await;
|
||||
}
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
if !update.success {
|
||||
let target = update.target.clone();
|
||||
tokio::spawn(async move {
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
target,
|
||||
ts: komodo_timestamp(),
|
||||
resolved_ts: Some(komodo_timestamp()),
|
||||
resolved: true,
|
||||
level: SeverityLevel::Warning,
|
||||
data: AlertData::RepoBuildFailed {
|
||||
id: repo.id,
|
||||
name: repo.name,
|
||||
},
|
||||
};
|
||||
send_alerts(&[alert]).await
|
||||
});
|
||||
}
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument("HandleRepoBuildEarlyReturn", skip(update))]
|
||||
async fn handle_builder_early_return(
|
||||
mut update: Update,
|
||||
repo_id: String,
|
||||
repo_name: String,
|
||||
is_cancel: bool,
|
||||
) -> mogh_error::Result<Update> {
|
||||
update.finalize();
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
// The Err case of to_document should be unreachable,
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db_client().updates,
|
||||
&update.id,
|
||||
database::mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
refresh_repo_state_cache().await;
|
||||
}
|
||||
update_update(update.clone()).await?;
|
||||
if !update.success && !is_cancel {
|
||||
let target = update.target.clone();
|
||||
tokio::spawn(async move {
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
target,
|
||||
ts: komodo_timestamp(),
|
||||
resolved_ts: Some(komodo_timestamp()),
|
||||
resolved: true,
|
||||
level: SeverityLevel::Warning,
|
||||
data: AlertData::RepoBuildFailed {
|
||||
id: repo_id,
|
||||
name: repo_name,
|
||||
},
|
||||
};
|
||||
send_alerts(&[alert]).await
|
||||
});
|
||||
}
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
pub async fn validate_cancel_repo_build(
|
||||
request: &ExecuteRequest,
|
||||
) -> anyhow::Result<()> {
|
||||
if let ExecuteRequest::CancelRepoBuild(req) = request {
|
||||
let repo = resource::get::<Repo>(&req.repo).await?;
|
||||
|
||||
let db = db_client();
|
||||
|
||||
let (latest_build, latest_cancel) = tokio::try_join!(
|
||||
db.updates
|
||||
.find_one(doc! {
|
||||
"operation": "BuildRepo",
|
||||
"target.id": &repo.id,
|
||||
},)
|
||||
.with_options(
|
||||
FindOneOptions::builder()
|
||||
.sort(doc! { "start_ts": -1 })
|
||||
.build()
|
||||
)
|
||||
.into_future(),
|
||||
db.updates
|
||||
.find_one(doc! {
|
||||
"operation": "CancelRepoBuild",
|
||||
"target.id": &repo.id,
|
||||
},)
|
||||
.with_options(
|
||||
FindOneOptions::builder()
|
||||
.sort(doc! { "start_ts": -1 })
|
||||
.build()
|
||||
)
|
||||
.into_future()
|
||||
)?;
|
||||
|
||||
match (latest_build, latest_cancel) {
|
||||
(Some(build), Some(cancel)) => {
|
||||
if cancel.start_ts > build.start_ts {
|
||||
return Err(anyhow!(
|
||||
"Repo build has already been cancelled"
|
||||
));
|
||||
}
|
||||
}
|
||||
(None, _) => return Err(anyhow!("No repo build in progress")),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for CancelRepoBuild {
|
||||
#[instrument(
|
||||
"CancelRepoBuild",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
repo = self.repo,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// make sure the build is building
|
||||
if !action_states()
|
||||
.repo
|
||||
.get(&repo.id)
|
||||
.await
|
||||
.and_then(|s| s.get().ok().map(|s| s.building))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
return Err(anyhow!("Repo is not building.").into());
|
||||
}
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
update.push_simple_log(
|
||||
"cancel triggered",
|
||||
"the repo build cancel has been triggered",
|
||||
);
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
repo_cancel_channel()
|
||||
.sender
|
||||
.lock()
|
||||
.await
|
||||
.send((repo.id, update.clone()))?;
|
||||
|
||||
// Make sure cancel is set to complete after some time in case
|
||||
// no reciever is there to do it. Prevents update stuck in InProgress.
|
||||
let update_id = update.id.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
if let Err(e) = update_one_by_id(
|
||||
&db_client().updates,
|
||||
&update_id,
|
||||
doc! { "$set": { "status": "Complete" } },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"failed to set CancelRepoBuild Update status Complete after timeout | {e:#}"
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
"Interpolate",
|
||||
skip_all,
|
||||
fields(
|
||||
skip_secret_interp = repo.config.skip_secret_interp
|
||||
)
|
||||
)]
|
||||
async fn interpolate(
|
||||
repo: &mut Repo,
|
||||
update: &mut Update,
|
||||
) -> anyhow::Result<HashSet<(String, String)>> {
|
||||
if !repo.config.skip_secret_interp {
|
||||
let VariablesAndSecrets { variables, secrets } =
|
||||
get_variables_and_secrets().await?;
|
||||
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolator
|
||||
.interpolate_repo(repo)?
|
||||
.push_logs(&mut update.logs);
|
||||
|
||||
Ok(interpolator.secret_replacers)
|
||||
} else {
|
||||
Ok(Default::default())
|
||||
}
|
||||
}
|
||||
1342
bin/core/src/api/execute/server.rs
Normal file
1342
bin/core/src/api/execute/server.rs
Normal file
File diff suppressed because it is too large
Load Diff
1341
bin/core/src/api/execute/stack.rs
Normal file
1341
bin/core/src/api/execute/stack.rs
Normal file
File diff suppressed because it is too large
Load Diff
516
bin/core/src/api/execute/swarm.rs
Normal file
516
bin/core/src/api/execute/swarm.rs
Normal file
@@ -0,0 +1,516 @@
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::execute::{
|
||||
CreateSwarmConfig, CreateSwarmSecret, RemoveSwarmConfigs,
|
||||
RemoveSwarmNodes, RemoveSwarmSecrets, RemoveSwarmServices,
|
||||
RemoveSwarmStacks, RotateSwarmConfig, RotateSwarmSecret,
|
||||
},
|
||||
entities::{permission::PermissionLevel, swarm::Swarm},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
api::execute::ExecuteArgs,
|
||||
helpers::{swarm::swarm_request, update::update_update},
|
||||
monitor::update_cache_for_swarm,
|
||||
permission::get_check_permissions,
|
||||
};
|
||||
|
||||
impl Resolve<ExecuteArgs> for RemoveSwarmNodes {
|
||||
#[instrument(
|
||||
"RemoveSwarmNodes",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
nodes = serde_json::to_string(&self.nodes).unwrap_or_else(|e| e.to_string()),
|
||||
force = self.force,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmNodes {
|
||||
nodes: self.nodes,
|
||||
force: self.force,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => {
|
||||
update.logs.push(log);
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
}
|
||||
Err(e) => update.push_error_log(
|
||||
"Remove Swarm Nodes",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm nodes").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RemoveSwarmStacks {
|
||||
#[instrument(
|
||||
"RemoveSwarmStacks",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
stacks = serde_json::to_string(&self.stacks).unwrap_or_else(|e| e.to_string()),
|
||||
detach = self.detach,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmStacks {
|
||||
stacks: self.stacks,
|
||||
detach: self.detach,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => {
|
||||
update.logs.push(log);
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
}
|
||||
Err(e) => update.push_error_log(
|
||||
"Remove Swarm Stacks",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm stacks").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RemoveSwarmServices {
|
||||
#[instrument(
|
||||
"RemoveSwarmServices",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
services = serde_json::to_string(&self.services).unwrap_or_else(|e| e.to_string()),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmServices {
|
||||
services: self.services,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => {
|
||||
update.logs.push(log);
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
}
|
||||
Err(e) => update.push_error_log(
|
||||
"Remove Swarm Services",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm services").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for CreateSwarmConfig {
|
||||
#[instrument(
|
||||
"CreateSwarmConfig",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
config = self.name,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::CreateSwarmConfig {
|
||||
name: self.name,
|
||||
data: self.data,
|
||||
labels: self.labels,
|
||||
template_driver: self.template_driver,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => {
|
||||
update.logs.push(log);
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
}
|
||||
Err(e) => update.push_error_log(
|
||||
"Create Swarm Config",
|
||||
format_serror(
|
||||
&e.context("Failed to create swarm config").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RotateSwarmConfig {
|
||||
#[instrument(
|
||||
"RotateSwarmConfig",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
config = self.config,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RotateSwarmConfig {
|
||||
config: self.config,
|
||||
data: self.data,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(logs) => {
|
||||
update.logs.extend(logs);
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
}
|
||||
Err(e) => update.push_error_log(
|
||||
"Rotate Swarm Config",
|
||||
format_serror(
|
||||
&e.context("Failed to rotate swarm config").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RemoveSwarmConfigs {
|
||||
#[instrument(
|
||||
"RemoveSwarmConfigs",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
configs = serde_json::to_string(&self.configs).unwrap_or_else(|e| e.to_string()),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmConfigs {
|
||||
configs: self.configs,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => {
|
||||
update.logs.push(log);
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
}
|
||||
Err(e) => update.push_error_log(
|
||||
"Remove Swarm Configs",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm configs").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for CreateSwarmSecret {
|
||||
#[instrument(
|
||||
"CreateSwarmSecret",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
secret = self.name,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::CreateSwarmSecret {
|
||||
name: self.name,
|
||||
data: self.data,
|
||||
driver: self.driver,
|
||||
labels: self.labels,
|
||||
template_driver: self.template_driver,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => {
|
||||
update.logs.push(log);
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
}
|
||||
Err(e) => update.push_error_log(
|
||||
"Create Swarm Secret",
|
||||
format_serror(
|
||||
&e.context("Failed to create swarm secret").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RotateSwarmSecret {
|
||||
#[instrument(
|
||||
"RotateSwarmSecret",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
secret = self.secret,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RotateSwarmSecret {
|
||||
secret: self.secret,
|
||||
data: self.data,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(logs) => {
|
||||
update.logs.extend(logs);
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
}
|
||||
Err(e) => update.push_error_log(
|
||||
"Rotate Swarm Secret",
|
||||
format_serror(
|
||||
&e.context("Failed to rotate swarm secret").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RemoveSwarmSecrets {
|
||||
#[instrument(
|
||||
"RemoveSwarmSecrets",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
secrets = serde_json::to_string(&self.secrets).unwrap_or_else(|e| e.to_string()),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmSecrets {
|
||||
secrets: self.secrets,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => {
|
||||
update.logs.push(log);
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
}
|
||||
Err(e) => update.push_error_log(
|
||||
"Remove Swarm Secrets",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm secrets").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
555
bin/core/src/api/execute/sync.rs
Normal file
555
bin/core/src/api/execute/sync.rs
Normal file
@@ -0,0 +1,555 @@
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::{
|
||||
by_id::update_one_by_id,
|
||||
mongodb::bson::{doc, oid::ObjectId},
|
||||
};
|
||||
use formatting::{Color, colored, format_serror};
|
||||
use komodo_client::{
|
||||
api::{execute::RunSync, write::RefreshResourceSyncPending},
|
||||
entities::{
|
||||
self, ResourceTargetVariant,
|
||||
action::Action,
|
||||
alerter::Alerter,
|
||||
build::Build,
|
||||
builder::Builder,
|
||||
deployment::Deployment,
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
stack::Stack,
|
||||
sync::ResourceSync,
|
||||
update::{Log, Update},
|
||||
user::sync_user,
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
api::write::WriteArgs,
|
||||
helpers::{
|
||||
all_resources::AllResourcesById, query::get_id_to_tags,
|
||||
update::update_update,
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
state::{action_states, db_client},
|
||||
sync::{
|
||||
ResourceSyncTrait,
|
||||
deploy::{
|
||||
SyncDeployParams, build_deploy_cache, deploy_from_cache,
|
||||
},
|
||||
execute::{ExecuteResourceSync, get_updates_for_execution},
|
||||
remote::RemoteResources,
|
||||
},
|
||||
};
|
||||
|
||||
use super::ExecuteArgs;
|
||||
|
||||
impl Resolve<ExecuteArgs> for RunSync {
|
||||
#[instrument(
|
||||
"RunSync",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
sync = self.sync,
|
||||
resource_type = format!("{:?}", self.resource_type),
|
||||
resources = format!("{:?}", self.resources),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let RunSync {
|
||||
sync,
|
||||
resource_type: match_resource_type,
|
||||
resources: match_resources,
|
||||
} = self;
|
||||
let sync = get_check_permissions::<entities::sync::ResourceSync>(
|
||||
&sync,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let repo = if !sync.config.files_on_host
|
||||
&& !sync.config.linked_repo.is_empty()
|
||||
{
|
||||
crate::resource::get::<Repo>(&sync.config.linked_repo)
|
||||
.await?
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// get the action state for the sync (or insert default).
|
||||
let action_state =
|
||||
action_states().sync.get_or_insert_default(&sync.id).await;
|
||||
|
||||
// This will set action state back to default when dropped.
|
||||
// Will also check to ensure sync not already busy before updating.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.syncing = true)?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
// Send update here for FE to recheck action state
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let RemoteResources {
|
||||
resources,
|
||||
logs,
|
||||
hash,
|
||||
message,
|
||||
file_errors,
|
||||
..
|
||||
} =
|
||||
crate::sync::remote::get_remote_resources(&sync, repo.as_ref())
|
||||
.await
|
||||
.context("failed to get remote resources")?;
|
||||
|
||||
update.logs.extend(logs);
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
if !file_errors.is_empty() {
|
||||
return Err(
|
||||
anyhow!("Found file errors. Cannot execute sync.").into(),
|
||||
);
|
||||
}
|
||||
|
||||
let resources = resources?;
|
||||
|
||||
let id_to_tags = get_id_to_tags(None).await?;
|
||||
let all_resources = AllResourcesById::load().await?;
|
||||
// Convert all match_resources to names
|
||||
let match_resources = match_resources.map(|resources| {
|
||||
resources
|
||||
.into_iter()
|
||||
.filter_map(|name_or_id| {
|
||||
let Some(resource_type) = match_resource_type else {
|
||||
return Some(name_or_id);
|
||||
};
|
||||
match ObjectId::from_str(&name_or_id) {
|
||||
Ok(_) => match resource_type {
|
||||
ResourceTargetVariant::Swarm => all_resources
|
||||
.swarms
|
||||
.get(&name_or_id)
|
||||
.map(|s| s.name.clone()),
|
||||
ResourceTargetVariant::Server => all_resources
|
||||
.servers
|
||||
.get(&name_or_id)
|
||||
.map(|s| s.name.clone()),
|
||||
ResourceTargetVariant::Stack => all_resources
|
||||
.stacks
|
||||
.get(&name_or_id)
|
||||
.map(|s| s.name.clone()),
|
||||
ResourceTargetVariant::Deployment => all_resources
|
||||
.deployments
|
||||
.get(&name_or_id)
|
||||
.map(|d| d.name.clone()),
|
||||
ResourceTargetVariant::Build => all_resources
|
||||
.builds
|
||||
.get(&name_or_id)
|
||||
.map(|b| b.name.clone()),
|
||||
ResourceTargetVariant::Repo => all_resources
|
||||
.repos
|
||||
.get(&name_or_id)
|
||||
.map(|r| r.name.clone()),
|
||||
ResourceTargetVariant::Procedure => all_resources
|
||||
.procedures
|
||||
.get(&name_or_id)
|
||||
.map(|p| p.name.clone()),
|
||||
ResourceTargetVariant::Action => all_resources
|
||||
.actions
|
||||
.get(&name_or_id)
|
||||
.map(|p| p.name.clone()),
|
||||
ResourceTargetVariant::ResourceSync => all_resources
|
||||
.syncs
|
||||
.get(&name_or_id)
|
||||
.map(|s| s.name.clone()),
|
||||
ResourceTargetVariant::Builder => all_resources
|
||||
.builders
|
||||
.get(&name_or_id)
|
||||
.map(|b| b.name.clone()),
|
||||
ResourceTargetVariant::Alerter => all_resources
|
||||
.alerters
|
||||
.get(&name_or_id)
|
||||
.map(|a| a.name.clone()),
|
||||
ResourceTargetVariant::System => None,
|
||||
},
|
||||
Err(_) => Some(name_or_id),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let deployments_by_name = all_resources
|
||||
.deployments
|
||||
.values()
|
||||
.filter(|deployment| {
|
||||
Deployment::include_resource(
|
||||
&deployment.name,
|
||||
&deployment.config,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&deployment.tags,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
})
|
||||
.map(|deployment| (deployment.name.clone(), deployment.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let stacks_by_name = all_resources
|
||||
.stacks
|
||||
.values()
|
||||
.filter(|stack| {
|
||||
Stack::include_resource(
|
||||
&stack.name,
|
||||
&stack.config,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&stack.tags,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
})
|
||||
.map(|stack| (stack.name.clone(), stack.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let deploy_cache = build_deploy_cache(SyncDeployParams {
|
||||
deployments: &resources.deployments,
|
||||
deployment_map: &deployments_by_name,
|
||||
stacks: &resources.stacks,
|
||||
stack_map: &stacks_by_name,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let delete = sync.config.managed || sync.config.delete;
|
||||
|
||||
let server_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Server>(
|
||||
resources.servers,
|
||||
delete,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let stack_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Stack>(
|
||||
resources.stacks,
|
||||
delete,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let deployment_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Deployment>(
|
||||
resources.deployments,
|
||||
delete,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let build_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Build>(
|
||||
resources.builds,
|
||||
delete,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let repo_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Repo>(
|
||||
resources.repos,
|
||||
delete,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let procedure_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Procedure>(
|
||||
resources.procedures,
|
||||
delete,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let action_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Action>(
|
||||
resources.actions,
|
||||
delete,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let builder_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Builder>(
|
||||
resources.builders,
|
||||
delete,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let alerter_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Alerter>(
|
||||
resources.alerters,
|
||||
delete,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let resource_sync_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<entities::sync::ResourceSync>(
|
||||
resources.resource_syncs,
|
||||
delete,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let (
|
||||
variables_to_create,
|
||||
variables_to_update,
|
||||
variables_to_delete,
|
||||
) = if match_resource_type.is_none()
|
||||
&& match_resources.is_none()
|
||||
&& sync.config.include_variables
|
||||
{
|
||||
crate::sync::variables::get_updates_for_execution(
|
||||
resources.variables,
|
||||
delete,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let (
|
||||
user_groups_to_create,
|
||||
user_groups_to_update,
|
||||
user_groups_to_delete,
|
||||
) = if match_resource_type.is_none()
|
||||
&& match_resources.is_none()
|
||||
&& sync.config.include_user_groups
|
||||
{
|
||||
crate::sync::user_groups::get_updates_for_execution(
|
||||
resources.user_groups,
|
||||
delete,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
if deploy_cache.is_empty()
|
||||
&& resource_sync_deltas.no_changes()
|
||||
&& server_deltas.no_changes()
|
||||
&& deployment_deltas.no_changes()
|
||||
&& stack_deltas.no_changes()
|
||||
&& build_deltas.no_changes()
|
||||
&& builder_deltas.no_changes()
|
||||
&& alerter_deltas.no_changes()
|
||||
&& repo_deltas.no_changes()
|
||||
&& procedure_deltas.no_changes()
|
||||
&& action_deltas.no_changes()
|
||||
&& user_groups_to_create.is_empty()
|
||||
&& user_groups_to_update.is_empty()
|
||||
&& user_groups_to_delete.is_empty()
|
||||
&& variables_to_create.is_empty()
|
||||
&& variables_to_update.is_empty()
|
||||
&& variables_to_delete.is_empty()
|
||||
{
|
||||
update.push_simple_log(
|
||||
"No Changes",
|
||||
format!(
|
||||
"{}. exiting.",
|
||||
colored("nothing to do", Color::Green)
|
||||
),
|
||||
);
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
return Ok(update);
|
||||
}
|
||||
|
||||
// =================
|
||||
|
||||
// No deps
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
crate::sync::variables::run_updates(
|
||||
variables_to_create,
|
||||
variables_to_update,
|
||||
variables_to_delete,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
crate::sync::user_groups::run_updates(
|
||||
user_groups_to_create,
|
||||
user_groups_to_update,
|
||||
user_groups_to_delete,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
ResourceSync::execute_sync_updates(resource_sync_deltas).await,
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Server::execute_sync_updates(server_deltas).await,
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Alerter::execute_sync_updates(alerter_deltas).await,
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Action::execute_sync_updates(action_deltas).await,
|
||||
);
|
||||
|
||||
// Dependent on server
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Builder::execute_sync_updates(builder_deltas).await,
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Repo::execute_sync_updates(repo_deltas).await,
|
||||
);
|
||||
|
||||
// Dependant on builder
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Build::execute_sync_updates(build_deltas).await,
|
||||
);
|
||||
|
||||
// Dependant on server / build
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Deployment::execute_sync_updates(deployment_deltas).await,
|
||||
);
|
||||
// stack only depends on server, but maybe will depend on build later.
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Stack::execute_sync_updates(stack_deltas).await,
|
||||
);
|
||||
|
||||
// Dependant on everything
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Procedure::execute_sync_updates(procedure_deltas).await,
|
||||
);
|
||||
|
||||
// Execute the deploy cache
|
||||
deploy_from_cache(deploy_cache, &mut update.logs).await;
|
||||
|
||||
let db = db_client();
|
||||
|
||||
if let Err(e) = update_one_by_id(
|
||||
&db.resource_syncs,
|
||||
&sync.id,
|
||||
doc! {
|
||||
"$set": {
|
||||
"info.last_sync_ts": komodo_timestamp(),
|
||||
"info.last_sync_hash": hash,
|
||||
"info.last_sync_message": message,
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"failed to update resource sync {} info after sync | {e:#}",
|
||||
sync.name
|
||||
)
|
||||
}
|
||||
|
||||
if let Err(e) = (RefreshResourceSyncPending { sync: sync.id })
|
||||
.resolve(&WriteArgs {
|
||||
user: sync_user().to_owned(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"failed to refresh sync {} after run | {:#}",
|
||||
sync.name, e.error
|
||||
);
|
||||
update.push_error_log(
|
||||
"refresh sync",
|
||||
format_serror(
|
||||
&e.error
|
||||
.context("failed to refresh sync pending after run")
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_extend(logs: &mut Vec<Log>, log: Option<Log>) {
|
||||
if let Some(log) = log {
|
||||
logs.push(log);
|
||||
}
|
||||
}
|
||||
63
bin/core/src/api/listener/integrations/github.rs
Normal file
63
bin/core/src/api/listener/integrations/github.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::http::HeaderMap;
|
||||
use hex::ToHex;
|
||||
use hmac::{Hmac, Mac};
|
||||
use serde::Deserialize;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::config::core_config;
|
||||
|
||||
use super::{ExtractBranch, VerifySecret};
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
/// Listener implementation for Github type API, including Gitea
|
||||
pub struct Github;
|
||||
|
||||
impl VerifySecret for Github {
|
||||
#[instrument("VerifyGithubSecret", skip_all)]
|
||||
fn verify_secret(
|
||||
headers: &HeaderMap,
|
||||
body: &str,
|
||||
custom_secret: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let signature = headers
|
||||
.get("x-hub-signature-256")
|
||||
.context("No github signature in headers")?;
|
||||
let signature = signature
|
||||
.to_str()
|
||||
.context("Failed to get signature as string")?;
|
||||
let signature =
|
||||
signature.strip_prefix("sha256=").unwrap_or(signature);
|
||||
let secret_bytes = if custom_secret.is_empty() {
|
||||
core_config().webhook_secret.as_bytes()
|
||||
} else {
|
||||
custom_secret.as_bytes()
|
||||
};
|
||||
let mut mac = HmacSha256::new_from_slice(secret_bytes)
|
||||
.context("Failed to create hmac sha256 from secret")?;
|
||||
mac.update(body.as_bytes());
|
||||
let expected = mac.finalize().into_bytes().encode_hex::<String>();
|
||||
if signature == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("Signature does not equal expected"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GithubWebhookBody {
|
||||
#[serde(rename = "ref")]
|
||||
branch: String,
|
||||
}
|
||||
|
||||
impl ExtractBranch for Github {
|
||||
fn extract_branch(body: &str) -> anyhow::Result<String> {
|
||||
let branch = serde_json::from_str::<GithubWebhookBody>(body)
|
||||
.context("Failed to parse github request body")?
|
||||
.branch
|
||||
.replace("refs/heads/", "");
|
||||
Ok(branch)
|
||||
}
|
||||
}
|
||||
51
bin/core/src/api/listener/integrations/gitlab.rs
Normal file
51
bin/core/src/api/listener/integrations/gitlab.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::http::HeaderMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::config::core_config;
|
||||
|
||||
use super::{ExtractBranch, VerifySecret};
|
||||
|
||||
/// Listener implementation for Gitlab type API
|
||||
pub struct Gitlab;
|
||||
|
||||
impl VerifySecret for Gitlab {
|
||||
#[instrument("VerifyGitlabSecret", skip_all)]
|
||||
fn verify_secret(
|
||||
headers: &HeaderMap,
|
||||
_body: &str,
|
||||
custom_secret: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let token = headers
|
||||
.get("x-gitlab-token")
|
||||
.context("No gitlab token in headers")?;
|
||||
let token =
|
||||
token.to_str().context("Failed to get token as string")?;
|
||||
let secret = if custom_secret.is_empty() {
|
||||
core_config().webhook_secret.as_str()
|
||||
} else {
|
||||
custom_secret
|
||||
};
|
||||
if token == secret {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("Webhook secret does not match expected."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GitlabWebhookBody {
|
||||
#[serde(rename = "ref")]
|
||||
branch: String,
|
||||
}
|
||||
|
||||
impl ExtractBranch for Gitlab {
|
||||
fn extract_branch(body: &str) -> anyhow::Result<String> {
|
||||
let branch = serde_json::from_str::<GitlabWebhookBody>(body)
|
||||
.context("Failed to parse gitlab request body")?
|
||||
.branch
|
||||
.replace("refs/heads/", "");
|
||||
Ok(branch)
|
||||
}
|
||||
}
|
||||
4
bin/core/src/api/listener/integrations/mod.rs
Normal file
4
bin/core/src/api/listener/integrations/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod github;
|
||||
pub mod gitlab;
|
||||
|
||||
use super::{ExtractBranch, VerifySecret};
|
||||
57
bin/core/src/api/listener/mod.rs
Normal file
57
bin/core/src/api/listener/mod.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::{Router, http::HeaderMap};
|
||||
use komodo_client::entities::resource::Resource;
|
||||
use mogh_cache::CloneCache;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::resource::KomodoResource;
|
||||
|
||||
mod integrations;
|
||||
mod resources;
|
||||
mod router;
|
||||
|
||||
use integrations::*;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.nest("/github", router::router::<github::Github>())
|
||||
.nest("/gitlab", router::router::<gitlab::Gitlab>())
|
||||
}
|
||||
|
||||
type ListenerLockCache = CloneCache<String, Arc<Mutex<()>>>;
|
||||
|
||||
/// Implemented for all resources which can recieve webhook.
|
||||
trait CustomSecret: KomodoResource {
|
||||
fn custom_secret(
|
||||
resource: &Resource<Self::Config, Self::Info>,
|
||||
) -> &str;
|
||||
}
|
||||
|
||||
/// Implemented on the integration struct, eg [integrations::github::Github]
|
||||
trait VerifySecret {
|
||||
fn verify_secret(
|
||||
headers: &HeaderMap,
|
||||
body: &str,
|
||||
custom_secret: &str,
|
||||
) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
/// Implemented on the integration struct, eg [integrations::github::Github]
|
||||
trait ExtractBranch {
|
||||
fn extract_branch(body: &str) -> anyhow::Result<String>;
|
||||
fn verify_branch(body: &str, expected: &str) -> anyhow::Result<()> {
|
||||
let branch = Self::extract_branch(body)?;
|
||||
if branch == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("request branch does not match expected"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// For Procedures and Actions, incoming webhook
|
||||
/// can be triggered by any branch by using `__ANY__`
|
||||
/// as the branch in the webhook URL.
|
||||
const ANY_BRANCH: &str = "__ANY__";
|
||||
591
bin/core/src/api/listener/resources.rs
Normal file
591
bin/core/src/api/listener/resources.rs
Normal file
@@ -0,0 +1,591 @@
|
||||
use std::{str::FromStr, sync::OnceLock};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::{
|
||||
execute::*,
|
||||
write::{RefreshResourceSyncPending, RefreshStackCache},
|
||||
},
|
||||
entities::{
|
||||
action::Action, build::Build, procedure::Procedure, repo::Repo,
|
||||
stack::Stack, sync::ResourceSync, user::git_webhook_user,
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
execute::{ExecuteArgs, ExecuteRequest},
|
||||
write::WriteArgs,
|
||||
},
|
||||
helpers::update::init_execution_update,
|
||||
resource,
|
||||
};
|
||||
|
||||
use super::{ANY_BRANCH, ListenerLockCache};
|
||||
|
||||
// =======
|
||||
// BUILD
|
||||
// =======
|
||||
|
||||
impl super::CustomSecret for Build {
|
||||
fn custom_secret(resource: &Self) -> &str {
|
||||
&resource.config.webhook_secret
|
||||
}
|
||||
}
|
||||
|
||||
fn build_locks() -> &'static ListenerLockCache {
|
||||
static BUILD_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
|
||||
BUILD_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn handle_build_webhook<B: super::ExtractBranch>(
|
||||
build: Build,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
if !build.config.webhook_enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = build_locks().get_or_insert_default(&build.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
// Use the correct target branch when using linked repo.
|
||||
let branch = if build.config.linked_repo.is_empty() {
|
||||
build.config.branch
|
||||
} else {
|
||||
resource::get::<Repo>(&build.config.linked_repo)
|
||||
.await
|
||||
.context("Failed to find 'linked_repo'")?
|
||||
.config
|
||||
.branch
|
||||
};
|
||||
|
||||
B::verify_branch(&body, &branch)?;
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req = ExecuteRequest::RunBuild(RunBuild { build: build.id });
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RunBuild(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
req
|
||||
.resolve(&ExecuteArgs {
|
||||
user,
|
||||
update,
|
||||
id: Uuid::new_v4(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ======
|
||||
// REPO
|
||||
// ======
|
||||
|
||||
impl super::CustomSecret for Repo {
|
||||
fn custom_secret(resource: &Self) -> &str {
|
||||
&resource.config.webhook_secret
|
||||
}
|
||||
}
|
||||
|
||||
fn repo_locks() -> &'static ListenerLockCache {
|
||||
static REPO_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
|
||||
REPO_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub trait RepoExecution {
|
||||
async fn resolve(repo: Repo) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
impl RepoExecution for CloneRepo {
|
||||
async fn resolve(repo: Repo) -> anyhow::Result<()> {
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req =
|
||||
crate::api::execute::ExecuteRequest::CloneRepo(CloneRepo {
|
||||
repo: repo.id,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let crate::api::execute::ExecuteRequest::CloneRepo(req) = req
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
req
|
||||
.resolve(&ExecuteArgs {
|
||||
user,
|
||||
update,
|
||||
id: Uuid::new_v4(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.error)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RepoExecution for PullRepo {
|
||||
async fn resolve(repo: Repo) -> anyhow::Result<()> {
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req =
|
||||
crate::api::execute::ExecuteRequest::PullRepo(PullRepo {
|
||||
repo: repo.id,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let crate::api::execute::ExecuteRequest::PullRepo(req) = req
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
req
|
||||
.resolve(&ExecuteArgs {
|
||||
user,
|
||||
update,
|
||||
id: Uuid::new_v4(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.error)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RepoExecution for BuildRepo {
|
||||
async fn resolve(repo: Repo) -> anyhow::Result<()> {
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req =
|
||||
crate::api::execute::ExecuteRequest::BuildRepo(BuildRepo {
|
||||
repo: repo.id,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let crate::api::execute::ExecuteRequest::BuildRepo(req) = req
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
req
|
||||
.resolve(&ExecuteArgs {
|
||||
user,
|
||||
update,
|
||||
id: Uuid::new_v4(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.error)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RepoWebhookOption {
|
||||
Clone,
|
||||
Pull,
|
||||
Build,
|
||||
}
|
||||
|
||||
pub async fn handle_repo_webhook<B: super::ExtractBranch>(
|
||||
option: RepoWebhookOption,
|
||||
repo: Repo,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
match option {
|
||||
RepoWebhookOption::Clone => {
|
||||
handle_repo_webhook_inner::<B, CloneRepo>(repo, body).await
|
||||
}
|
||||
RepoWebhookOption::Pull => {
|
||||
handle_repo_webhook_inner::<B, PullRepo>(repo, body).await
|
||||
}
|
||||
RepoWebhookOption::Build => {
|
||||
handle_repo_webhook_inner::<B, BuildRepo>(repo, body).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_repo_webhook_inner<
|
||||
B: super::ExtractBranch,
|
||||
E: RepoExecution,
|
||||
>(
|
||||
repo: Repo,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
if !repo.config.webhook_enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = repo_locks().get_or_insert_default(&repo.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
B::verify_branch(&body, &repo.config.branch)?;
|
||||
|
||||
E::resolve(repo).await
|
||||
}
|
||||
|
||||
// =======
|
||||
// STACK
|
||||
// =======
|
||||
|
||||
impl super::CustomSecret for Stack {
|
||||
fn custom_secret(resource: &Self) -> &str {
|
||||
&resource.config.webhook_secret
|
||||
}
|
||||
}
|
||||
|
||||
fn stack_locks() -> &'static ListenerLockCache {
|
||||
static STACK_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
|
||||
STACK_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub trait StackExecution {
|
||||
async fn resolve(stack: Stack) -> mogh_error::Result<()>;
|
||||
}
|
||||
|
||||
impl StackExecution for RefreshStackCache {
|
||||
async fn resolve(stack: Stack) -> mogh_error::Result<()> {
|
||||
RefreshStackCache { stack: stack.id }
|
||||
.resolve(&WriteArgs {
|
||||
user: git_webhook_user().to_owned(),
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl StackExecution for DeployStack {
|
||||
async fn resolve(stack: Stack) -> mogh_error::Result<()> {
|
||||
let user = git_webhook_user().to_owned();
|
||||
if stack.config.webhook_force_deploy {
|
||||
let req = ExecuteRequest::DeployStack(DeployStack {
|
||||
stack: stack.id,
|
||||
services: Vec::new(),
|
||||
stop_time: None,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::DeployStack(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
req
|
||||
.resolve(&ExecuteArgs {
|
||||
user,
|
||||
update,
|
||||
id: Uuid::new_v4(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.error)?;
|
||||
} else {
|
||||
let req =
|
||||
ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged {
|
||||
stack: stack.id,
|
||||
stop_time: None,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::DeployStackIfChanged(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
req
|
||||
.resolve(&ExecuteArgs {
|
||||
user,
|
||||
update,
|
||||
id: Uuid::new_v4(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.error)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum StackWebhookOption {
|
||||
Refresh,
|
||||
Deploy,
|
||||
}
|
||||
|
||||
pub async fn handle_stack_webhook<B: super::ExtractBranch>(
|
||||
option: StackWebhookOption,
|
||||
stack: Stack,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
match option {
|
||||
StackWebhookOption::Refresh => {
|
||||
handle_stack_webhook_inner::<B, RefreshStackCache>(stack, body)
|
||||
.await
|
||||
}
|
||||
StackWebhookOption::Deploy => {
|
||||
handle_stack_webhook_inner::<B, DeployStack>(stack, body).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_stack_webhook_inner<
|
||||
B: super::ExtractBranch,
|
||||
E: StackExecution,
|
||||
>(
|
||||
stack: Stack,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
if !stack.config.webhook_enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through, from "action state busy".
|
||||
let lock = stack_locks().get_or_insert_default(&stack.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
// Use the correct target branch when using linked repo.
|
||||
let branch = if stack.config.linked_repo.is_empty() {
|
||||
stack.config.branch.clone()
|
||||
} else {
|
||||
resource::get::<Repo>(&stack.config.linked_repo)
|
||||
.await
|
||||
.context("Failed to find 'linked_repo'")?
|
||||
.config
|
||||
.branch
|
||||
};
|
||||
|
||||
B::verify_branch(&body, &branch)?;
|
||||
|
||||
E::resolve(stack).await.map_err(|e| e.error)
|
||||
}
|
||||
|
||||
// ======
|
||||
// SYNC
|
||||
// ======
|
||||
|
||||
impl super::CustomSecret for ResourceSync {
|
||||
fn custom_secret(resource: &Self) -> &str {
|
||||
&resource.config.webhook_secret
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_locks() -> &'static ListenerLockCache {
|
||||
static SYNC_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
|
||||
SYNC_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub trait SyncExecution {
|
||||
async fn resolve(sync: ResourceSync) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
impl SyncExecution for RefreshResourceSyncPending {
|
||||
async fn resolve(sync: ResourceSync) -> anyhow::Result<()> {
|
||||
RefreshResourceSyncPending { sync: sync.id }
|
||||
.resolve(&WriteArgs {
|
||||
user: git_webhook_user().to_owned(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.error)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SyncExecution for RunSync {
|
||||
async fn resolve(sync: ResourceSync) -> anyhow::Result<()> {
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req = ExecuteRequest::RunSync(RunSync {
|
||||
sync: sync.id,
|
||||
resource_type: None,
|
||||
resources: None,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RunSync(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
req
|
||||
.resolve(&ExecuteArgs {
|
||||
user,
|
||||
update,
|
||||
id: Uuid::new_v4(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.error)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SyncWebhookOption {
|
||||
Refresh,
|
||||
Sync,
|
||||
}
|
||||
|
||||
pub async fn handle_sync_webhook<B: super::ExtractBranch>(
|
||||
option: SyncWebhookOption,
|
||||
sync: ResourceSync,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
match option {
|
||||
SyncWebhookOption::Refresh => {
|
||||
handle_sync_webhook_inner::<B, RefreshResourceSyncPending>(
|
||||
sync, body,
|
||||
)
|
||||
.await
|
||||
}
|
||||
SyncWebhookOption::Sync => {
|
||||
handle_sync_webhook_inner::<B, RunSync>(sync, body).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_sync_webhook_inner<
|
||||
B: super::ExtractBranch,
|
||||
E: SyncExecution,
|
||||
>(
|
||||
sync: ResourceSync,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
if !sync.config.webhook_enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = sync_locks().get_or_insert_default(&sync.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
// Use the correct target branch when using linked repo.
|
||||
let branch = if sync.config.linked_repo.is_empty() {
|
||||
sync.config.branch.clone()
|
||||
} else {
|
||||
resource::get::<Repo>(&sync.config.linked_repo)
|
||||
.await
|
||||
.context("Failed to find 'linked_repo'")?
|
||||
.config
|
||||
.branch
|
||||
};
|
||||
|
||||
B::verify_branch(&body, &branch)?;
|
||||
|
||||
E::resolve(sync).await
|
||||
}
|
||||
|
||||
// ===========
|
||||
// PROCEDURE
|
||||
// ===========
|
||||
|
||||
impl super::CustomSecret for Procedure {
|
||||
fn custom_secret(resource: &Self) -> &str {
|
||||
&resource.config.webhook_secret
|
||||
}
|
||||
}
|
||||
|
||||
fn procedure_locks() -> &'static ListenerLockCache {
|
||||
static PROCEDURE_LOCKS: OnceLock<ListenerLockCache> =
|
||||
OnceLock::new();
|
||||
PROCEDURE_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn handle_procedure_webhook<B: super::ExtractBranch>(
|
||||
procedure: Procedure,
|
||||
target_branch: &str,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
if !procedure.config.webhook_enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock =
|
||||
procedure_locks().get_or_insert_default(&procedure.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
if target_branch != ANY_BRANCH {
|
||||
B::verify_branch(&body, target_branch)?;
|
||||
}
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req = ExecuteRequest::RunProcedure(RunProcedure {
|
||||
procedure: procedure.id,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RunProcedure(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
req
|
||||
.resolve(&ExecuteArgs {
|
||||
user,
|
||||
update,
|
||||
id: Uuid::new_v4(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ========
|
||||
// ACTION
|
||||
// ========
|
||||
|
||||
impl super::CustomSecret for Action {
|
||||
fn custom_secret(resource: &Self) -> &str {
|
||||
&resource.config.webhook_secret
|
||||
}
|
||||
}
|
||||
|
||||
fn action_locks() -> &'static ListenerLockCache {
|
||||
static ACTION_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
|
||||
ACTION_LOCKS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn handle_action_webhook<B: super::ExtractBranch>(
|
||||
action: Action,
|
||||
target_branch: &str,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
if !action.config.webhook_enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = action_locks().get_or_insert_default(&action.id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
let branch = B::extract_branch(&body)?;
|
||||
|
||||
if target_branch != ANY_BRANCH && branch != target_branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
|
||||
let body = serde_json::Value::from_str(&body)
|
||||
.context("Failed to deserialize webhook body")?;
|
||||
let serde_json::Value::Object(args) = json!({
|
||||
"WEBHOOK_BRANCH": branch,
|
||||
"WEBHOOK_BODY": body,
|
||||
}) else {
|
||||
return Err(anyhow!("Something is wrong with serde_json..."));
|
||||
};
|
||||
|
||||
let req = ExecuteRequest::RunAction(RunAction {
|
||||
action: action.id,
|
||||
args: args.into(),
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RunAction(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
req
|
||||
.resolve(&ExecuteArgs {
|
||||
user,
|
||||
update,
|
||||
id: Uuid::new_v4(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.error)?;
|
||||
Ok(())
|
||||
}
|
||||
229
bin/core/src/api/listener/router.rs
Normal file
229
bin/core/src/api/listener/router.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use axum::{Router, extract::Path, http::HeaderMap, routing::post};
|
||||
use komodo_client::entities::{
|
||||
action::Action, build::Build, procedure::Procedure, repo::Repo,
|
||||
resource::Resource, stack::Stack, sync::ResourceSync,
|
||||
};
|
||||
use mogh_auth_server::request_ip::RequestIp;
|
||||
use mogh_error::AddStatusCode;
|
||||
use mogh_rate_limit::WithFailureRateLimit;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::{auth::GENERAL_RATE_LIMITER, resource::KomodoResource};
|
||||
|
||||
use super::{
|
||||
CustomSecret, ExtractBranch, VerifySecret,
|
||||
resources::{
|
||||
RepoWebhookOption, StackWebhookOption, SyncWebhookOption,
|
||||
handle_action_webhook, handle_build_webhook,
|
||||
handle_procedure_webhook, handle_repo_webhook,
|
||||
handle_stack_webhook, handle_sync_webhook,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Id {
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct IdAndOption<T> {
|
||||
id: String,
|
||||
option: T,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct IdAndBranch {
|
||||
id: String,
|
||||
#[serde(default = "default_branch")]
|
||||
branch: String,
|
||||
}
|
||||
|
||||
fn default_branch() -> String {
|
||||
String::from("main")
|
||||
}
|
||||
|
||||
pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/build/{id}",
|
||||
post(
|
||||
|Path(Id { id }), RequestIp(ip), headers: HeaderMap, body: String| async move {
|
||||
let build =
|
||||
auth_webhook::<P, Build>(&id, &headers, ip, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("BuildWebhook", id);
|
||||
async {
|
||||
let res = handle_build_webhook::<P>(
|
||||
build, body,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed at running webhook for build {id} | {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
mogh_error::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/repo/{id}/{option}",
|
||||
post(
|
||||
|Path(IdAndOption::<RepoWebhookOption> { id, option }), RequestIp(ip), headers: HeaderMap, body: String| async move {
|
||||
let repo =
|
||||
auth_webhook::<P, Repo>(&id, &headers, ip, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("RepoWebhook", id);
|
||||
async {
|
||||
let res = handle_repo_webhook::<P>(
|
||||
option, repo, body,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed at running webhook for repo {id} | {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
mogh_error::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/stack/{id}/{option}",
|
||||
post(
|
||||
|Path(IdAndOption::<StackWebhookOption> { id, option }), RequestIp(ip), headers: HeaderMap, body: String| async move {
|
||||
let stack =
|
||||
auth_webhook::<P, Stack>(&id, &headers, ip, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("StackWebhook", id);
|
||||
async {
|
||||
let res = handle_stack_webhook::<P>(
|
||||
option, stack, body,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed at running webhook for stack {id} | {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
mogh_error::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/sync/{id}/{option}",
|
||||
post(
|
||||
|Path(IdAndOption::<SyncWebhookOption> { id, option }), RequestIp(ip), headers: HeaderMap, body: String| async move {
|
||||
let sync =
|
||||
auth_webhook::<P, ResourceSync>(&id, &headers, ip, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("ResourceSyncWebhook", id);
|
||||
async {
|
||||
let res = handle_sync_webhook::<P>(
|
||||
option, sync, body,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed at running webhook for resource sync {id} | {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
mogh_error::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/procedure/{id}/{branch}",
|
||||
post(
|
||||
|Path(IdAndBranch { id, branch }), RequestIp(ip), headers: HeaderMap, body: String| async move {
|
||||
let procedure =
|
||||
auth_webhook::<P, Procedure>(&id, &headers, ip, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("ProcedureWebhook", id);
|
||||
async {
|
||||
let res = handle_procedure_webhook::<P>(
|
||||
procedure, &branch, body,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed at running webhook for procedure {id} | target branch: {branch} | {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
mogh_error::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/action/{id}/{branch}",
|
||||
post(
|
||||
|Path(IdAndBranch { id, branch }), RequestIp(ip), headers: HeaderMap, body: String| async move {
|
||||
let action =
|
||||
auth_webhook::<P, Action>(&id, &headers, ip, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("ActionWebhook", id);
|
||||
async {
|
||||
let res = handle_action_webhook::<P>(
|
||||
action, &branch, body,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed at running webhook for action {id} | target branch: {branch} | {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
mogh_error::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
async fn auth_webhook<P, R>(
|
||||
id: &str,
|
||||
headers: &HeaderMap,
|
||||
ip: IpAddr,
|
||||
body: &str,
|
||||
) -> mogh_error::Result<Resource<R::Config, R::Info>>
|
||||
where
|
||||
P: VerifySecret,
|
||||
R: KomodoResource + CustomSecret,
|
||||
{
|
||||
async {
|
||||
let resource = crate::resource::get::<R>(id)
|
||||
.await
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
P::verify_secret(headers, body, R::custom_secret(&resource))
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
mogh_error::Result::Ok(resource)
|
||||
}
|
||||
.with_failure_rate_limit_using_ip(&GENERAL_RATE_LIMITER, &ip)
|
||||
.await
|
||||
}
|
||||
57
bin/core/src/api/mod.rs
Normal file
57
bin/core/src/api/mod.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use axum::{Extension, Router, routing::get};
|
||||
use komodo_client::entities::user::User;
|
||||
use mogh_auth_server::middleware::authenticate_request;
|
||||
use mogh_error::Json;
|
||||
use mogh_server::{
|
||||
cors::cors_layer, session::memory_session_layer,
|
||||
ui::serve_static_ui,
|
||||
};
|
||||
|
||||
use crate::{auth::KomodoAuthImpl, config::core_config, ts_client};
|
||||
|
||||
pub mod execute;
|
||||
pub mod read;
|
||||
pub mod write;
|
||||
|
||||
mod listener;
|
||||
mod openapi;
|
||||
mod terminal;
|
||||
mod ws;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Variant {
|
||||
variant: String,
|
||||
}
|
||||
|
||||
pub fn app() -> Router {
|
||||
let config = core_config();
|
||||
Router::new()
|
||||
.merge(openapi::serve_docs())
|
||||
.route("/version", get(|| async { env!("CARGO_PKG_VERSION") }))
|
||||
.nest("/auth", mogh_auth_server::api::router::<KomodoAuthImpl>())
|
||||
.nest("/user", user_router())
|
||||
.nest("/read", read::router())
|
||||
.nest("/write", write::router())
|
||||
.nest("/execute", execute::router())
|
||||
.nest("/terminal", terminal::router())
|
||||
.nest("/listener", listener::router())
|
||||
.nest("/ws", ws::router())
|
||||
.nest("/client", ts_client::router())
|
||||
.layer(memory_session_layer(config))
|
||||
.fallback_service(serve_static_ui(
|
||||
&config.ui_path,
|
||||
config.ui_index_force_no_cache,
|
||||
))
|
||||
.layer(cors_layer(config))
|
||||
}
|
||||
|
||||
fn user_router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/",
|
||||
get(|Extension(user): Extension<User>| async { Json(user) }),
|
||||
)
|
||||
.layer(axum::middleware::from_fn(
|
||||
authenticate_request::<KomodoAuthImpl, false>,
|
||||
))
|
||||
}
|
||||
18
bin/core/src/api/openapi/docs.html
Normal file
18
bin/core/src/api/openapi/docs.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Komodo API Docs</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script id="api-reference" type="application/json">
|
||||
$spec
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
8
bin/core/src/api/openapi/mod.rs
Normal file
8
bin/core/src/api/openapi/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use komodo_client::openapi::KomodoApi;
|
||||
use utoipa::OpenApi as _;
|
||||
use utoipa_scalar::{Scalar, Servable as _};
|
||||
|
||||
pub fn serve_docs() -> Scalar<utoipa::openapi::OpenApi> {
|
||||
Scalar::with_url("/docs", KomodoApi::openapi())
|
||||
.custom_html(include_str!("docs.html"))
|
||||
}
|
||||
147
bin/core/src/api/read/action.rs
Normal file
147
bin/core/src/api/read/action.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use anyhow::Context;
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
action::{
|
||||
Action, ActionActionState, ActionListItem, ActionState,
|
||||
},
|
||||
permission::PermissionLevel,
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{action_state_cache, action_states},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetAction {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Action> {
|
||||
Ok(
|
||||
get_check_permissions::<Action>(
|
||||
&self.action,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListActions {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Vec<ActionListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Action>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListFullActions {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListFullActionsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Action>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetActionActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ActionActionState> {
|
||||
let action = get_check_permissions::<Action>(
|
||||
&self.action,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.action
|
||||
.get(&action.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetActionsSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetActionsSummaryResponse> {
|
||||
let actions = resource::list_full_for_user::<Action>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get actions from db")?;
|
||||
|
||||
let mut res = GetActionsSummaryResponse::default();
|
||||
|
||||
let cache = action_state_cache();
|
||||
let action_states = action_states();
|
||||
|
||||
for action in actions {
|
||||
res.total += 1;
|
||||
|
||||
match (
|
||||
cache.get(&action.id).await.unwrap_or_default(),
|
||||
action_states
|
||||
.action
|
||||
.get(&action.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?,
|
||||
) {
|
||||
(_, action_states) if action_states.running > 0 => {
|
||||
res.running += action_states.running;
|
||||
}
|
||||
(ActionState::Ok, _) => res.ok += 1,
|
||||
(ActionState::Failed, _) => res.failed += 1,
|
||||
(ActionState::Unknown, _) => res.unknown += 1,
|
||||
// will never come off the cache in the running state, since that comes from action states
|
||||
(ActionState::Running, _) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
81
bin/core/src/api/read/alert.rs
Normal file
81
bin/core/src/api/read/alert.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use anyhow::Context;
|
||||
use database::mungos::{
|
||||
by_id::find_one_by_id,
|
||||
find::find_collect,
|
||||
mongodb::{bson::doc, options::FindOptions},
|
||||
};
|
||||
use komodo_client::{
|
||||
api::read::{
|
||||
GetAlert, GetAlertResponse, ListAlerts, ListAlertsResponse,
|
||||
},
|
||||
entities::permission::PermissionLevel,
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
permission::{
|
||||
check_user_target_access, user_resource_target_query,
|
||||
},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
const NUM_ALERTS_PER_PAGE: u64 = 100;
|
||||
|
||||
impl Resolve<ReadArgs> for ListAlerts {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListAlertsResponse> {
|
||||
// Alerts
|
||||
let query = user_resource_target_query(user, self.query)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let alerts = find_collect(
|
||||
&db_client().alerts,
|
||||
query,
|
||||
FindOptions::builder()
|
||||
.sort(doc! { "ts": -1 })
|
||||
.limit(NUM_ALERTS_PER_PAGE as i64)
|
||||
.skip(self.page * NUM_ALERTS_PER_PAGE)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.context("failed to get alerts from db")?;
|
||||
|
||||
let next_page = if alerts.len() < NUM_ALERTS_PER_PAGE as usize {
|
||||
None
|
||||
} else {
|
||||
Some((self.page + 1) as i64)
|
||||
};
|
||||
|
||||
let res = ListAlertsResponse { next_page, alerts };
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetAlert {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetAlertResponse> {
|
||||
let alert = find_one_by_id(&db_client().alerts, &self.id)
|
||||
.await
|
||||
.context("failed to query db for alert")?
|
||||
.context("no alert found with given id")?;
|
||||
if user.admin || core_config().transparent_mode {
|
||||
return Ok(alert);
|
||||
}
|
||||
check_user_target_access(
|
||||
&alert.target,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(alert)
|
||||
}
|
||||
}
|
||||
109
bin/core/src/api/read/alerter.rs
Normal file
109
bin/core/src/api/read/alerter.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use anyhow::Context;
|
||||
use database::mongo_indexed::Document;
|
||||
use database::mungos::mongodb::bson::doc;
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
alerter::{Alerter, AlerterListItem},
|
||||
permission::PermissionLevel,
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
permission::{get_check_permissions, list_resource_ids_for_user},
|
||||
resource,
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetAlerter {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Alerter> {
|
||||
Ok(
|
||||
get_check_permissions::<Alerter>(
|
||||
&self.alerter,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListAlerters {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Vec<AlerterListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Alerter>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListFullAlerters {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListFullAlertersResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Alerter>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetAlertersSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetAlertersSummaryResponse> {
|
||||
let query = match list_resource_ids_for_user::<Alerter>(
|
||||
None,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(ids) => doc! {
|
||||
"_id": { "$in": ids }
|
||||
},
|
||||
None => Document::new(),
|
||||
};
|
||||
let total = db_client()
|
||||
.alerters
|
||||
.count_documents(query)
|
||||
.await
|
||||
.context("failed to count all alerter documents")?;
|
||||
let res = GetAlertersSummaryResponse {
|
||||
total: total as u32,
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
304
bin/core/src/api/read/build.rs
Normal file
304
bin/core/src/api/read/build.rs
Normal file
@@ -0,0 +1,304 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use anyhow::Context;
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use database::mungos::{
|
||||
find::find_collect,
|
||||
mongodb::{bson::doc, options::FindOptions},
|
||||
};
|
||||
use futures_util::TryStreamExt;
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
Operation,
|
||||
build::{Build, BuildActionState, BuildListItem, BuildState},
|
||||
permission::PermissionLevel,
|
||||
update::UpdateStatus,
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{action_states, build_state_cache, db_client},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetBuild {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Build> {
|
||||
Ok(
|
||||
get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListBuilds {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Vec<BuildListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Build>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListFullBuilds {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListFullBuildsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Build>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetBuildActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<BuildActionState> {
|
||||
let build = get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.build
|
||||
.get(&build.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetBuildsSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetBuildsSummaryResponse> {
|
||||
let builds = resource::list_full_for_user::<Build>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get all builds")?;
|
||||
|
||||
let mut res = GetBuildsSummaryResponse::default();
|
||||
|
||||
let cache = build_state_cache();
|
||||
let action_states = action_states();
|
||||
|
||||
for build in builds {
|
||||
res.total += 1;
|
||||
|
||||
match (
|
||||
cache.get(&build.id).await.unwrap_or_default(),
|
||||
action_states
|
||||
.build
|
||||
.get(&build.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?,
|
||||
) {
|
||||
(_, action_states) if action_states.building => {
|
||||
res.building += 1;
|
||||
}
|
||||
(BuildState::Ok, _) => res.ok += 1,
|
||||
(BuildState::Failed, _) => res.failed += 1,
|
||||
(BuildState::Unknown, _) => res.unknown += 1,
|
||||
// will never come off the cache in the building state, since that comes from action states
|
||||
(BuildState::Building, _) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
const ONE_DAY_MS: i64 = 86400000;
|
||||
|
||||
impl Resolve<ReadArgs> for GetBuildMonthlyStats {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &ReadArgs,
|
||||
) -> mogh_error::Result<GetBuildMonthlyStatsResponse> {
|
||||
let curr_ts = unix_timestamp_ms() as i64;
|
||||
let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS;
|
||||
|
||||
let close_ts = next_day - self.page as i64 * 30 * ONE_DAY_MS;
|
||||
let open_ts = close_ts - 30 * ONE_DAY_MS;
|
||||
|
||||
let mut build_updates = db_client()
|
||||
.updates
|
||||
.find(doc! {
|
||||
"start_ts": {
|
||||
"$gte": open_ts,
|
||||
"$lt": close_ts
|
||||
},
|
||||
"operation": Operation::RunBuild.to_string(),
|
||||
})
|
||||
.await
|
||||
.context("failed to get updates cursor")?;
|
||||
|
||||
let mut days = HashMap::<i64, BuildStatsDay>::with_capacity(32);
|
||||
|
||||
let mut curr = open_ts;
|
||||
|
||||
while curr < close_ts {
|
||||
let stats = BuildStatsDay {
|
||||
ts: curr as f64,
|
||||
..Default::default()
|
||||
};
|
||||
days.insert(curr, stats);
|
||||
curr += ONE_DAY_MS;
|
||||
}
|
||||
|
||||
while let Some(update) = build_updates.try_next().await? {
|
||||
if let Some(end_ts) = update.end_ts {
|
||||
let day = update.start_ts - update.start_ts % ONE_DAY_MS;
|
||||
let entry = days.entry(day).or_default();
|
||||
entry.count += 1.0;
|
||||
entry.time += ms_to_hour(end_ts - update.start_ts);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(GetBuildMonthlyStatsResponse::new(
|
||||
days.into_values().collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
const MS_TO_HOUR_DIVISOR: f64 = 1000.0 * 60.0 * 60.0;
|
||||
fn ms_to_hour(duration: i64) -> f64 {
|
||||
duration as f64 / MS_TO_HOUR_DIVISOR
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListBuildVersions {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Vec<BuildVersionResponseItem>> {
|
||||
let ListBuildVersions {
|
||||
build,
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
limit,
|
||||
} = self;
|
||||
let build = get_check_permissions::<Build>(
|
||||
&build,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut filter = doc! {
|
||||
"target": {
|
||||
"type": "Build",
|
||||
"id": build.id
|
||||
},
|
||||
"operation": Operation::RunBuild.to_string(),
|
||||
"status": UpdateStatus::Complete.to_string(),
|
||||
"success": true
|
||||
};
|
||||
if let Some(major) = major {
|
||||
filter.insert("version.major", major);
|
||||
}
|
||||
if let Some(minor) = minor {
|
||||
filter.insert("version.minor", minor);
|
||||
}
|
||||
if let Some(patch) = patch {
|
||||
filter.insert("version.patch", patch);
|
||||
}
|
||||
|
||||
let versions = find_collect(
|
||||
&db_client().updates,
|
||||
filter,
|
||||
FindOptions::builder()
|
||||
.sort(doc! { "_id": -1 })
|
||||
.limit(limit)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.context("failed to pull versions from mongo")?
|
||||
.into_iter()
|
||||
.map(|u| (u.version, u.start_ts))
|
||||
.filter(|(v, _)| !v.is_none())
|
||||
.map(|(version, ts)| BuildVersionResponseItem { version, ts })
|
||||
.collect();
|
||||
Ok(versions)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListCommonBuildExtraArgs {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListCommonBuildExtraArgsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
let builds = resource::list_full_for_user::<Build>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await
|
||||
.context("failed to get resources matching query")?;
|
||||
|
||||
// first collect with guaranteed uniqueness
|
||||
let mut res = HashSet::<String>::new();
|
||||
|
||||
for build in builds {
|
||||
for extra_arg in build.config.extra_args {
|
||||
res.insert(extra_arg);
|
||||
}
|
||||
}
|
||||
|
||||
let mut res = res.into_iter().collect::<Vec<_>>();
|
||||
res.sort();
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
109
bin/core/src/api/read/builder.rs
Normal file
109
bin/core/src/api/read/builder.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use anyhow::Context;
|
||||
use database::mongo_indexed::Document;
|
||||
use database::mungos::mongodb::bson::doc;
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
builder::{Builder, BuilderListItem},
|
||||
permission::PermissionLevel,
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
permission::{get_check_permissions, list_resource_ids_for_user},
|
||||
resource,
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetBuilder {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Builder> {
|
||||
Ok(
|
||||
get_check_permissions::<Builder>(
|
||||
&self.builder,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListBuilders {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Vec<BuilderListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Builder>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListFullBuilders {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListFullBuildersResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Builder>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetBuildersSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetBuildersSummaryResponse> {
|
||||
let query = match list_resource_ids_for_user::<Builder>(
|
||||
None,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(ids) => doc! {
|
||||
"_id": { "$in": ids }
|
||||
},
|
||||
None => Document::new(),
|
||||
};
|
||||
let total = db_client()
|
||||
.builders
|
||||
.count_documents(query)
|
||||
.await
|
||||
.context("failed to count all builder documents")?;
|
||||
let res = GetBuildersSummaryResponse {
|
||||
total: total as u32,
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
445
bin/core/src/api/read/deployment.rs
Normal file
445
bin/core/src/api/read/deployment.rs
Normal file
@@ -0,0 +1,445 @@
|
||||
use std::{cmp, collections::HashSet};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
SwarmOrServer,
|
||||
deployment::{
|
||||
Deployment, DeploymentActionState, DeploymentConfig,
|
||||
DeploymentListItem, DeploymentState,
|
||||
},
|
||||
docker::{
|
||||
container::{Container, ContainerStats},
|
||||
service::SwarmService,
|
||||
},
|
||||
permission::PermissionLevel,
|
||||
server::{Server, ServerState},
|
||||
update::Log,
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCodeError as _;
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api::{self, container::InspectContainer};
|
||||
use reqwest::StatusCode;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
periphery_client, query::get_all_tags, swarm::swarm_request,
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource::{self, setup_deployment_execution},
|
||||
state::{
|
||||
action_states, deployment_status_cache, server_status_cache,
|
||||
},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetDeployment {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Deployment> {
|
||||
Ok(
|
||||
get_check_permissions::<Deployment>(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListDeployments {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Vec<DeploymentListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
let only_update_available = self.query.specific.update_available;
|
||||
let deployments = resource::list_for_user::<Deployment>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?;
|
||||
let deployments = if only_update_available {
|
||||
deployments
|
||||
.into_iter()
|
||||
.filter(|deployment| deployment.info.update_available)
|
||||
.collect()
|
||||
} else {
|
||||
deployments
|
||||
};
|
||||
Ok(deployments)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListFullDeployments {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListFullDeploymentsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Deployment>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetDeploymentContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetDeploymentContainerResponse> {
|
||||
let deployment = get_check_permissions::<Deployment>(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let status = deployment_status_cache()
|
||||
.get(&deployment.id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let response = GetDeploymentContainerResponse {
|
||||
state: status.curr.state,
|
||||
container: status.curr.container.clone(),
|
||||
};
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_LOG_LENGTH: u64 = 5000;
|
||||
|
||||
impl Resolve<ReadArgs> for GetDeploymentLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Log> {
|
||||
let GetDeploymentLog {
|
||||
deployment,
|
||||
tail,
|
||||
timestamps,
|
||||
} = self;
|
||||
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&deployment,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::None => unreachable!(),
|
||||
SwarmOrServer::Swarm(swarm) => swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLog {
|
||||
service: deployment.name,
|
||||
tail,
|
||||
timestamps,
|
||||
no_task_ids: false,
|
||||
no_resolve: false,
|
||||
details: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to get service log from swarm")?,
|
||||
SwarmOrServer::Server(server) => periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::GetContainerLog {
|
||||
name: deployment.name,
|
||||
tail: cmp::min(tail, MAX_LOG_LENGTH),
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed at call to periphery")?,
|
||||
};
|
||||
|
||||
Ok(log)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for SearchDeploymentLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Log> {
|
||||
let SearchDeploymentLog {
|
||||
deployment,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
} = self;
|
||||
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&deployment,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::None => unreachable!(),
|
||||
SwarmOrServer::Swarm(swarm) => swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLogSearch {
|
||||
service: deployment.name,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
no_task_ids: false,
|
||||
no_resolve: false,
|
||||
details: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to search service log from swarm")?,
|
||||
SwarmOrServer::Server(server) => periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::GetContainerLogSearch {
|
||||
name: deployment.name,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("Failed to search container log from server")?,
|
||||
};
|
||||
|
||||
Ok(log)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectDeploymentContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Container> {
|
||||
let InspectDeploymentContainer { deployment } = self;
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&deployment,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"InspectDeploymentContainer should not be called for Deployment in Swarm Mode"
|
||||
)
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
|
||||
if cache.state != ServerState::Ok {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"Cannot inspect container: Server is {:?}",
|
||||
cache.state
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
periphery_client(&server)
|
||||
.await?
|
||||
.request(InspectContainer {
|
||||
name: deployment.name,
|
||||
})
|
||||
.await
|
||||
.context("Failed to inspect container on server")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectDeploymentSwarmService {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<SwarmService> {
|
||||
let InspectDeploymentSwarmService { deployment } = self;
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&deployment,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Swarm(swarm) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"InspectDeploymentSwarmService should only be called for Deployment in Swarm Mode"
|
||||
)
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmService {
|
||||
service: deployment.name,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to inspect service on swarm")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetDeploymentStats {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ContainerStats> {
|
||||
let Deployment {
|
||||
name,
|
||||
config: DeploymentConfig { server_id, .. },
|
||||
..
|
||||
} = get_check_permissions::<Deployment>(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
if server_id.is_empty() {
|
||||
return Err(
|
||||
anyhow!("deployment has no server attached").into(),
|
||||
);
|
||||
}
|
||||
let server = resource::get::<Server>(&server_id).await?;
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::GetContainerStats { name })
|
||||
.await
|
||||
.context("failed to get stats from periphery")?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetDeploymentActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<DeploymentActionState> {
|
||||
let deployment = get_check_permissions::<Deployment>(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get(&deployment.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetDeploymentsSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetDeploymentsSummaryResponse> {
|
||||
let deployments = resource::list_full_for_user::<Deployment>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get deployments from db")?;
|
||||
let mut res = GetDeploymentsSummaryResponse::default();
|
||||
let status_cache = deployment_status_cache();
|
||||
for deployment in deployments {
|
||||
res.total += 1;
|
||||
let status =
|
||||
status_cache.get(&deployment.id).await.unwrap_or_default();
|
||||
match status.curr.state {
|
||||
DeploymentState::Running => {
|
||||
res.running += 1;
|
||||
}
|
||||
DeploymentState::Exited | DeploymentState::Paused => {
|
||||
res.stopped += 1;
|
||||
}
|
||||
DeploymentState::NotDeployed => {
|
||||
res.not_deployed += 1;
|
||||
}
|
||||
DeploymentState::Unknown => {
|
||||
if !deployment.template {
|
||||
res.unknown += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
res.unhealthy += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListCommonDeploymentExtraArgs {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListCommonDeploymentExtraArgsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
let deployments = resource::list_full_for_user::<Deployment>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await
|
||||
.context("failed to get resources matching query")?;
|
||||
|
||||
// first collect with guaranteed uniqueness
|
||||
let mut res = HashSet::<String>::new();
|
||||
|
||||
for deployment in deployments {
|
||||
for extra_arg in deployment.config.extra_args {
|
||||
res.insert(extra_arg);
|
||||
}
|
||||
}
|
||||
|
||||
let mut res = res.into_iter().collect::<Vec<_>>();
|
||||
res.sort();
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
637
bin/core/src/api/read/mod.rs
Normal file
637
bin/core/src/api/read/mod.rs
Normal file
@@ -0,0 +1,637 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
Extension, Router, extract::Path, middleware, routing::post,
|
||||
};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
ResourceTarget,
|
||||
build::Build,
|
||||
builder::{Builder, BuilderConfig},
|
||||
config::{DockerRegistry, GitProvider},
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
sync::ResourceSync,
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mogh_auth_server::middleware::authenticate_request;
|
||||
use mogh_error::Response;
|
||||
use mogh_error::{AddStatusCodeError, Json};
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use strum::{Display, EnumDiscriminants};
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
auth::KomodoAuthImpl,
|
||||
config::{core_config, core_keys},
|
||||
helpers::periphery_client,
|
||||
resource,
|
||||
};
|
||||
|
||||
use super::Variant;
|
||||
|
||||
mod action;
|
||||
mod alert;
|
||||
mod alerter;
|
||||
mod build;
|
||||
mod builder;
|
||||
mod deployment;
|
||||
mod onboarding_key;
|
||||
mod permission;
|
||||
mod procedure;
|
||||
mod provider;
|
||||
mod repo;
|
||||
mod schedule;
|
||||
mod server;
|
||||
mod stack;
|
||||
mod swarm;
|
||||
mod sync;
|
||||
mod tag;
|
||||
mod terminal;
|
||||
mod toml;
|
||||
mod update;
|
||||
mod user;
|
||||
mod user_group;
|
||||
mod variable;
|
||||
|
||||
pub struct ReadArgs {
|
||||
pub user: User,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants,
|
||||
)]
|
||||
#[strum_discriminants(name(ReadRequestVariant), derive(Display))]
|
||||
#[args(ReadArgs)]
|
||||
#[response(Response)]
|
||||
#[error(mogh_error::Error)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
enum ReadRequest {
|
||||
GetVersion(GetVersion),
|
||||
GetCoreInfo(GetCoreInfo),
|
||||
ListSecrets(ListSecrets),
|
||||
ListGitProvidersFromConfig(ListGitProvidersFromConfig),
|
||||
ListDockerRegistriesFromConfig(ListDockerRegistriesFromConfig),
|
||||
|
||||
// ==== SWARM ====
|
||||
GetSwarmsSummary(GetSwarmsSummary),
|
||||
GetSwarm(GetSwarm),
|
||||
GetSwarmActionState(GetSwarmActionState),
|
||||
ListSwarms(ListSwarms),
|
||||
InspectSwarm(InspectSwarm),
|
||||
ListFullSwarms(ListFullSwarms),
|
||||
ListSwarmNodes(ListSwarmNodes),
|
||||
InspectSwarmNode(InspectSwarmNode),
|
||||
ListSwarmConfigs(ListSwarmConfigs),
|
||||
InspectSwarmConfig(InspectSwarmConfig),
|
||||
ListSwarmSecrets(ListSwarmSecrets),
|
||||
InspectSwarmSecret(InspectSwarmSecret),
|
||||
ListSwarmStacks(ListSwarmStacks),
|
||||
InspectSwarmStack(InspectSwarmStack),
|
||||
ListSwarmTasks(ListSwarmTasks),
|
||||
InspectSwarmTask(InspectSwarmTask),
|
||||
ListSwarmServices(ListSwarmServices),
|
||||
InspectSwarmService(InspectSwarmService),
|
||||
GetSwarmServiceLog(GetSwarmServiceLog),
|
||||
SearchSwarmServiceLog(SearchSwarmServiceLog),
|
||||
ListSwarmNetworks(ListSwarmNetworks),
|
||||
|
||||
// ==== SERVER ====
|
||||
GetServersSummary(GetServersSummary),
|
||||
GetServer(GetServer),
|
||||
GetServerState(GetServerState),
|
||||
GetPeripheryInformation(GetPeripheryInformation),
|
||||
GetServerActionState(GetServerActionState),
|
||||
ListServers(ListServers),
|
||||
ListFullServers(ListFullServers),
|
||||
|
||||
// ==== TERMINAL ====
|
||||
ListTerminals(ListTerminals),
|
||||
|
||||
// ==== DOCKER ====
|
||||
GetDockerContainersSummary(GetDockerContainersSummary),
|
||||
ListAllDockerContainers(ListAllDockerContainers),
|
||||
ListDockerContainers(ListDockerContainers),
|
||||
InspectDockerContainer(InspectDockerContainer),
|
||||
GetResourceMatchingContainer(GetResourceMatchingContainer),
|
||||
GetContainerLog(GetContainerLog),
|
||||
SearchContainerLog(SearchContainerLog),
|
||||
ListComposeProjects(ListComposeProjects),
|
||||
ListDockerNetworks(ListDockerNetworks),
|
||||
InspectDockerNetwork(InspectDockerNetwork),
|
||||
ListDockerImages(ListDockerImages),
|
||||
InspectDockerImage(InspectDockerImage),
|
||||
ListDockerImageHistory(ListDockerImageHistory),
|
||||
ListDockerVolumes(ListDockerVolumes),
|
||||
InspectDockerVolume(InspectDockerVolume),
|
||||
|
||||
// ==== SERVER STATS ====
|
||||
GetSystemInformation(GetSystemInformation),
|
||||
GetSystemStats(GetSystemStats),
|
||||
GetHistoricalServerStats(GetHistoricalServerStats),
|
||||
ListSystemProcesses(ListSystemProcesses),
|
||||
|
||||
// ==== STACK ====
|
||||
GetStacksSummary(GetStacksSummary),
|
||||
GetStack(GetStack),
|
||||
GetStackActionState(GetStackActionState),
|
||||
GetStackLog(GetStackLog),
|
||||
SearchStackLog(SearchStackLog),
|
||||
InspectStackContainer(InspectStackContainer),
|
||||
InspectStackSwarmService(InspectStackSwarmService),
|
||||
ListStacks(ListStacks),
|
||||
ListFullStacks(ListFullStacks),
|
||||
ListStackServices(ListStackServices),
|
||||
ListCommonStackExtraArgs(ListCommonStackExtraArgs),
|
||||
ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs),
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
GetDeploymentsSummary(GetDeploymentsSummary),
|
||||
GetDeployment(GetDeployment),
|
||||
GetDeploymentContainer(GetDeploymentContainer),
|
||||
GetDeploymentActionState(GetDeploymentActionState),
|
||||
GetDeploymentStats(GetDeploymentStats),
|
||||
GetDeploymentLog(GetDeploymentLog),
|
||||
SearchDeploymentLog(SearchDeploymentLog),
|
||||
InspectDeploymentContainer(InspectDeploymentContainer),
|
||||
InspectDeploymentSwarmService(InspectDeploymentSwarmService),
|
||||
ListDeployments(ListDeployments),
|
||||
ListFullDeployments(ListFullDeployments),
|
||||
ListCommonDeploymentExtraArgs(ListCommonDeploymentExtraArgs),
|
||||
|
||||
// ==== BUILD ====
|
||||
GetBuildsSummary(GetBuildsSummary),
|
||||
GetBuild(GetBuild),
|
||||
GetBuildActionState(GetBuildActionState),
|
||||
GetBuildMonthlyStats(GetBuildMonthlyStats),
|
||||
ListBuildVersions(ListBuildVersions),
|
||||
ListBuilds(ListBuilds),
|
||||
ListFullBuilds(ListFullBuilds),
|
||||
ListCommonBuildExtraArgs(ListCommonBuildExtraArgs),
|
||||
|
||||
// ==== REPO ====
|
||||
GetReposSummary(GetReposSummary),
|
||||
GetRepo(GetRepo),
|
||||
GetRepoActionState(GetRepoActionState),
|
||||
ListRepos(ListRepos),
|
||||
ListFullRepos(ListFullRepos),
|
||||
|
||||
// ==== PROCEDURE ====
|
||||
GetProceduresSummary(GetProceduresSummary),
|
||||
GetProcedure(GetProcedure),
|
||||
GetProcedureActionState(GetProcedureActionState),
|
||||
ListProcedures(ListProcedures),
|
||||
ListFullProcedures(ListFullProcedures),
|
||||
|
||||
// ==== ACTION ====
|
||||
GetActionsSummary(GetActionsSummary),
|
||||
GetAction(GetAction),
|
||||
GetActionActionState(GetActionActionState),
|
||||
ListActions(ListActions),
|
||||
ListFullActions(ListFullActions),
|
||||
|
||||
// ==== SCHEDULE ====
|
||||
ListSchedules(ListSchedules),
|
||||
|
||||
// ==== SYNC ====
|
||||
GetResourceSyncsSummary(GetResourceSyncsSummary),
|
||||
GetResourceSync(GetResourceSync),
|
||||
GetResourceSyncActionState(GetResourceSyncActionState),
|
||||
ListResourceSyncs(ListResourceSyncs),
|
||||
ListFullResourceSyncs(ListFullResourceSyncs),
|
||||
|
||||
// ==== BUILDER ====
|
||||
GetBuildersSummary(GetBuildersSummary),
|
||||
GetBuilder(GetBuilder),
|
||||
ListBuilders(ListBuilders),
|
||||
ListFullBuilders(ListFullBuilders),
|
||||
|
||||
// ==== ALERTER ====
|
||||
GetAlertersSummary(GetAlertersSummary),
|
||||
GetAlerter(GetAlerter),
|
||||
ListAlerters(ListAlerters),
|
||||
ListFullAlerters(ListFullAlerters),
|
||||
|
||||
// ==== TOML ====
|
||||
ExportAllResourcesToToml(ExportAllResourcesToToml),
|
||||
ExportResourcesToToml(ExportResourcesToToml),
|
||||
|
||||
// ==== TAG ====
|
||||
GetTag(GetTag),
|
||||
ListTags(ListTags),
|
||||
|
||||
// ==== USER ====
|
||||
GetUsername(GetUsername),
|
||||
GetPermission(GetPermission),
|
||||
FindUser(FindUser),
|
||||
ListUsers(ListUsers),
|
||||
ListApiKeys(ListApiKeys),
|
||||
ListApiKeysForServiceUser(ListApiKeysForServiceUser),
|
||||
ListPermissions(ListPermissions),
|
||||
ListUserTargetPermissions(ListUserTargetPermissions),
|
||||
|
||||
// ==== USER GROUP ====
|
||||
GetUserGroup(GetUserGroup),
|
||||
ListUserGroups(ListUserGroups),
|
||||
|
||||
// ==== UPDATE ====
|
||||
GetUpdate(GetUpdate),
|
||||
ListUpdates(ListUpdates),
|
||||
|
||||
// ==== ALERT ====
|
||||
ListAlerts(ListAlerts),
|
||||
GetAlert(GetAlert),
|
||||
|
||||
// ==== VARIABLE ====
|
||||
GetVariable(GetVariable),
|
||||
ListVariables(ListVariables),
|
||||
|
||||
// ==== PROVIDER ====
|
||||
GetGitProviderAccount(GetGitProviderAccount),
|
||||
ListGitProviderAccounts(ListGitProviderAccounts),
|
||||
GetDockerRegistryAccount(GetDockerRegistryAccount),
|
||||
ListDockerRegistryAccounts(ListDockerRegistryAccounts),
|
||||
|
||||
// ==== ONBOARDING KEY ====
|
||||
ListOnboardingKeys(ListOnboardingKeys),
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.route("/{variant}", post(variant_handler))
|
||||
.layer(middleware::from_fn(
|
||||
authenticate_request::<KomodoAuthImpl, true>,
|
||||
))
|
||||
}
|
||||
|
||||
async fn variant_handler(
|
||||
user: Extension<User>,
|
||||
Path(Variant { variant }): Path<Variant>,
|
||||
Json(params): Json<serde_json::Value>,
|
||||
) -> mogh_error::Result<axum::response::Response> {
|
||||
let req: ReadRequest = serde_json::from_value(json!({
|
||||
"type": variant,
|
||||
"params": params,
|
||||
}))?;
|
||||
handler(user, Json(req)).await
|
||||
}
|
||||
|
||||
async fn handler(
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<ReadRequest>,
|
||||
) -> mogh_error::Result<axum::response::Response> {
|
||||
let req_id = Uuid::new_v4();
|
||||
let variant: ReadRequestVariant = (&request).into();
|
||||
|
||||
debug!(
|
||||
"READ REQUEST {req_id} | METHOD: {variant} | USER: {} ({})",
|
||||
user.username, user.id
|
||||
);
|
||||
|
||||
let res = request.resolve(&ReadArgs { user }).await;
|
||||
|
||||
if let Err(e) = &res {
|
||||
debug!(
|
||||
"READ REQUEST {req_id} | METHOD: {variant} | ERROR: {:#}",
|
||||
e.error
|
||||
);
|
||||
}
|
||||
|
||||
res.map(|res| res.0)
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetVersion {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &ReadArgs,
|
||||
) -> mogh_error::Result<GetVersionResponse> {
|
||||
Ok(GetVersionResponse {
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<ReadArgs> for GetCoreInfo {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &ReadArgs,
|
||||
) -> mogh_error::Result<GetCoreInfoResponse> {
|
||||
let config = core_config();
|
||||
let info = GetCoreInfoResponse {
|
||||
title: config.title.clone(),
|
||||
monitoring_interval: config.monitoring_interval,
|
||||
webhook_base_url: if config.webhook_base_url.is_empty() {
|
||||
config.host.clone()
|
||||
} else {
|
||||
config.webhook_base_url.clone()
|
||||
},
|
||||
transparent_mode: config.transparent_mode,
|
||||
ui_write_disabled: config.ui_write_disabled,
|
||||
disable_confirm_dialog: config.disable_confirm_dialog,
|
||||
disable_non_admin_create: config.disable_non_admin_create,
|
||||
disable_websocket_reconnect: config.disable_websocket_reconnect,
|
||||
enable_fancy_toml: config.enable_fancy_toml,
|
||||
timezone: config.timezone.clone(),
|
||||
public_key: core_keys().load().public.to_string(),
|
||||
};
|
||||
Ok(info)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<ReadArgs> for ListSecrets {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &ReadArgs,
|
||||
) -> mogh_error::Result<ListSecretsResponse> {
|
||||
let mut secrets = core_config()
|
||||
.secrets
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
if let Some(target) = self.target {
|
||||
let server_id = match target {
|
||||
ResourceTarget::Server(id) => Some(id),
|
||||
ResourceTarget::Builder(id) => {
|
||||
match resource::get::<Builder>(&id).await?.config {
|
||||
BuilderConfig::Url(_) => None,
|
||||
BuilderConfig::Server(config) => Some(config.server_id),
|
||||
BuilderConfig::Aws(config) => {
|
||||
secrets.extend(config.secrets);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(
|
||||
anyhow!("target must be `Server` or `Builder`")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
};
|
||||
if let Some(id) = server_id {
|
||||
let server = resource::get::<Server>(&id).await?;
|
||||
let more = periphery_client(&server)
|
||||
.await?
|
||||
.request(periphery_client::api::ListSecrets {})
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to get secrets from server {}",
|
||||
server.name
|
||||
)
|
||||
})?;
|
||||
secrets.extend(more);
|
||||
}
|
||||
}
|
||||
|
||||
let mut secrets = secrets.into_iter().collect::<Vec<_>>();
|
||||
secrets.sort();
|
||||
|
||||
Ok(secrets)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListGitProvidersFromConfigResponse> {
|
||||
let mut providers = core_config().git_providers.clone();
|
||||
|
||||
if let Some(target) = self.target {
|
||||
match target {
|
||||
ResourceTarget::Server(id) => {
|
||||
merge_git_providers_for_server(&mut providers, &id).await?;
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
match resource::get::<Builder>(&id).await?.config {
|
||||
BuilderConfig::Url(_) => {}
|
||||
BuilderConfig::Server(config) => {
|
||||
merge_git_providers_for_server(
|
||||
&mut providers,
|
||||
&config.server_id,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
BuilderConfig::Aws(config) => {
|
||||
merge_git_providers(
|
||||
&mut providers,
|
||||
config.git_providers,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(
|
||||
anyhow!("target must be `Server` or `Builder`")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (builds, repos, syncs) = tokio::try_join!(
|
||||
resource::list_full_for_user::<Build>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[]
|
||||
),
|
||||
resource::list_full_for_user::<Repo>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[]
|
||||
),
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[]
|
||||
),
|
||||
)?;
|
||||
|
||||
for build in builds {
|
||||
if !providers
|
||||
.iter()
|
||||
.any(|provider| provider.domain == build.config.git_provider)
|
||||
{
|
||||
providers.push(GitProvider {
|
||||
domain: build.config.git_provider,
|
||||
https: build.config.git_https,
|
||||
accounts: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
for repo in repos {
|
||||
if !providers
|
||||
.iter()
|
||||
.any(|provider| provider.domain == repo.config.git_provider)
|
||||
{
|
||||
providers.push(GitProvider {
|
||||
domain: repo.config.git_provider,
|
||||
https: repo.config.git_https,
|
||||
accounts: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
for sync in syncs {
|
||||
if !providers
|
||||
.iter()
|
||||
.any(|provider| provider.domain == sync.config.git_provider)
|
||||
{
|
||||
providers.push(GitProvider {
|
||||
domain: sync.config.git_provider,
|
||||
https: sync.config.git_https,
|
||||
accounts: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
providers.sort();
|
||||
|
||||
Ok(providers)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<ReadArgs> for ListDockerRegistriesFromConfig {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &ReadArgs,
|
||||
) -> mogh_error::Result<ListDockerRegistriesFromConfigResponse> {
|
||||
let mut registries = core_config().docker_registries.clone();
|
||||
|
||||
if let Some(target) = self.target {
|
||||
match target {
|
||||
ResourceTarget::Server(id) => {
|
||||
merge_docker_registries_for_server(&mut registries, &id)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
match resource::get::<Builder>(&id).await?.config {
|
||||
BuilderConfig::Url(_) => {}
|
||||
BuilderConfig::Server(config) => {
|
||||
merge_docker_registries_for_server(
|
||||
&mut registries,
|
||||
&config.server_id,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
BuilderConfig::Aws(config) => {
|
||||
merge_docker_registries(
|
||||
&mut registries,
|
||||
config.docker_registries,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(
|
||||
anyhow!("target must be `Server` or `Builder`").into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registries.sort();
|
||||
|
||||
Ok(registries)
|
||||
}
|
||||
}
|
||||
|
||||
async fn merge_git_providers_for_server(
|
||||
providers: &mut Vec<GitProvider>,
|
||||
server_id: &str,
|
||||
) -> mogh_error::Result<()> {
|
||||
let server = resource::get::<Server>(server_id).await?;
|
||||
let more = periphery_client(&server)
|
||||
.await?
|
||||
.request(periphery_client::api::ListGitProviders {})
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to get git providers from server {}",
|
||||
server.name
|
||||
)
|
||||
})?;
|
||||
merge_git_providers(providers, more);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn merge_git_providers(
|
||||
providers: &mut Vec<GitProvider>,
|
||||
more: Vec<GitProvider>,
|
||||
) {
|
||||
for incoming_provider in more {
|
||||
if let Some(provider) = providers
|
||||
.iter_mut()
|
||||
.find(|provider| provider.domain == incoming_provider.domain)
|
||||
{
|
||||
for account in incoming_provider.accounts {
|
||||
if !provider.accounts.contains(&account) {
|
||||
provider.accounts.push(account);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
providers.push(incoming_provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn merge_docker_registries_for_server(
|
||||
registries: &mut Vec<DockerRegistry>,
|
||||
server_id: &str,
|
||||
) -> mogh_error::Result<()> {
|
||||
let server = resource::get::<Server>(server_id).await?;
|
||||
let more = periphery_client(&server)
|
||||
.await?
|
||||
.request(periphery_client::api::ListDockerRegistries {})
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to get docker registries from server {}",
|
||||
server.name
|
||||
)
|
||||
})?;
|
||||
merge_docker_registries(registries, more);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn merge_docker_registries(
|
||||
registries: &mut Vec<DockerRegistry>,
|
||||
more: Vec<DockerRegistry>,
|
||||
) {
|
||||
for incoming_registry in more {
|
||||
if let Some(registry) = registries
|
||||
.iter_mut()
|
||||
.find(|registry| registry.domain == incoming_registry.domain)
|
||||
{
|
||||
for account in incoming_registry.accounts {
|
||||
if !registry.accounts.contains(&account) {
|
||||
registry.accounts.push(account);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
registries.push(incoming_registry);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
bin/core/src/api/read/onboarding_key.rs
Normal file
51
bin/core/src/api/read/onboarding_key.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::find::find_collect;
|
||||
use komodo_client::api::read::{
|
||||
ListOnboardingKeys, ListOnboardingKeysResponse,
|
||||
};
|
||||
use mogh_error::AddStatusCodeError;
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
use crate::{api::read::ReadArgs, state::db_client};
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<ReadArgs> for ListOnboardingKeys {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user: admin }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListOnboardingKeysResponse> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This call is admin only")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let mut keys =
|
||||
find_collect(&db_client().onboarding_keys, None, None)
|
||||
.await
|
||||
.context(
|
||||
"Failed to query database for Server onboarding keys",
|
||||
)?;
|
||||
|
||||
// No expiry keys first, followed
|
||||
keys.sort_by(|a, b| {
|
||||
if a.expires == b.expires {
|
||||
Ordering::Equal
|
||||
} else if a.expires == 0 {
|
||||
Ordering::Less
|
||||
} else if b.expires == 0 {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
// Descending
|
||||
b.expires.cmp(&a.expires)
|
||||
}
|
||||
});
|
||||
|
||||
Ok(keys)
|
||||
}
|
||||
}
|
||||
71
bin/core/src/api/read/permission.rs
Normal file
71
bin/core/src/api/read/permission.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use komodo_client::{
|
||||
api::read::{
|
||||
GetPermission, GetPermissionResponse, ListPermissions,
|
||||
ListPermissionsResponse, ListUserTargetPermissions,
|
||||
ListUserTargetPermissionsResponse,
|
||||
},
|
||||
entities::permission::PermissionLevel,
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_user_permission_on_target, state::db_client,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for ListPermissions {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListPermissionsResponse> {
|
||||
let res = find_collect(
|
||||
&db_client().permissions,
|
||||
doc! {
|
||||
"user_target.type": "User",
|
||||
"user_target.id": &user.id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to query db for permissions")?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetPermission {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetPermissionResponse> {
|
||||
if user.admin {
|
||||
return Ok(PermissionLevel::Write.all());
|
||||
}
|
||||
Ok(get_user_permission_on_target(user, &self.target).await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListUserTargetPermissions {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListUserTargetPermissionsResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("this method is admin only").into());
|
||||
}
|
||||
let (variant, id) = self.user_target.extract_variant_id();
|
||||
let res = find_collect(
|
||||
&db_client().permissions,
|
||||
doc! {
|
||||
"user_target.type": variant.as_ref(),
|
||||
"user_target.id": id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to query db for permissions")?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
145
bin/core/src/api/read/procedure.rs
Normal file
145
bin/core/src/api/read/procedure.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use anyhow::Context;
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
permission::PermissionLevel,
|
||||
procedure::{Procedure, ProcedureState},
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{action_states, procedure_state_cache},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetProcedure {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetProcedureResponse> {
|
||||
Ok(
|
||||
get_check_permissions::<Procedure>(
|
||||
&self.procedure,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListProcedures {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListProceduresResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Procedure>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListFullProcedures {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListFullProceduresResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Procedure>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetProceduresSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetProceduresSummaryResponse> {
|
||||
let procedures = resource::list_full_for_user::<Procedure>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get procedures from db")?;
|
||||
|
||||
let mut res = GetProceduresSummaryResponse::default();
|
||||
|
||||
let cache = procedure_state_cache();
|
||||
let action_states = action_states();
|
||||
|
||||
for procedure in procedures {
|
||||
res.total += 1;
|
||||
|
||||
match (
|
||||
cache.get(&procedure.id).await.unwrap_or_default(),
|
||||
action_states
|
||||
.procedure
|
||||
.get(&procedure.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?,
|
||||
) {
|
||||
(_, action_states) if action_states.running => {
|
||||
res.running += 1;
|
||||
}
|
||||
(ProcedureState::Ok, _) => res.ok += 1,
|
||||
(ProcedureState::Failed, _) => res.failed += 1,
|
||||
(ProcedureState::Unknown, _) => res.unknown += 1,
|
||||
// will never come off the cache in the running state, since that comes from action states
|
||||
(ProcedureState::Running, _) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetProcedureActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetProcedureActionStateResponse> {
|
||||
let procedure = get_check_permissions::<Procedure>(
|
||||
&self.procedure,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.procedure
|
||||
.get(&procedure.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
115
bin/core/src/api/read/provider.rs
Normal file
115
bin/core/src/api/read/provider.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mongo_indexed::{Document, doc};
|
||||
use database::mungos::{
|
||||
by_id::find_one_by_id, find::find_collect,
|
||||
mongodb::options::FindOptions,
|
||||
};
|
||||
use komodo_client::api::read::*;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::state::db_client;
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetGitProviderAccount {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetGitProviderAccountResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can read git provider accounts").into(),
|
||||
);
|
||||
}
|
||||
let res = find_one_by_id(&db_client().git_accounts, &self.id)
|
||||
.await
|
||||
.context("failed to query db for git provider accounts")?
|
||||
.context(
|
||||
"did not find git provider account with the given id",
|
||||
)?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListGitProviderAccounts {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListGitProviderAccountsResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can read git provider accounts").into(),
|
||||
);
|
||||
}
|
||||
let mut filter = Document::new();
|
||||
if let Some(domain) = self.domain {
|
||||
filter.insert("domain", domain);
|
||||
}
|
||||
if let Some(username) = self.username {
|
||||
filter.insert("username", username);
|
||||
}
|
||||
let res = find_collect(
|
||||
&db_client().git_accounts,
|
||||
filter,
|
||||
FindOptions::builder()
|
||||
.sort(doc! { "domain": 1, "username": 1 })
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.context("failed to query db for git provider accounts")?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetDockerRegistryAccount {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetDockerRegistryAccountResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can read docker registry accounts")
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let res =
|
||||
find_one_by_id(&db_client().registry_accounts, &self.id)
|
||||
.await
|
||||
.context("failed to query db for docker registry accounts")?
|
||||
.context(
|
||||
"did not find docker registry account with the given id",
|
||||
)?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListDockerRegistryAccounts {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListDockerRegistryAccountsResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can read docker registry accounts")
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let mut filter = Document::new();
|
||||
if let Some(domain) = self.domain {
|
||||
filter.insert("domain", domain);
|
||||
}
|
||||
if let Some(username) = self.username {
|
||||
filter.insert("username", username);
|
||||
}
|
||||
let res = find_collect(
|
||||
&db_client().registry_accounts,
|
||||
filter,
|
||||
FindOptions::builder()
|
||||
.sort(doc! { "domain": 1, "username": 1 })
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.context("failed to query db for docker registry accounts")?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
159
bin/core/src/api/read/repo.rs
Normal file
159
bin/core/src/api/read/repo.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use anyhow::Context;
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
permission::PermissionLevel,
|
||||
repo::{Repo, RepoActionState, RepoListItem, RepoState},
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{action_states, repo_state_cache},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetRepo {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Repo> {
|
||||
Ok(
|
||||
get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListRepos {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Vec<RepoListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Repo>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListFullRepos {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListFullReposResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Repo>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetRepoActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<RepoActionState> {
|
||||
let repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.repo
|
||||
.get(&repo.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetReposSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetReposSummaryResponse> {
|
||||
let repos = resource::list_full_for_user::<Repo>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get repos from db")?;
|
||||
|
||||
let mut res = GetReposSummaryResponse::default();
|
||||
|
||||
let cache = repo_state_cache();
|
||||
let action_states = action_states();
|
||||
|
||||
for repo in repos {
|
||||
res.total += 1;
|
||||
|
||||
match (
|
||||
cache.get(&repo.id).await.unwrap_or_default(),
|
||||
action_states
|
||||
.repo
|
||||
.get(&repo.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?,
|
||||
) {
|
||||
(_, action_states) if action_states.cloning => {
|
||||
res.cloning += 1;
|
||||
}
|
||||
(_, action_states) if action_states.pulling => {
|
||||
res.pulling += 1;
|
||||
}
|
||||
(_, action_states) if action_states.building => {
|
||||
res.building += 1;
|
||||
}
|
||||
(RepoState::Ok, _) => res.ok += 1,
|
||||
(RepoState::Failed, _) => res.failed += 1,
|
||||
(RepoState::Unknown, _) => {
|
||||
if !repo.template {
|
||||
res.unknown += 1
|
||||
}
|
||||
}
|
||||
// will never come off the cache in the building state, since that comes from action states
|
||||
(RepoState::Cloning, _)
|
||||
| (RepoState::Pulling, _)
|
||||
| (RepoState::Building, _) => {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
107
bin/core/src/api/read/schedule.rs
Normal file
107
bin/core/src/api/read/schedule.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use futures_util::future::join_all;
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
ResourceTarget,
|
||||
action::Action,
|
||||
permission::PermissionLevel,
|
||||
procedure::Procedure,
|
||||
resource::{ResourceQuery, TemplatesQueryBehavior},
|
||||
schedule::Schedule,
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::{get_all_tags, get_last_run_at},
|
||||
resource::list_full_for_user,
|
||||
schedule::get_schedule_item_info,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for ListSchedules {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &ReadArgs,
|
||||
) -> mogh_error::Result<Vec<Schedule>> {
|
||||
let all_tags = get_all_tags(None).await?;
|
||||
let (actions, procedures) = tokio::try_join!(
|
||||
list_full_for_user::<Action>(
|
||||
ResourceQuery {
|
||||
names: Default::default(),
|
||||
templates: TemplatesQueryBehavior::Include,
|
||||
tag_behavior: self.tag_behavior,
|
||||
tags: self.tags.clone(),
|
||||
specific: Default::default(),
|
||||
},
|
||||
&args.user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
),
|
||||
list_full_for_user::<Procedure>(
|
||||
ResourceQuery {
|
||||
names: Default::default(),
|
||||
templates: TemplatesQueryBehavior::Include,
|
||||
tag_behavior: self.tag_behavior,
|
||||
tags: self.tags.clone(),
|
||||
specific: Default::default(),
|
||||
},
|
||||
&args.user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
)?;
|
||||
let actions = actions.into_iter().map(async |action| {
|
||||
let (next_scheduled_run, schedule_error) =
|
||||
get_schedule_item_info(&ResourceTarget::Action(
|
||||
action.id.clone(),
|
||||
));
|
||||
let last_run_at =
|
||||
get_last_run_at::<Action>(&action.id).await.unwrap_or(None);
|
||||
Schedule {
|
||||
target: ResourceTarget::Action(action.id),
|
||||
name: action.name,
|
||||
enabled: action.config.schedule_enabled,
|
||||
schedule_format: action.config.schedule_format,
|
||||
schedule: action.config.schedule,
|
||||
schedule_timezone: action.config.schedule_timezone,
|
||||
tags: action.tags,
|
||||
last_run_at,
|
||||
next_scheduled_run,
|
||||
schedule_error,
|
||||
}
|
||||
});
|
||||
let procedures = procedures.into_iter().map(async |procedure| {
|
||||
let (next_scheduled_run, schedule_error) =
|
||||
get_schedule_item_info(&ResourceTarget::Procedure(
|
||||
procedure.id.clone(),
|
||||
));
|
||||
let last_run_at = get_last_run_at::<Procedure>(&procedure.id)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
Schedule {
|
||||
target: ResourceTarget::Procedure(procedure.id),
|
||||
name: procedure.name,
|
||||
enabled: procedure.config.schedule_enabled,
|
||||
schedule_format: procedure.config.schedule_format,
|
||||
schedule: procedure.config.schedule,
|
||||
schedule_timezone: procedure.config.schedule_timezone,
|
||||
tags: procedure.tags,
|
||||
last_run_at,
|
||||
next_scheduled_run,
|
||||
schedule_error,
|
||||
}
|
||||
});
|
||||
let (actions, procedures) =
|
||||
tokio::join!(join_all(actions), join_all(procedures));
|
||||
|
||||
Ok(
|
||||
actions
|
||||
.into_iter()
|
||||
.chain(procedures)
|
||||
.filter(|s| !s.schedule.is_empty())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
879
bin/core/src/api/read/server.rs
Normal file
879
bin/core/src/api/read/server.rs
Normal file
@@ -0,0 +1,879 @@
|
||||
use std::{
|
||||
cmp,
|
||||
collections::HashMap,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::{
|
||||
FIFTEEN_SECONDS_MS, get_timelength_in_ms, unix_timestamp_ms,
|
||||
};
|
||||
use database::mungos::{
|
||||
find::find_collect,
|
||||
mongodb::{bson::doc, options::FindOptions},
|
||||
};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
ResourceTarget,
|
||||
deployment::Deployment,
|
||||
docker::{
|
||||
container::{
|
||||
Container, ContainerListItem, ContainerStateStatusEnum,
|
||||
},
|
||||
image::{Image, ImageHistoryResponseItem},
|
||||
network::Network,
|
||||
volume::Volume,
|
||||
},
|
||||
permission::PermissionLevel,
|
||||
server::{
|
||||
Server, ServerActionState, ServerListItem, ServerQuery,
|
||||
ServerState,
|
||||
},
|
||||
stack::{Stack, StackServiceNames},
|
||||
stats::{SystemInformation, SystemProcess},
|
||||
update::Log,
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCode;
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api::{
|
||||
self as periphery,
|
||||
container::InspectContainer,
|
||||
docker::{
|
||||
ImageHistory, InspectImage, InspectNetwork, InspectVolume,
|
||||
},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
helpers::{periphery_client, query::get_all_tags},
|
||||
permission::{get_check_permissions, list_resources_for_user},
|
||||
resource,
|
||||
stack::compose_container_match_regex,
|
||||
state::{action_states, db_client, server_status_cache},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetServersSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetServersSummaryResponse> {
|
||||
let servers = resource::list_for_user::<Server>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let core_version = env!("CARGO_PKG_VERSION");
|
||||
let mut res = GetServersSummaryResponse::default();
|
||||
|
||||
for server in servers {
|
||||
res.total += 1;
|
||||
match server.info.state {
|
||||
ServerState::Ok => {
|
||||
// Check for version mismatch
|
||||
if matches!(&server.info.version, Some(version) if version != core_version)
|
||||
{
|
||||
res.warning += 1;
|
||||
} else {
|
||||
res.healthy += 1;
|
||||
}
|
||||
}
|
||||
ServerState::NotOk => {
|
||||
res.unhealthy += 1;
|
||||
}
|
||||
ServerState::Disabled => {
|
||||
if !server.template {
|
||||
res.disabled += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetServer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Server> {
|
||||
Ok(
|
||||
get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListServers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Vec<ServerListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Server>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListFullServers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListFullServersResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Server>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetServerState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetServerStateResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let status = server_status_cache()
|
||||
.get(&server.id)
|
||||
.await
|
||||
.ok_or(anyhow!("did not find cached status for server"))?;
|
||||
let response = GetServerStateResponse {
|
||||
status: status.state,
|
||||
};
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetServerActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ServerActionState> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.server
|
||||
.get(&server.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetPeripheryInformation {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetPeripheryInformationResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
server_status_cache()
|
||||
.get(&server.id)
|
||||
.await
|
||||
.context("Missing server status")?
|
||||
.periphery_info
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.context("Server status missing Periphery Info. The Server may be disconnected.")
|
||||
.status_code(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetSystemInformation {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<SystemInformation> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
server_status_cache()
|
||||
.get(&server.id)
|
||||
.await
|
||||
.context("Missing server status")?
|
||||
.system_info
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.context("Server status missing system Info. The Server may be disconnected.")
|
||||
.status_code(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetSystemStats {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetSystemStatsResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
server_status_cache()
|
||||
.get(&server.id)
|
||||
.await
|
||||
.context("Missing server status")?
|
||||
.system_stats
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.context("Server status missing system stats. The Server may be disconnected.")
|
||||
.status_code(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
// This protects the peripheries from spam requests
|
||||
const PROCESSES_EXPIRY: u128 = FIFTEEN_SECONDS_MS;
|
||||
type ProcessesCache =
|
||||
Mutex<HashMap<String, Arc<(Vec<SystemProcess>, u128)>>>;
|
||||
fn processes_cache() -> &'static ProcessesCache {
|
||||
static PROCESSES_CACHE: OnceLock<ProcessesCache> = OnceLock::new();
|
||||
PROCESSES_CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSystemProcesses {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListSystemProcessesResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.processes(),
|
||||
)
|
||||
.await?;
|
||||
let mut lock = processes_cache().lock().await;
|
||||
let res = match lock.get(&server.id) {
|
||||
Some(cached) if cached.1 > unix_timestamp_ms() => {
|
||||
cached.0.clone()
|
||||
}
|
||||
_ => {
|
||||
let stats = periphery_client(&server)
|
||||
.await?
|
||||
.request(periphery::stats::GetSystemProcesses {})
|
||||
.await?;
|
||||
lock.insert(
|
||||
server.id,
|
||||
(stats.clone(), unix_timestamp_ms() + PROCESSES_EXPIRY)
|
||||
.into(),
|
||||
);
|
||||
stats
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
const STATS_PER_PAGE: i64 = 200;
|
||||
|
||||
impl Resolve<ReadArgs> for GetHistoricalServerStats {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetHistoricalServerStatsResponse> {
|
||||
let GetHistoricalServerStats {
|
||||
server,
|
||||
granularity,
|
||||
page,
|
||||
} = self;
|
||||
let server = get_check_permissions::<Server>(
|
||||
&server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let granularity =
|
||||
get_timelength_in_ms(granularity.to_string().parse().unwrap())
|
||||
as i64;
|
||||
let mut ts_vec = Vec::<i64>::new();
|
||||
let curr_ts = unix_timestamp_ms() as i64;
|
||||
let mut curr_ts = curr_ts
|
||||
- curr_ts % granularity
|
||||
- granularity * STATS_PER_PAGE * page as i64;
|
||||
for _ in 0..STATS_PER_PAGE {
|
||||
ts_vec.push(curr_ts);
|
||||
curr_ts -= granularity;
|
||||
}
|
||||
|
||||
let stats = find_collect(
|
||||
&db_client().stats,
|
||||
doc! {
|
||||
"sid": server.id,
|
||||
"ts": { "$in": ts_vec },
|
||||
},
|
||||
FindOptions::builder()
|
||||
.sort(doc! { "ts": -1 })
|
||||
.skip(page as u64 * STATS_PER_PAGE as u64)
|
||||
.limit(STATS_PER_PAGE)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.context("failed to pull stats from db")?;
|
||||
let next_page = if stats.len() == STATS_PER_PAGE as usize {
|
||||
Some(page + 1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let res = GetHistoricalServerStatsResponse { stats, next_page };
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListDockerContainers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListDockerContainersResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if let Some(docker) = &cache.docker {
|
||||
Ok(docker.containers.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListAllDockerContainers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListAllDockerContainersResponse> {
|
||||
let servers = resource::list_for_user::<Server>(
|
||||
ServerQuery::builder().names(self.servers.clone()).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut containers = Vec::<ContainerListItem>::new();
|
||||
|
||||
for server in servers {
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
let Some(docker) = &cache.docker else {
|
||||
continue;
|
||||
};
|
||||
let more = docker
|
||||
.containers
|
||||
.iter()
|
||||
.filter(|container| {
|
||||
self.containers.is_empty()
|
||||
|| self.containers.contains(&container.name)
|
||||
})
|
||||
.cloned();
|
||||
containers.extend(more);
|
||||
}
|
||||
|
||||
Ok(containers)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetDockerContainersSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetDockerContainersSummaryResponse> {
|
||||
let servers = resource::list_full_for_user::<Server>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get servers from db")?;
|
||||
|
||||
let mut res = GetDockerContainersSummaryResponse::default();
|
||||
|
||||
for server in servers {
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
|
||||
if let Some(docker) = &cache.docker {
|
||||
for container in &docker.containers {
|
||||
res.total += 1;
|
||||
match container.state {
|
||||
ContainerStateStatusEnum::Created
|
||||
| ContainerStateStatusEnum::Paused
|
||||
| ContainerStateStatusEnum::Exited => res.stopped += 1,
|
||||
ContainerStateStatusEnum::Running => res.running += 1,
|
||||
ContainerStateStatusEnum::Empty => res.unknown += 1,
|
||||
_ => res.unhealthy += 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectDockerContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Container> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if cache.state != ServerState::Ok {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"Cannot inspect container: server is {:?}",
|
||||
cache.state
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(InspectContainer {
|
||||
name: self.container,
|
||||
})
|
||||
.await?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_LOG_LENGTH: u64 = 5000;
|
||||
|
||||
impl Resolve<ReadArgs> for GetContainerLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Log> {
|
||||
let GetContainerLog {
|
||||
server,
|
||||
container,
|
||||
tail,
|
||||
timestamps,
|
||||
} = self;
|
||||
let server = get_check_permissions::<Server>(
|
||||
&server,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(periphery::container::GetContainerLog {
|
||||
name: container,
|
||||
tail: cmp::min(tail, MAX_LOG_LENGTH),
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed at call to periphery")?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for SearchContainerLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Log> {
|
||||
let SearchContainerLog {
|
||||
server,
|
||||
container,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
} = self;
|
||||
let server = get_check_permissions::<Server>(
|
||||
&server,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(periphery::container::GetContainerLogSearch {
|
||||
name: container,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed at call to periphery")?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetResourceMatchingContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetResourceMatchingContainerResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
// first check deployments
|
||||
if let Ok(deployment) =
|
||||
resource::get::<Deployment>(&self.container).await
|
||||
{
|
||||
return Ok(GetResourceMatchingContainerResponse {
|
||||
resource: ResourceTarget::Deployment(deployment.id).into(),
|
||||
});
|
||||
}
|
||||
|
||||
// then check stacks
|
||||
let stacks = list_resources_for_user::<Stack>(
|
||||
doc! { "config.server_id": &server.id },
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// check matching stack
|
||||
for stack in stacks {
|
||||
for StackServiceNames {
|
||||
service_name,
|
||||
container_name,
|
||||
..
|
||||
} in stack
|
||||
.info
|
||||
.deployed_services
|
||||
.unwrap_or(stack.info.latest_services)
|
||||
{
|
||||
let is_match = match compose_container_match_regex(&container_name)
|
||||
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
|
||||
{
|
||||
Ok(regex) => regex,
|
||||
Err(e) => {
|
||||
warn!("{e:#}");
|
||||
continue;
|
||||
}
|
||||
}.is_match(&self.container);
|
||||
|
||||
if is_match {
|
||||
return Ok(GetResourceMatchingContainerResponse {
|
||||
resource: ResourceTarget::Stack(stack.id).into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(GetResourceMatchingContainerResponse { resource: None })
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListDockerNetworks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListDockerNetworksResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if let Some(docker) = &cache.docker {
|
||||
Ok(docker.networks.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectDockerNetwork {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Network> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if cache.state != ServerState::Ok {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"Cannot inspect network: server is {:?}",
|
||||
cache.state
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(InspectNetwork { name: self.network })
|
||||
.await?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListDockerImages {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListDockerImagesResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if let Some(docker) = &cache.docker {
|
||||
Ok(docker.images.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectDockerImage {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Image> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if cache.state != ServerState::Ok {
|
||||
return Err(
|
||||
anyhow!("Cannot inspect image: server is {:?}", cache.state)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(InspectImage { name: self.image })
|
||||
.await?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListDockerImageHistory {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Vec<ImageHistoryResponseItem>> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if cache.state != ServerState::Ok {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"Cannot get image history: server is {:?}",
|
||||
cache.state
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(ImageHistory { name: self.image })
|
||||
.await?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListDockerVolumes {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListDockerVolumesResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if let Some(docker) = &cache.docker {
|
||||
Ok(docker.volumes.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectDockerVolume {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Volume> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if cache.state != ServerState::Ok {
|
||||
return Err(
|
||||
anyhow!("Cannot inspect volume: server is {:?}", cache.state)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(InspectVolume { name: self.volume })
|
||||
.await?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListComposeProjects {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListComposeProjectsResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if let Some(docker) = &cache.docker {
|
||||
Ok(docker.projects.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// impl Resolve<ReadArgs> for ListAllTerminals {
|
||||
// async fn resolve(
|
||||
// self,
|
||||
// args: &ReadArgs,
|
||||
// ) -> Result<Self::Response, Self::Error> {
|
||||
// // match self.tar
|
||||
// let mut terminals = resource::list_full_for_user::<Server>(
|
||||
// self.query, &args.user, &all_tags,
|
||||
// )
|
||||
// .await?
|
||||
// .into_iter()
|
||||
// .map(|server| async move {
|
||||
// (
|
||||
// list_terminals_inner(&server, self.fresh).await,
|
||||
// (server.id, server.name),
|
||||
// )
|
||||
// })
|
||||
// .collect::<FuturesUnordered<_>>()
|
||||
// .collect::<Vec<_>>()
|
||||
// .await
|
||||
// .into_iter()
|
||||
// .flat_map(|(terminals, server)| {
|
||||
// let terminals = terminals.ok()?;
|
||||
// Some((terminals, server))
|
||||
// })
|
||||
// .flat_map(|(terminals, (server_id, server_name))| {
|
||||
// terminals.into_iter().map(move |info| {
|
||||
// TerminalInfoWithServer::from_terminal_info(
|
||||
// &server_id,
|
||||
// &server_name,
|
||||
// info,
|
||||
// )
|
||||
// })
|
||||
// })
|
||||
// .collect::<Vec<_>>();
|
||||
|
||||
// terminals.sort_by(|a, b| {
|
||||
// a.server_name.cmp(&b.server_name).then(a.name.cmp(&b.name))
|
||||
// });
|
||||
|
||||
// Ok(terminals)
|
||||
// }
|
||||
// }
|
||||
513
bin/core/src/api/read/stack.rs
Normal file
513
bin/core/src/api/read/stack.rs
Normal file
@@ -0,0 +1,513 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
SwarmOrServer,
|
||||
docker::{
|
||||
container::Container, service::SwarmService, stack::SwarmStack,
|
||||
},
|
||||
permission::PermissionLevel,
|
||||
stack::{Stack, StackActionState, StackListItem, StackState},
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCodeError as _;
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api::{
|
||||
compose::{GetComposeLog, GetComposeLogSearch},
|
||||
container::InspectContainer,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
periphery_client, query::get_all_tags, swarm::swarm_request,
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
stack::setup_stack_execution,
|
||||
state::{action_states, stack_status_cache},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Stack> {
|
||||
Ok(
|
||||
get_check_permissions::<Stack>(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListStackServices {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListStackServicesResponse> {
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let services = stack_status_cache()
|
||||
.get(&stack.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.curr
|
||||
.services
|
||||
.clone();
|
||||
|
||||
Ok(services)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetStackLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetStackLogResponse> {
|
||||
let GetStackLog {
|
||||
stack,
|
||||
mut services,
|
||||
tail,
|
||||
timestamps,
|
||||
} = self;
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&stack,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::None => unreachable!(),
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
let service = services.pop().context(
|
||||
"Must pass single service for Swarm mode Stack logs",
|
||||
)?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLog {
|
||||
// The actual service name on swarm will be stackname_servicename
|
||||
service: format!(
|
||||
"{}_{service}",
|
||||
stack.project_name(false)
|
||||
),
|
||||
tail,
|
||||
timestamps,
|
||||
no_task_ids: false,
|
||||
no_resolve: false,
|
||||
details: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to get stack service log from swarm")?
|
||||
}
|
||||
SwarmOrServer::Server(server) => periphery_client(&server)
|
||||
.await?
|
||||
.request(GetComposeLog {
|
||||
project: stack.project_name(false),
|
||||
services,
|
||||
tail,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("Failed to get stack log from periphery")?,
|
||||
};
|
||||
|
||||
Ok(log)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for SearchStackLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<SearchStackLogResponse> {
|
||||
let SearchStackLog {
|
||||
stack,
|
||||
mut services,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
} = self;
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&stack,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::None => unreachable!(),
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
let service = services.pop().context(
|
||||
"Must pass single service for Swarm mode Stack logs",
|
||||
)?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLogSearch {
|
||||
service,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
no_task_ids: false,
|
||||
no_resolve: false,
|
||||
details: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to get stack service log from swarm")?
|
||||
}
|
||||
SwarmOrServer::Server(server) => periphery_client(&server)
|
||||
.await?
|
||||
.request(GetComposeLogSearch {
|
||||
project: stack.project_name(false),
|
||||
services,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("Failed to search stack log from periphery")?,
|
||||
};
|
||||
|
||||
Ok(log)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectStackContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Container> {
|
||||
let InspectStackContainer { stack, service } = self;
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&stack,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"InspectStackContainer should not be called for Stack in Swarm Mode"
|
||||
)
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
let services = &stack_status_cache()
|
||||
.get(&stack.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.curr
|
||||
.services;
|
||||
|
||||
let Some(name) = services
|
||||
.iter()
|
||||
.find(|s| s.service == service)
|
||||
.and_then(|s| s.container.as_ref().map(|c| c.name.clone()))
|
||||
else {
|
||||
return Err(anyhow!(
|
||||
"No service found matching '{service}'. Was the stack last deployed manually?"
|
||||
).into());
|
||||
};
|
||||
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(InspectContainer { name })
|
||||
.await
|
||||
.context("Failed to inspect container on server")?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectStackSwarmService {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<SwarmService> {
|
||||
let InspectStackSwarmService { stack, service } = self;
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&stack,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Swarm(swarm) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"InspectStackSwarmService should only be called for Stack in Swarm Mode"
|
||||
)
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
let services = &stack_status_cache()
|
||||
.get(&stack.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.curr
|
||||
.services;
|
||||
|
||||
let Some(service) = services
|
||||
.iter()
|
||||
.find(|s| s.service == service)
|
||||
.and_then(|s| {
|
||||
s.swarm_service.as_ref().and_then(|c| c.name.clone())
|
||||
})
|
||||
else {
|
||||
return Err(anyhow!(
|
||||
"No service found matching '{service}'. Was the stack last deployed manually?"
|
||||
).into());
|
||||
};
|
||||
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmService { service },
|
||||
)
|
||||
.await
|
||||
.context("Failed to inspect service on swarm")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectStackSwarmInfo {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<SwarmStack> {
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Swarm(swarm) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"InspectStackSwarmInfo should only be called for Stack in Swarm Mode"
|
||||
)
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmStack {
|
||||
stack: stack.project_name(false),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to inspect stack info on swarm")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListCommonStackExtraArgsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
let stacks = resource::list_full_for_user::<Stack>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await
|
||||
.context("Failed to get resources matching query")?;
|
||||
|
||||
// first collect with guaranteed uniqueness
|
||||
let mut res = HashSet::<String>::new();
|
||||
|
||||
for stack in stacks {
|
||||
for extra_arg in stack.config.extra_args {
|
||||
res.insert(extra_arg);
|
||||
}
|
||||
}
|
||||
|
||||
let mut res = res.into_iter().collect::<Vec<_>>();
|
||||
res.sort();
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListCommonStackBuildExtraArgsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
let stacks = resource::list_full_for_user::<Stack>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await
|
||||
.context("Failed to get resources matching query")?;
|
||||
|
||||
// first collect with guaranteed uniqueness
|
||||
let mut res = HashSet::<String>::new();
|
||||
|
||||
for stack in stacks {
|
||||
for extra_arg in stack.config.build_extra_args {
|
||||
res.insert(extra_arg);
|
||||
}
|
||||
}
|
||||
|
||||
let mut res = res.into_iter().collect::<Vec<_>>();
|
||||
res.sort();
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListStacks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Vec<StackListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
let only_update_available = self.query.specific.update_available;
|
||||
let stacks = resource::list_for_user::<Stack>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?;
|
||||
let stacks = if only_update_available {
|
||||
stacks
|
||||
.into_iter()
|
||||
.filter(|stack| {
|
||||
stack
|
||||
.info
|
||||
.services
|
||||
.iter()
|
||||
.any(|service| service.update_available)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
stacks
|
||||
};
|
||||
Ok(stacks)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListFullStacks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListFullStacksResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Stack>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetStackActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<StackActionState> {
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.stack
|
||||
.get(&stack.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetStacksSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetStacksSummaryResponse> {
|
||||
let stacks = resource::list_full_for_user::<Stack>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("Failed to get stacks from database")?;
|
||||
|
||||
let mut res = GetStacksSummaryResponse::default();
|
||||
|
||||
let cache = stack_status_cache();
|
||||
|
||||
for stack in stacks {
|
||||
res.total += 1;
|
||||
match cache.get(&stack.id).await.unwrap_or_default().curr.state
|
||||
{
|
||||
StackState::Running => res.running += 1,
|
||||
StackState::Stopped | StackState::Paused => res.stopped += 1,
|
||||
StackState::Down => res.down += 1,
|
||||
StackState::Unknown => {
|
||||
if !stack.template {
|
||||
res.unknown += 1
|
||||
}
|
||||
}
|
||||
_ => res.unhealthy += 1,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
522
bin/core/src/api/read/swarm.rs
Normal file
522
bin/core/src/api/read/swarm.rs
Normal file
@@ -0,0 +1,522 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
permission::PermissionLevel,
|
||||
swarm::{Swarm, SwarmActionState, SwarmListItem, SwarmState},
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{query::get_all_tags, swarm::swarm_request},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{action_states, server_status_cache, swarm_status_cache},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetSwarm {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Swarm> {
|
||||
Ok(
|
||||
get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarms {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Vec<SwarmListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Swarm>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListFullSwarms {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListFullSwarmsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Swarm>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetSwarmActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<SwarmActionState> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.swarm
|
||||
.get(&swarm.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetSwarmsSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetSwarmsSummaryResponse> {
|
||||
let swarms = resource::list_full_for_user::<Swarm>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get swarms from db")?;
|
||||
|
||||
let mut res = GetSwarmsSummaryResponse::default();
|
||||
|
||||
let cache = swarm_status_cache();
|
||||
|
||||
for swarm in swarms {
|
||||
res.total += 1;
|
||||
|
||||
match cache
|
||||
.get(&swarm.id)
|
||||
.await
|
||||
.map(|status| status.state)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
SwarmState::Unknown => {
|
||||
res.unknown += 1;
|
||||
}
|
||||
SwarmState::Healthy => {
|
||||
res.healthy += 1;
|
||||
}
|
||||
SwarmState::Unhealthy => {
|
||||
res.unhealthy += 1;
|
||||
}
|
||||
SwarmState::Down => {
|
||||
res.down += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectSwarm {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<InspectSwarmResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
let cache =
|
||||
swarm_status_cache().get_or_insert_default(&swarm.id).await;
|
||||
let inspect = cache
|
||||
.inspect
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.context("SwarmInspectInfo not available")?;
|
||||
Ok(inspect)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmNodes {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListSwarmNodesResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache =
|
||||
swarm_status_cache().get_or_insert_default(&swarm.id).await;
|
||||
if let Some(lists) = &cache.lists {
|
||||
Ok(lists.nodes.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectSwarmNode {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<InspectSwarmNodeResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmNode {
|
||||
node: self.node,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmServices {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListSwarmServicesResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache =
|
||||
swarm_status_cache().get_or_insert_default(&swarm.id).await;
|
||||
if let Some(lists) = &cache.lists {
|
||||
Ok(lists.services.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectSwarmService {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<InspectSwarmServiceResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmService {
|
||||
service: self.service,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetSwarmServiceLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetSwarmServiceLogResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLog {
|
||||
service: self.service,
|
||||
tail: self.tail,
|
||||
timestamps: self.timestamps,
|
||||
no_task_ids: self.no_task_ids,
|
||||
no_resolve: self.no_resolve,
|
||||
details: self.details,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for SearchSwarmServiceLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<SearchSwarmServiceLogResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLogSearch {
|
||||
service: self.service,
|
||||
terms: self.terms,
|
||||
combinator: self.combinator,
|
||||
invert: self.invert,
|
||||
timestamps: self.timestamps,
|
||||
no_task_ids: self.no_task_ids,
|
||||
no_resolve: self.no_resolve,
|
||||
details: self.details,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmTasks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListSwarmTasksResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache =
|
||||
swarm_status_cache().get_or_insert_default(&swarm.id).await;
|
||||
if let Some(lists) = &cache.lists {
|
||||
Ok(lists.tasks.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectSwarmTask {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<InspectSwarmTaskResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmTask {
|
||||
task: self.task,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmSecrets {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListSwarmSecretsResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache =
|
||||
swarm_status_cache().get_or_insert_default(&swarm.id).await;
|
||||
if let Some(lists) = &cache.lists {
|
||||
Ok(lists.secrets.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectSwarmSecret {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<InspectSwarmSecretResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmSecret {
|
||||
secret: self.secret,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmConfigs {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListSwarmConfigsResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache =
|
||||
swarm_status_cache().get_or_insert_default(&swarm.id).await;
|
||||
if let Some(lists) = &cache.lists {
|
||||
Ok(lists.configs.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectSwarmConfig {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<InspectSwarmConfigResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmConfig {
|
||||
config: self.config,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmStacks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListSwarmStacksResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache =
|
||||
swarm_status_cache().get_or_insert_default(&swarm.id).await;
|
||||
if let Some(lists) = &cache.lists {
|
||||
Ok(lists.stacks.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectSwarmStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<InspectSwarmStackResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmStack {
|
||||
stack: self.stack,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmNetworks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cache = server_status_cache();
|
||||
|
||||
for server_id in swarm.config.server_ids {
|
||||
let Some(status) = cache.get(&server_id).await else {
|
||||
continue;
|
||||
};
|
||||
let Some(docker) = &status.docker else {
|
||||
continue;
|
||||
};
|
||||
let networks = docker
|
||||
.networks
|
||||
.iter()
|
||||
.filter(|network| {
|
||||
network.driver.as_deref() == Some("overlay")
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
return Ok(networks);
|
||||
}
|
||||
|
||||
Err(
|
||||
anyhow!(
|
||||
"Failed to retrieve swarm networks from any manager node."
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
152
bin/core/src/api/read/sync.rs
Normal file
152
bin/core/src/api/read/sync.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use anyhow::Context;
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
permission::PermissionLevel,
|
||||
sync::{
|
||||
ResourceSync, ResourceSyncActionState, ResourceSyncListItem,
|
||||
},
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags, permission::get_check_permissions,
|
||||
resource, state::action_states,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetResourceSync {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ResourceSync> {
|
||||
Ok(
|
||||
get_check_permissions::<ResourceSync>(
|
||||
&self.sync,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListResourceSyncs {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Vec<ResourceSyncListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<ResourceSync>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListFullResourceSyncs {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListFullResourceSyncsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetResourceSyncActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ResourceSyncActionState> {
|
||||
let sync = get_check_permissions::<ResourceSync>(
|
||||
&self.sync,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.sync
|
||||
.get(&sync.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetResourceSyncsSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetResourceSyncsSummaryResponse> {
|
||||
let resource_syncs =
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get resource_syncs from db")?;
|
||||
|
||||
let mut res = GetResourceSyncsSummaryResponse::default();
|
||||
|
||||
let action_states = action_states();
|
||||
|
||||
for resource_sync in resource_syncs {
|
||||
res.total += 1;
|
||||
|
||||
if !(resource_sync.info.pending_deploy.to_deploy == 0
|
||||
&& resource_sync.info.resource_updates.is_empty()
|
||||
&& resource_sync.info.variable_updates.is_empty()
|
||||
&& resource_sync.info.user_group_updates.is_empty())
|
||||
{
|
||||
res.pending += 1;
|
||||
continue;
|
||||
} else if resource_sync.info.pending_error.is_some()
|
||||
|| !resource_sync.info.remote_errors.is_empty()
|
||||
{
|
||||
res.failed += 1;
|
||||
continue;
|
||||
}
|
||||
if action_states
|
||||
.sync
|
||||
.get(&resource_sync.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?
|
||||
.syncing
|
||||
{
|
||||
res.syncing += 1;
|
||||
continue;
|
||||
}
|
||||
res.ok += 1;
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
36
bin/core/src/api/read/tag.rs
Normal file
36
bin/core/src/api/read/tag.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use anyhow::Context;
|
||||
use database::mongo_indexed::doc;
|
||||
use database::mungos::{
|
||||
find::find_collect, mongodb::options::FindOptions,
|
||||
};
|
||||
use komodo_client::{
|
||||
api::read::{GetTag, ListTags},
|
||||
entities::tag::Tag,
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{helpers::query::get_tag, state::db_client};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetTag {
|
||||
async fn resolve(self, _: &ReadArgs) -> mogh_error::Result<Tag> {
|
||||
Ok(get_tag(&self.tag).await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListTags {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &ReadArgs,
|
||||
) -> mogh_error::Result<Vec<Tag>> {
|
||||
let res = find_collect(
|
||||
&db_client().tags,
|
||||
self.query,
|
||||
FindOptions::builder().sort(doc! { "name": 1 }).build(),
|
||||
)
|
||||
.await
|
||||
.context("failed to get tags from db")?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
247
bin/core/src/api/read/terminal.rs
Normal file
247
bin/core/src/api/read/terminal.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
use anyhow::Context as _;
|
||||
use futures_util::{
|
||||
FutureExt, StreamExt as _, stream::FuturesUnordered,
|
||||
};
|
||||
use komodo_client::{
|
||||
api::read::{ListTerminals, ListTerminalsResponse},
|
||||
entities::{
|
||||
deployment::Deployment,
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
stack::Stack,
|
||||
terminal::{Terminal, TerminalTarget},
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCode;
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
use crate::{
|
||||
helpers::periphery_client, permission::get_check_permissions,
|
||||
resource,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<ReadArgs> for ListTerminals {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListTerminalsResponse> {
|
||||
let Some(target) = self.target else {
|
||||
return list_all_terminals_for_user(user, self.use_names).await;
|
||||
};
|
||||
match &target {
|
||||
TerminalTarget::Server { server } => {
|
||||
let server = server
|
||||
.as_ref()
|
||||
.context("Must provide 'target.params.server'")
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
let server = get_check_permissions::<Server>(
|
||||
server,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
list_terminals_on_server(&server, Some(target)).await
|
||||
}
|
||||
TerminalTarget::Container { server, .. } => {
|
||||
let server = get_check_permissions::<Server>(
|
||||
server,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
list_terminals_on_server(&server, Some(target)).await
|
||||
}
|
||||
TerminalTarget::Stack { stack, .. } => {
|
||||
let server = get_check_permissions::<Stack>(
|
||||
stack,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?
|
||||
.config
|
||||
.server_id;
|
||||
let server = resource::get::<Server>(&server).await?;
|
||||
list_terminals_on_server(&server, Some(target)).await
|
||||
}
|
||||
TerminalTarget::Deployment { deployment } => {
|
||||
let server = get_check_permissions::<Deployment>(
|
||||
deployment,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?
|
||||
.config
|
||||
.server_id;
|
||||
let server = resource::get::<Server>(&server).await?;
|
||||
list_terminals_on_server(&server, Some(target)).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_all_terminals_for_user(
|
||||
user: &User,
|
||||
use_names: bool,
|
||||
) -> mogh_error::Result<Vec<Terminal>> {
|
||||
let (mut servers, stacks, deployments) = tokio::try_join!(
|
||||
resource::list_full_for_user::<Server>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
&[]
|
||||
)
|
||||
.map(|res| res.map(|servers| servers
|
||||
.into_iter()
|
||||
// true denotes user actually has permission on this Server.
|
||||
.map(|server| (server, true))
|
||||
.collect::<Vec<_>>())),
|
||||
resource::list_full_for_user::<Stack>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
&[]
|
||||
),
|
||||
resource::list_full_for_user::<Deployment>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
&[]
|
||||
),
|
||||
)?;
|
||||
|
||||
// Ensure any missing servers are present to query
|
||||
for stack in &stacks {
|
||||
if !stack.config.server_id.is_empty()
|
||||
&& !servers
|
||||
.iter()
|
||||
.any(|(server, _)| server.id == stack.config.server_id)
|
||||
{
|
||||
let server =
|
||||
resource::get::<Server>(&stack.config.server_id).await?;
|
||||
servers.push((server, false));
|
||||
}
|
||||
}
|
||||
for deployment in &deployments {
|
||||
if !deployment.config.server_id.is_empty()
|
||||
&& !servers
|
||||
.iter()
|
||||
.any(|(server, _)| server.id == deployment.config.server_id)
|
||||
{
|
||||
let server =
|
||||
resource::get::<Server>(&deployment.config.server_id).await?;
|
||||
servers.push((server, false));
|
||||
}
|
||||
}
|
||||
|
||||
let mut terminals = servers
|
||||
.into_iter()
|
||||
.map(|(server, server_permission)| async move {
|
||||
(
|
||||
list_terminals_on_server(&server, None).await,
|
||||
(server.id, server.name, server_permission),
|
||||
)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.flat_map(
|
||||
|(terminals, (server_id, server_name, server_permission))| {
|
||||
let terminals = terminals
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.filter_map(|mut terminal| {
|
||||
// Only keep terminals with appropriate perms.
|
||||
match terminal.target.clone() {
|
||||
TerminalTarget::Server { .. } => server_permission
|
||||
.then(|| {
|
||||
terminal.target = TerminalTarget::Server {
|
||||
server: Some(if use_names {
|
||||
server_name.clone()
|
||||
} else {
|
||||
server_id.clone()
|
||||
}),
|
||||
};
|
||||
terminal
|
||||
}),
|
||||
TerminalTarget::Container { container, .. } => {
|
||||
server_permission.then(|| {
|
||||
terminal.target = TerminalTarget::Container {
|
||||
server: if use_names {
|
||||
server_name.clone()
|
||||
} else {
|
||||
server_id.clone()
|
||||
},
|
||||
container,
|
||||
};
|
||||
terminal
|
||||
})
|
||||
}
|
||||
TerminalTarget::Stack { stack, service } => {
|
||||
stacks.iter().find(|s| s.id == stack).map(|s| {
|
||||
terminal.target = TerminalTarget::Stack {
|
||||
stack: if use_names {
|
||||
s.name.clone()
|
||||
} else {
|
||||
s.id.clone()
|
||||
},
|
||||
service,
|
||||
};
|
||||
terminal
|
||||
})
|
||||
}
|
||||
TerminalTarget::Deployment { deployment } => {
|
||||
deployments.iter().find(|d| d.id == deployment).map(
|
||||
|d| {
|
||||
terminal.target = TerminalTarget::Deployment {
|
||||
deployment: if use_names {
|
||||
d.name.clone()
|
||||
} else {
|
||||
d.id.clone()
|
||||
},
|
||||
};
|
||||
terminal
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Some(terminals)
|
||||
},
|
||||
)
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
terminals.sort_by(|a, b| {
|
||||
a.target.cmp(&b.target).then(a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(terminals)
|
||||
}
|
||||
|
||||
async fn list_terminals_on_server(
|
||||
server: &Server,
|
||||
target: Option<TerminalTarget>,
|
||||
) -> mogh_error::Result<Vec<Terminal>> {
|
||||
periphery_client(server)
|
||||
.await?
|
||||
.request(periphery_client::api::terminal::ListTerminals {
|
||||
target,
|
||||
})
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to get Terminal list from Server {} ({})",
|
||||
server.name, server.id
|
||||
)
|
||||
})
|
||||
.map_err(Into::into)
|
||||
}
|
||||
532
bin/core/src/api/read/toml.rs
Normal file
532
bin/core/src/api/read/toml.rs
Normal file
@@ -0,0 +1,532 @@
|
||||
use anyhow::Context;
|
||||
use database::mungos::find::find_collect;
|
||||
use komodo_client::{
|
||||
api::read::{
|
||||
ExportAllResourcesToToml, ExportAllResourcesToTomlResponse,
|
||||
ExportResourcesToToml, ExportResourcesToTomlResponse,
|
||||
ListUserGroups,
|
||||
},
|
||||
entities::{
|
||||
ResourceTarget, action::Action, alerter::Alerter, build::Build,
|
||||
builder::Builder, deployment::Deployment,
|
||||
permission::PermissionLevel, procedure::Procedure, repo::Repo,
|
||||
resource::ResourceQuery, server::Server, stack::Stack,
|
||||
swarm::Swarm, sync::ResourceSync, toml::ResourcesToml,
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::{
|
||||
get_all_tags, get_id_to_tags, get_user_user_group_ids,
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::db_client,
|
||||
sync::{
|
||||
toml::{ToToml, convert_resource},
|
||||
user_groups::{convert_user_groups, user_group_to_toml},
|
||||
variables::variable_to_toml,
|
||||
},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
async fn get_all_targets(
|
||||
tags: &[String],
|
||||
user: &User,
|
||||
) -> anyhow::Result<Vec<ResourceTarget>> {
|
||||
let mut targets = Vec::<ResourceTarget>::new();
|
||||
let all_tags = if tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Alerter>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Alerter(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Builder>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Builder(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Server>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Server(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Stack>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Stack(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Deployment>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Deployment(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Build>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Build(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Repo>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Repo(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Procedure>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Procedure(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Action>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Action(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
// These will already be filtered by [ExportResourcesToToml]
|
||||
.map(|resource| ResourceTarget::ResourceSync(resource.id)),
|
||||
);
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ExportAllResourcesToToml {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &ReadArgs,
|
||||
) -> mogh_error::Result<ExportAllResourcesToTomlResponse> {
|
||||
let targets = if self.include_resources {
|
||||
get_all_targets(&self.tags, &args.user).await?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let user_groups = if self.include_user_groups {
|
||||
if args.user.admin {
|
||||
find_collect(&db_client().user_groups, None, None)
|
||||
.await
|
||||
.context("failed to query db for user groups")?
|
||||
.into_iter()
|
||||
.map(|user_group| user_group.id)
|
||||
.collect()
|
||||
} else {
|
||||
get_user_user_group_ids(&args.user.id).await?
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
ExportResourcesToToml {
|
||||
targets,
|
||||
user_groups,
|
||||
include_variables: self.include_variables,
|
||||
}
|
||||
.resolve(args)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &ReadArgs,
|
||||
) -> mogh_error::Result<ExportResourcesToTomlResponse> {
|
||||
let ExportResourcesToToml {
|
||||
targets,
|
||||
user_groups,
|
||||
include_variables,
|
||||
} = self;
|
||||
let mut res = ResourcesToml::default();
|
||||
let id_to_tags = get_id_to_tags(None).await?;
|
||||
let ReadArgs { user } = args;
|
||||
for target in targets {
|
||||
match target {
|
||||
ResourceTarget::Swarm(id) => {
|
||||
let mut swarm = get_check_permissions::<Swarm>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Swarm::replace_ids(&mut swarm);
|
||||
res.swarms.push(convert_resource::<Swarm>(
|
||||
swarm,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Server(id) => {
|
||||
let mut server = get_check_permissions::<Server>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Server::replace_ids(&mut server);
|
||||
res.servers.push(convert_resource::<Server>(
|
||||
server,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Stack(id) => {
|
||||
let mut stack = get_check_permissions::<Stack>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Stack::replace_ids(&mut stack);
|
||||
res.stacks.push(convert_resource::<Stack>(
|
||||
stack,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Deployment(id) => {
|
||||
let mut deployment = get_check_permissions::<Deployment>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Deployment::replace_ids(&mut deployment);
|
||||
res.deployments.push(convert_resource::<Deployment>(
|
||||
deployment,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Build(id) => {
|
||||
let mut build = get_check_permissions::<Build>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Build::replace_ids(&mut build);
|
||||
res.builds.push(convert_resource::<Build>(
|
||||
build,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Repo(id) => {
|
||||
let mut repo = get_check_permissions::<Repo>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Repo::replace_ids(&mut repo);
|
||||
res.repos.push(convert_resource::<Repo>(
|
||||
repo,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
let mut procedure = get_check_permissions::<Procedure>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Procedure::replace_ids(&mut procedure);
|
||||
res.procedures.push(convert_resource::<Procedure>(
|
||||
procedure,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
));
|
||||
}
|
||||
ResourceTarget::Action(id) => {
|
||||
let mut action = get_check_permissions::<Action>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Action::replace_ids(&mut action);
|
||||
res.actions.push(convert_resource::<Action>(
|
||||
action,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
));
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
let mut sync = get_check_permissions::<ResourceSync>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
if sync.config.file_contents.is_empty()
|
||||
&& (sync.config.files_on_host
|
||||
|| !sync.config.repo.is_empty()
|
||||
|| !sync.config.linked_repo.is_empty())
|
||||
{
|
||||
ResourceSync::replace_ids(&mut sync);
|
||||
res.resource_syncs.push(convert_resource::<ResourceSync>(
|
||||
sync,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
let mut builder = get_check_permissions::<Builder>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Builder::replace_ids(&mut builder);
|
||||
res.builders.push(convert_resource::<Builder>(
|
||||
builder,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Alerter(id) => {
|
||||
let mut alerter = get_check_permissions::<Alerter>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Alerter::replace_ids(&mut alerter);
|
||||
res.alerters.push(convert_resource::<Alerter>(
|
||||
alerter,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::System(_) => continue,
|
||||
};
|
||||
}
|
||||
|
||||
add_user_groups(user_groups, &mut res, args)
|
||||
.await
|
||||
.context("failed to add user groups")?;
|
||||
|
||||
if include_variables {
|
||||
res.variables =
|
||||
find_collect(&db_client().variables, None, None)
|
||||
.await
|
||||
.context("failed to get variables from db")?
|
||||
.into_iter()
|
||||
.map(|mut variable| {
|
||||
if !user.admin && variable.is_secret {
|
||||
variable.value = "#".repeat(variable.value.len())
|
||||
}
|
||||
variable
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
let toml = serialize_resources_toml(res)
|
||||
.context("failed to serialize resources to toml")?;
|
||||
|
||||
Ok(ExportResourcesToTomlResponse { toml })
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_user_groups(
|
||||
user_groups: Vec<String>,
|
||||
res: &mut ResourcesToml,
|
||||
args: &ReadArgs,
|
||||
) -> anyhow::Result<()> {
|
||||
let user_groups = ListUserGroups {}
|
||||
.resolve(args)
|
||||
.await
|
||||
.map_err(|e| e.error)?
|
||||
.into_iter()
|
||||
.filter(|ug| {
|
||||
user_groups.contains(&ug.name) || user_groups.contains(&ug.id)
|
||||
});
|
||||
let mut ug = Vec::with_capacity(user_groups.size_hint().0);
|
||||
convert_user_groups(user_groups, &mut ug).await?;
|
||||
res.user_groups = ug.into_iter().map(|ug| ug.1).collect();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_resources_toml(
|
||||
resources: ResourcesToml,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut toml = String::new();
|
||||
|
||||
for server in resources.servers {
|
||||
if !toml.is_empty() {
|
||||
toml.push_str("\n\n##\n\n");
|
||||
}
|
||||
toml.push_str("[[server]]\n");
|
||||
Server::push_to_toml_string(server, &mut toml)?;
|
||||
}
|
||||
|
||||
for stack in resources.stacks {
|
||||
if !toml.is_empty() {
|
||||
toml.push_str("\n\n##\n\n");
|
||||
}
|
||||
toml.push_str("[[stack]]\n");
|
||||
Stack::push_to_toml_string(stack, &mut toml)?;
|
||||
}
|
||||
|
||||
for deployment in resources.deployments {
|
||||
if !toml.is_empty() {
|
||||
toml.push_str("\n\n##\n\n");
|
||||
}
|
||||
toml.push_str("[[deployment]]\n");
|
||||
Deployment::push_to_toml_string(deployment, &mut toml)?;
|
||||
}
|
||||
|
||||
for build in resources.builds {
|
||||
if !toml.is_empty() {
|
||||
toml.push_str("\n\n##\n\n");
|
||||
}
|
||||
toml.push_str("[[build]]\n");
|
||||
Build::push_to_toml_string(build, &mut toml)?;
|
||||
}
|
||||
|
||||
for repo in resources.repos {
|
||||
if !toml.is_empty() {
|
||||
toml.push_str("\n\n##\n\n");
|
||||
}
|
||||
toml.push_str("[[repo]]\n");
|
||||
Repo::push_to_toml_string(repo, &mut toml)?;
|
||||
}
|
||||
|
||||
for procedure in resources.procedures {
|
||||
if !toml.is_empty() {
|
||||
toml.push_str("\n\n##\n\n");
|
||||
}
|
||||
toml.push_str("[[procedure]]\n");
|
||||
Procedure::push_to_toml_string(procedure, &mut toml)?;
|
||||
}
|
||||
|
||||
for action in resources.actions {
|
||||
if !toml.is_empty() {
|
||||
toml.push_str("\n\n##\n\n");
|
||||
}
|
||||
toml.push_str("[[action]]\n");
|
||||
Action::push_to_toml_string(action, &mut toml)?;
|
||||
}
|
||||
|
||||
for alerter in resources.alerters {
|
||||
if !toml.is_empty() {
|
||||
toml.push_str("\n\n##\n\n");
|
||||
}
|
||||
toml.push_str("[[alerter]]\n");
|
||||
Alerter::push_to_toml_string(alerter, &mut toml)?;
|
||||
}
|
||||
|
||||
for builder in resources.builders {
|
||||
if !toml.is_empty() {
|
||||
toml.push_str("\n\n##\n\n");
|
||||
}
|
||||
toml.push_str("[[builder]]\n");
|
||||
Builder::push_to_toml_string(builder, &mut toml)?;
|
||||
}
|
||||
|
||||
for resource_sync in resources.resource_syncs {
|
||||
if !toml.is_empty() {
|
||||
toml.push_str("\n\n##\n\n");
|
||||
}
|
||||
toml.push_str("[[resource_sync]]\n");
|
||||
ResourceSync::push_to_toml_string(resource_sync, &mut toml)?;
|
||||
}
|
||||
|
||||
for variable in &resources.variables {
|
||||
if !toml.is_empty() {
|
||||
toml.push_str("\n\n##\n\n");
|
||||
}
|
||||
toml.push_str(&variable_to_toml(variable)?);
|
||||
}
|
||||
|
||||
for user_group in resources.user_groups {
|
||||
if !toml.is_empty() {
|
||||
toml.push_str("\n\n##\n\n");
|
||||
}
|
||||
toml.push_str(&user_group_to_toml(user_group)?);
|
||||
}
|
||||
|
||||
Ok(toml)
|
||||
}
|
||||
111
bin/core/src/api/read/update.rs
Normal file
111
bin/core/src/api/read/update.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Context;
|
||||
use database::mungos::{
|
||||
by_id::find_one_by_id,
|
||||
find::find_collect,
|
||||
mongodb::{bson::doc, options::FindOptions},
|
||||
};
|
||||
use komodo_client::{
|
||||
api::read::{GetUpdate, ListUpdates, ListUpdatesResponse},
|
||||
entities::{
|
||||
permission::PermissionLevel,
|
||||
update::{Update, UpdateListItem},
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
permission::{
|
||||
check_user_target_access, user_resource_target_query,
|
||||
},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
const UPDATES_PER_PAGE: i64 = 100;
|
||||
|
||||
impl Resolve<ReadArgs> for ListUpdates {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListUpdatesResponse> {
|
||||
let query = user_resource_target_query(user, self.query).await?;
|
||||
|
||||
let usernames = find_collect(&db_client().users, None, None)
|
||||
.await
|
||||
.context("failed to pull users from db")?
|
||||
.into_iter()
|
||||
.map(|u| (u.id, u.username))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let updates = find_collect(
|
||||
&db_client().updates,
|
||||
query,
|
||||
FindOptions::builder()
|
||||
.sort(doc! { "start_ts": -1 })
|
||||
.skip(self.page as u64 * UPDATES_PER_PAGE as u64)
|
||||
.limit(UPDATES_PER_PAGE)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.context("failed to pull updates from db")?
|
||||
.into_iter()
|
||||
.map(|u| {
|
||||
let username = if User::is_service_user(&u.operator) {
|
||||
u.operator.clone()
|
||||
} else {
|
||||
usernames
|
||||
.get(&u.operator)
|
||||
.cloned()
|
||||
.unwrap_or("unknown".to_string())
|
||||
};
|
||||
UpdateListItem {
|
||||
username,
|
||||
id: u.id,
|
||||
operation: u.operation,
|
||||
start_ts: u.start_ts,
|
||||
success: u.success,
|
||||
operator: u.operator,
|
||||
target: u.target,
|
||||
status: u.status,
|
||||
version: u.version,
|
||||
other_data: u.other_data,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let next_page = if updates.len() == UPDATES_PER_PAGE as usize {
|
||||
Some(self.page + 1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ListUpdatesResponse { updates, next_page })
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetUpdate {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let update = find_one_by_id(&db_client().updates, &self.id)
|
||||
.await
|
||||
.context("failed to query to db")?
|
||||
.context("no update exists with given id")?;
|
||||
if user.admin || core_config().transparent_mode {
|
||||
return Ok(update);
|
||||
}
|
||||
check_user_target_access(
|
||||
&update.target,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
137
bin/core/src/api/read/user.rs
Normal file
137
bin/core/src/api/read/user.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::{
|
||||
by_id::find_one_by_id,
|
||||
find::find_collect,
|
||||
mongodb::{bson::doc, options::FindOptions},
|
||||
};
|
||||
use komodo_client::{
|
||||
api::read::{
|
||||
FindUser, FindUserResponse, GetUsername, GetUsernameResponse,
|
||||
ListApiKeys, ListApiKeysForServiceUser,
|
||||
ListApiKeysForServiceUserResponse, ListApiKeysResponse,
|
||||
ListUsers, ListUsersResponse,
|
||||
},
|
||||
entities::user::{UserConfig, admin_service_user},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{helpers::query::get_user, state::db_client};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetUsername {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &ReadArgs,
|
||||
) -> mogh_error::Result<GetUsernameResponse> {
|
||||
if let Some(user) = admin_service_user(&self.user_id) {
|
||||
return Ok(GetUsernameResponse {
|
||||
username: user.username,
|
||||
avatar: None,
|
||||
});
|
||||
}
|
||||
|
||||
let user = find_one_by_id(&db_client().users, &self.user_id)
|
||||
.await
|
||||
.context("failed at mongo query for user")?
|
||||
.context("no user found with id")?;
|
||||
|
||||
let avatar = match user.config {
|
||||
UserConfig::Github { avatar, .. } => Some(avatar),
|
||||
UserConfig::Google { avatar, .. } => Some(avatar),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Ok(GetUsernameResponse {
|
||||
username: user.username,
|
||||
avatar,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for FindUser {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user: admin }: &ReadArgs,
|
||||
) -> mogh_error::Result<FindUserResponse> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This method is admin only.").into());
|
||||
}
|
||||
Ok(get_user(&self.user).await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListUsers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListUsersResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("this route is only accessable by admins").into(),
|
||||
);
|
||||
}
|
||||
let mut users = find_collect(
|
||||
&db_client().users,
|
||||
None,
|
||||
FindOptions::builder().sort(doc! { "username": 1 }).build(),
|
||||
)
|
||||
.await
|
||||
.context("failed to pull users from db")?;
|
||||
users.iter_mut().for_each(|user| user.sanitize());
|
||||
Ok(users)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListApiKeys {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListApiKeysResponse> {
|
||||
let api_keys = find_collect(
|
||||
&db_client().api_keys,
|
||||
doc! { "user_id": &user.id },
|
||||
FindOptions::builder().sort(doc! { "name": 1 }).build(),
|
||||
)
|
||||
.await
|
||||
.context("failed to query db for api keys")?
|
||||
.into_iter()
|
||||
.map(|mut api_keys| {
|
||||
api_keys.sanitize();
|
||||
api_keys
|
||||
})
|
||||
.collect();
|
||||
Ok(api_keys)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListApiKeysForServiceUser {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user: admin }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListApiKeysForServiceUserResponse> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This method is admin only.").into());
|
||||
}
|
||||
|
||||
let user = get_user(&self.user).await?;
|
||||
|
||||
let UserConfig::Service { .. } = user.config else {
|
||||
return Err(anyhow!("Given user is not service user").into());
|
||||
};
|
||||
let api_keys = find_collect(
|
||||
&db_client().api_keys,
|
||||
doc! { "user_id": &user.id },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to query db for api keys")?
|
||||
.into_iter()
|
||||
.map(|mut api_keys| {
|
||||
api_keys.sanitize();
|
||||
api_keys
|
||||
})
|
||||
.collect();
|
||||
Ok(api_keys)
|
||||
}
|
||||
}
|
||||
60
bin/core/src/api/read/user_group.rs
Normal file
60
bin/core/src/api/read/user_group.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Context;
|
||||
use database::mungos::{
|
||||
find::find_collect,
|
||||
mongodb::{
|
||||
bson::{Document, doc, oid::ObjectId},
|
||||
options::FindOptions,
|
||||
},
|
||||
};
|
||||
use komodo_client::api::read::*;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::state::db_client;
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetUserGroup {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetUserGroupResponse> {
|
||||
let mut filter = match ObjectId::from_str(&self.user_group) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": &self.user_group },
|
||||
};
|
||||
// Don't allow non admin users to get UserGroups they aren't a part of.
|
||||
if !user.admin {
|
||||
// Filter for only UserGroups which contain the users id
|
||||
filter.insert("users", &user.id);
|
||||
}
|
||||
let res = db_client()
|
||||
.user_groups
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("failed to query db for user groups")?
|
||||
.context("no UserGroup found with given name or id")?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListUserGroups {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListUserGroupsResponse> {
|
||||
let mut filter = Document::new();
|
||||
if !user.admin {
|
||||
filter.insert("users", &user.id);
|
||||
}
|
||||
let res = find_collect(
|
||||
&db_client().user_groups,
|
||||
filter,
|
||||
FindOptions::builder().sort(doc! { "name": 1 }).build(),
|
||||
)
|
||||
.await
|
||||
.context("failed to query db for UserGroups")?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
53
bin/core/src/api/read/variable.rs
Normal file
53
bin/core/src/api/read/variable.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use anyhow::Context;
|
||||
use database::mongo_indexed::doc;
|
||||
use database::mungos::{
|
||||
find::find_collect, mongodb::options::FindOptions,
|
||||
};
|
||||
use komodo_client::api::read::*;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{helpers::query::get_variable, state::db_client};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetVariable {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<GetVariableResponse> {
|
||||
let mut variable = get_variable(&self.name).await?;
|
||||
if !variable.is_secret || user.admin {
|
||||
return Ok(variable);
|
||||
}
|
||||
variable.value = "#".repeat(variable.value.len());
|
||||
Ok(variable)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListVariables {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> mogh_error::Result<ListVariablesResponse> {
|
||||
let variables = find_collect(
|
||||
&db_client().variables,
|
||||
None,
|
||||
FindOptions::builder().sort(doc! { "name": 1 }).build(),
|
||||
)
|
||||
.await
|
||||
.context("failed to query db for variables")?;
|
||||
if user.admin {
|
||||
return Ok(variables);
|
||||
}
|
||||
let variables = variables
|
||||
.into_iter()
|
||||
.map(|mut variable| {
|
||||
if variable.is_secret {
|
||||
variable.value = "#".repeat(variable.value.len());
|
||||
}
|
||||
variable
|
||||
})
|
||||
.collect();
|
||||
Ok(variables)
|
||||
}
|
||||
}
|
||||
56
bin/core/src/api/terminal.rs
Normal file
56
bin/core/src/api/terminal.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use anyhow::Context;
|
||||
use axum::{Extension, Router, middleware, routing::post};
|
||||
use komodo_client::{api::terminal::*, entities::user::User};
|
||||
use mogh_auth_server::middleware::authenticate_request;
|
||||
use mogh_error::Json;
|
||||
|
||||
use crate::{
|
||||
auth::KomodoAuthImpl, helpers::terminal::setup_target_for_user,
|
||||
};
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/execute", post(execute_terminal))
|
||||
.layer(middleware::from_fn(
|
||||
authenticate_request::<KomodoAuthImpl, true>,
|
||||
))
|
||||
}
|
||||
|
||||
// =================
|
||||
// ExecuteTerminal
|
||||
// =================
|
||||
|
||||
#[instrument(
|
||||
name = "ExecuteTerminal",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
target,
|
||||
terminal,
|
||||
init = format!("{init:?}")
|
||||
)
|
||||
)]
|
||||
async fn execute_terminal(
|
||||
Extension(user): Extension<User>,
|
||||
Json(ExecuteTerminalBody {
|
||||
target,
|
||||
terminal,
|
||||
command,
|
||||
init,
|
||||
}): Json<ExecuteTerminalBody>,
|
||||
) -> mogh_error::Result<axum::body::Body> {
|
||||
info!(
|
||||
"TERMINAL EXECUTE REQUEST | USER: {} ({})",
|
||||
user.username, user.id
|
||||
);
|
||||
|
||||
let (target, terminal, periphery) =
|
||||
setup_target_for_user(target, terminal, init, &user).await?;
|
||||
|
||||
let stream = periphery
|
||||
.execute_terminal(target, terminal, command)
|
||||
.await
|
||||
.context("Failed to execute command on Terminal")?;
|
||||
|
||||
Ok(axum::body::Body::from_stream(stream))
|
||||
}
|
||||
108
bin/core/src/api/write/action.rs
Normal file
108
bin/core/src/api/write/action.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
action::Action, permission::PermissionLevel, update::Update,
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{permission::get_check_permissions, resource};
|
||||
|
||||
use super::WriteArgs;
|
||||
|
||||
impl Resolve<WriteArgs> for CreateAction {
|
||||
#[instrument(
|
||||
"CreateAction",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
action = self.name,
|
||||
config = serde_json::to_string(&self.config).unwrap(),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> mogh_error::Result<Action> {
|
||||
resource::create::<Action>(&self.name, self.config, None, user)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for CopyAction {
|
||||
#[instrument(
|
||||
"CopyAction",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
action = self.name,
|
||||
copy_action = self.id,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> mogh_error::Result<Action> {
|
||||
let Action { config, .. } = get_check_permissions::<Action>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
resource::create::<Action>(&self.name, config.into(), None, user)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for UpdateAction {
|
||||
#[instrument(
|
||||
"UpdateAction",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
action = self.id,
|
||||
update = serde_json::to_string(&self.config).unwrap(),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> mogh_error::Result<Action> {
|
||||
Ok(resource::update::<Action>(&self.id, self.config, user).await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for RenameAction {
|
||||
#[instrument(
|
||||
"RenameAction",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
action = self.id,
|
||||
new_name = self.name,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
Ok(resource::rename::<Action>(&self.id, &self.name, user).await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for DeleteAction {
|
||||
#[instrument(
|
||||
"DeleteAction",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
action = self.id
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> mogh_error::Result<Action> {
|
||||
Ok(resource::delete::<Action>(&self.id, user).await?)
|
||||
}
|
||||
}
|
||||
41
bin/core/src/api/write/alert.rs
Normal file
41
bin/core/src/api/write/alert.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::mongodb::bson::{doc, oid::ObjectId};
|
||||
use komodo_client::{api::write::CloseAlert, entities::NoData};
|
||||
use mogh_error::AddStatusCodeError;
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
use crate::{api::write::WriteArgs, state::db_client};
|
||||
|
||||
impl Resolve<WriteArgs> for CloseAlert {
|
||||
#[instrument(
|
||||
"CloseAlert",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = admin.id,
|
||||
alert_id = self.id,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This call is admin only")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
db_client()
|
||||
.alerts
|
||||
.update_one(
|
||||
doc! { "_id": ObjectId::from_str(&self.id)? },
|
||||
doc! { "$set": { "resolved": true } },
|
||||
)
|
||||
.await
|
||||
.context("Failed to close Alert on database")?;
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user