forked from github-starred/komodo
Compare commits
956 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,4 +1,8 @@
|
||||
/target
|
||||
/config_example
|
||||
config.*
|
||||
.env
|
||||
readme.md
|
||||
typeshare.toml
|
||||
LICENSE
|
||||
*.code-workspace
|
||||
|
||||
*/node_modules
|
||||
*/dist
|
||||
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
|
||||
/frontend/build
|
||||
node_modules
|
||||
/lib/ts_client/build
|
||||
dist
|
||||
.env
|
||||
.env.development
|
||||
creds.toml
|
||||
core.config.toml
|
||||
26
.vscode/resolver.code-snippets
vendored
Normal file
26
.vscode/resolver.code-snippets
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"resolve": {
|
||||
"scope": "rust",
|
||||
"prefix": "resolve",
|
||||
"body": [
|
||||
"#[async_trait]",
|
||||
"impl Resolve<${1}, User> for State {",
|
||||
"\tasync fn resolve(&self, ${1} { ${0} }: ${1}, _: User) -> anyhow::Result<${2}> {",
|
||||
"\t\ttodo!()",
|
||||
"\t}",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"static": {
|
||||
"scope": "rust",
|
||||
"prefix": "static",
|
||||
"body": [
|
||||
"fn ${1}() -> &'static ${2} {",
|
||||
"\tstatic ${3}: OnceLock<${2}> = OnceLock::new();",
|
||||
"\t${3}.get_or_init(|| {",
|
||||
"\t\t${0}",
|
||||
"\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;",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
89
.vscode/tasks.json
vendored
89
.vscode/tasks.json
vendored
@@ -24,14 +24,14 @@
|
||||
"label": "start dev",
|
||||
"dependsOn": [
|
||||
"run core",
|
||||
"yarn: start frontend"
|
||||
"start frontend"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "yarn start",
|
||||
"label": "yarn: start frontend",
|
||||
"label": "start frontend",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/frontend"
|
||||
},
|
||||
@@ -44,7 +44,7 @@
|
||||
"command": "run",
|
||||
"label": "run core",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/core"
|
||||
"cwd": "${workspaceFolder}/bin/core"
|
||||
},
|
||||
"presentation": {
|
||||
"group": "start"
|
||||
@@ -55,24 +55,7 @@
|
||||
"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"
|
||||
"cwd": "${workspaceFolder}/bin/periphery"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -80,14 +63,14 @@
|
||||
"command": "run",
|
||||
"label": "run tests",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/tests"
|
||||
"cwd": "${workspaceFolder}/bin/tests"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "publish",
|
||||
"args": ["--allow-dirty"],
|
||||
"label": "publish monitor types",
|
||||
"label": "publish types",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/lib/types"
|
||||
}
|
||||
@@ -95,68 +78,14 @@
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "publish",
|
||||
"label": "publish monitor client",
|
||||
"label": "publish rs client",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/lib/monitor_client"
|
||||
"cwd": "${workspaceFolder}/lib/rs_client"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"command": "node ./client/ts/generate_types.mjs",
|
||||
"label": "generate typescript types",
|
||||
"problemMatcher": []
|
||||
}
|
||||
|
||||
3164
Cargo.lock
generated
3164
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
108
Cargo.toml
108
Cargo.toml
@@ -1,14 +1,94 @@
|
||||
[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 = "1.0.0"
|
||||
edition = "2021"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[workspace.dependencies]
|
||||
# LOCAL
|
||||
monitor_macros = { path = "lib/macros" }
|
||||
monitor_client = { path = "client/core/rs" }
|
||||
periphery_client = { path = "client/periphery/rs" }
|
||||
logger = { path = "lib/logger" }
|
||||
|
||||
# MOGH
|
||||
run_command = { version = "0.0.6", features = ["async_tokio"] }
|
||||
serror = { version = "0.3.4", features = ["axum"] }
|
||||
slack = { version = "0.1.0", package = "slack_client_rs" }
|
||||
derive_default_builder = "0.1.8"
|
||||
derive_empty_traits = "0.1.0"
|
||||
merge_config_files = "0.1.5"
|
||||
termination_signal = "0.1.3"
|
||||
async_timing_util = "0.1.14"
|
||||
partial_derive2 = "0.3.2"
|
||||
derive_variants = "0.1.3"
|
||||
mongo_indexed = "0.2.2"
|
||||
resolver_api = "0.1.9"
|
||||
parse_csl = "0.1.0"
|
||||
mungos = "0.5.4"
|
||||
svi = "0.1.4"
|
||||
|
||||
# ASYNC
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
reqwest = { version = "0.12.3", features = ["json"] }
|
||||
tokio-util = "0.7.10"
|
||||
futures = "0.3.30"
|
||||
futures-util = "0.3.30"
|
||||
async-trait = "0.1.80"
|
||||
async-recursion = "1.1.0"
|
||||
|
||||
# SERVER
|
||||
axum = { version = "0.7.5", features = ["ws", "json"] }
|
||||
axum-extra = { version = "0.9.3", features = ["typed-header"] }
|
||||
tower = { version = "0.4.13", features = ["timeout"] }
|
||||
tower-http = { version = "0.5.2", features = ["fs", "cors"] }
|
||||
tokio-tungstenite = "0.21.0"
|
||||
|
||||
# SER/DE
|
||||
serde = { version = "1.0.198", features = ["derive"] }
|
||||
strum = { version = "0.26.2", features = ["derive"] }
|
||||
serde_json = "1.0.116"
|
||||
toml = "0.8.12"
|
||||
|
||||
# ERROR
|
||||
anyhow = "1.0.82"
|
||||
thiserror = "1.0.58"
|
||||
|
||||
# LOGGING
|
||||
opentelemetry_sdk = { version = "0.22.1", features = ["rt-tokio"] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["json"] }
|
||||
tracing-opentelemetry = "0.23.0"
|
||||
opentelemetry-otlp = "0.15.0"
|
||||
opentelemetry = "0.22.0"
|
||||
tracing = "0.1.40"
|
||||
|
||||
# CONFIG
|
||||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
dotenv = "0.15.0"
|
||||
envy = "0.4.2"
|
||||
|
||||
# CRYPTO
|
||||
uuid = { version = "1.8.0", features = ["v4", "fast-rng", "serde"] }
|
||||
urlencoding = "2.1.3"
|
||||
rand = "0.8.5"
|
||||
jwt = "0.16.0"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.8"
|
||||
bcrypt = "0.15.1"
|
||||
hex = "0.4.3"
|
||||
|
||||
# SYSTEM
|
||||
bollard = "0.16.1"
|
||||
sysinfo = "0.30.11"
|
||||
|
||||
# CLOUD
|
||||
aws-config = "1.2.0"
|
||||
aws-sdk-ec2 = "1.34.0"
|
||||
|
||||
# MISC
|
||||
derive_builder = "0.20.0"
|
||||
typeshare = "1.0.2"
|
||||
|
||||
23
bin/alert_logger/Cargo.toml
Normal file
23
bin/alert_logger/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "alert_logger"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# local
|
||||
monitor_client.workspace = true
|
||||
logger.workspace = true
|
||||
# mogh
|
||||
termination_signal.workspace = true
|
||||
# external
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
axum.workspace = true
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
dotenv.workspace = true
|
||||
envy.workspace = true
|
||||
14
bin/alert_logger/Dockerfile
Normal file
14
bin/alert_logger/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM rust:1.71.1 as builder
|
||||
WORKDIR /builder
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN cargo build -p alert_logger --release
|
||||
|
||||
FROM gcr.io/distroless/cc
|
||||
|
||||
COPY --from=builder /builder/target/release/alert_logger /
|
||||
|
||||
EXPOSE 7000
|
||||
|
||||
CMD ["./alert_logger"]
|
||||
70
bin/alert_logger/src/main.rs
Normal file
70
bin/alert_logger/src/main.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
use std::{net::SocketAddr, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{routing::post, Json, Router};
|
||||
use monitor_client::entities::{
|
||||
alert::Alert, server::stats::SeverityLevel,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use termination_signal::tokio::immediate_term_handle;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Env {
|
||||
#[serde(default = "default_port")]
|
||||
port: u16,
|
||||
}
|
||||
|
||||
fn default_port() -> u16 {
|
||||
7000
|
||||
}
|
||||
|
||||
async fn app() -> anyhow::Result<()> {
|
||||
dotenv::dotenv().ok();
|
||||
logger::init(&Default::default())?;
|
||||
|
||||
let Env { port } =
|
||||
envy::from_env().context("failed to parse env")?;
|
||||
|
||||
let socket_addr = SocketAddr::from_str(&format!("0.0.0.0:{port}"))
|
||||
.context("invalid socket addr")?;
|
||||
|
||||
info!("v {} | {socket_addr}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
let app = Router::new().route(
|
||||
"/",
|
||||
post(|Json(alert): Json<Alert>| async move {
|
||||
if alert.resolved {
|
||||
info!("Alert Resolved!: {alert:?}");
|
||||
return;
|
||||
}
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => info!("{alert:?}"),
|
||||
SeverityLevel::Warning => warn!("{alert:?}"),
|
||||
SeverityLevel::Critical => error!("{alert:?}"),
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(socket_addr)
|
||||
.await
|
||||
.context("failed to bind tcp listener")?;
|
||||
|
||||
axum::serve(listener, app).await.context("server crashed")
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let term_signal = immediate_term_handle()?;
|
||||
|
||||
let app = tokio::spawn(app());
|
||||
|
||||
tokio::select! {
|
||||
res = app => return res?,
|
||||
_ = term_signal => {},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
57
bin/core/Cargo.toml
Normal file
57
bin/core/Cargo.toml
Normal file
@@ -0,0 +1,57 @@
|
||||
[package]
|
||||
name = "monitor_core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.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
|
||||
monitor_client.workspace = true
|
||||
periphery_client.workspace = true
|
||||
logger.workspace = true
|
||||
# mogh
|
||||
async_timing_util.workspace = true
|
||||
merge_config_files.workspace = true
|
||||
parse_csl.workspace = true
|
||||
termination_signal.workspace = true
|
||||
resolver_api.workspace = true
|
||||
mungos.workspace = true
|
||||
mongo_indexed.workspace = true
|
||||
slack.workspace = true
|
||||
serror.workspace = true
|
||||
# external
|
||||
tokio.workspace = true
|
||||
tokio-util.workspace = true
|
||||
axum.workspace = true
|
||||
axum-extra.workspace = true
|
||||
tower.workspace = true
|
||||
tower-http.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
toml.workspace = true
|
||||
uuid.workspace = true
|
||||
anyhow.workspace = true
|
||||
tracing.workspace = true
|
||||
dotenv.workspace = true
|
||||
envy.workspace = true
|
||||
reqwest.workspace = true
|
||||
urlencoding.workspace = true
|
||||
rand.workspace = true
|
||||
jwt.workspace = true
|
||||
hmac.workspace = true
|
||||
sha2.workspace = true
|
||||
bcrypt.workspace = true
|
||||
hex.workspace = true
|
||||
async-trait.workspace = true
|
||||
async-recursion.workspace = true
|
||||
futures.workspace = true
|
||||
aws-config.workspace = true
|
||||
aws-sdk-ec2.workspace = true
|
||||
typeshare.workspace = true
|
||||
23
bin/core/Dockerfile
Normal file
23
bin/core/Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
# Build Core
|
||||
FROM rust:1.77.2-bullseye as core-builder
|
||||
WORKDIR /builder
|
||||
COPY . .
|
||||
RUN cargo build -p monitor_core --release
|
||||
|
||||
# Build Frontend
|
||||
FROM node:20.12-alpine as frontend-builder
|
||||
WORKDIR /builder
|
||||
COPY ./frontend ./frontend
|
||||
COPY ./client/core/ts ./client
|
||||
RUN cd client && yarn && yarn build && yarn link
|
||||
RUN cd frontend && yarn link @monitor/client && yarn && yarn build
|
||||
|
||||
# Final Image
|
||||
# FROM gcr.io/distroless/cc
|
||||
FROM debian:bullseye-slim
|
||||
RUN apt update && apt install -y ca-certificates
|
||||
COPY ./config_example/core.config.example.toml /config/config.toml
|
||||
COPY --from=core-builder /builder/target/release/core /
|
||||
COPY --from=frontend-builder /builder/frontend/dist /frontend
|
||||
EXPOSE 9000
|
||||
CMD ["./core"]
|
||||
132
bin/core/src/api/auth.rs
Normal file
132
bin/core/src/api/auth.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use std::{sync::OnceLock, time::Instant};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use async_trait::async_trait;
|
||||
use axum::{http::HeaderMap, routing::post, Router};
|
||||
use axum_extra::{headers::ContentType, TypedHeader};
|
||||
use monitor_client::{api::auth::*, entities::user::User};
|
||||
use resolver_api::{derive::Resolver, Resolve, Resolver};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serror::Json;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
auth::{
|
||||
get_user_id_from_headers,
|
||||
github::{self, client::github_oauth_client},
|
||||
google::{self, client::google_oauth_client},
|
||||
},
|
||||
config::core_config,
|
||||
helpers::query::get_user,
|
||||
state::{jwt_client, State},
|
||||
};
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
|
||||
#[resolver_target(State)]
|
||||
#[resolver_args(HeaderMap)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
#[allow(clippy::enum_variant_names, clippy::large_enum_variant)]
|
||||
pub enum AuthRequest {
|
||||
GetLoginOptions(GetLoginOptions),
|
||||
CreateLocalUser(CreateLocalUser),
|
||||
LoginLocalUser(LoginLocalUser),
|
||||
ExchangeForJwt(ExchangeForJwt),
|
||||
GetUser(GetUser),
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
let mut router = Router::new().route("/", post(handler));
|
||||
|
||||
if github_oauth_client().is_some() {
|
||||
router = router.nest("/github", github::router())
|
||||
}
|
||||
|
||||
if google_oauth_client().is_some() {
|
||||
router = router.nest("/google", google::router())
|
||||
}
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
#[instrument(name = "AuthHandler", level = "debug", skip(headers))]
|
||||
async fn handler(
|
||||
headers: HeaderMap,
|
||||
Json(request): Json<AuthRequest>,
|
||||
) -> serror::Result<(TypedHeader<ContentType>, String)> {
|
||||
let timer = Instant::now();
|
||||
let req_id = Uuid::new_v4();
|
||||
debug!("/auth request {req_id} | METHOD: {}", request.req_type());
|
||||
let res = State.resolve_request(request, headers).await.map_err(
|
||||
|e| match e {
|
||||
resolver_api::Error::Serialization(e) => {
|
||||
anyhow!("{e:?}").context("response serialization error")
|
||||
}
|
||||
resolver_api::Error::Inner(e) => e,
|
||||
},
|
||||
);
|
||||
if let Err(e) = &res {
|
||||
debug!("/auth request {req_id} | error: {e:#}");
|
||||
}
|
||||
let elapsed = timer.elapsed();
|
||||
debug!("/auth request {req_id} | resolve time: {elapsed:?}");
|
||||
Ok((TypedHeader(ContentType::json()), res?))
|
||||
}
|
||||
|
||||
fn login_options_reponse() -> &'static GetLoginOptionsResponse {
|
||||
static GET_LOGIN_OPTIONS_RESPONSE: OnceLock<
|
||||
GetLoginOptionsResponse,
|
||||
> = OnceLock::new();
|
||||
GET_LOGIN_OPTIONS_RESPONSE.get_or_init(|| {
|
||||
let config = core_config();
|
||||
GetLoginOptionsResponse {
|
||||
local: config.local_auth,
|
||||
github: config.github_oauth.enabled
|
||||
&& !config.github_oauth.id.is_empty()
|
||||
&& !config.github_oauth.secret.is_empty(),
|
||||
google: config.google_oauth.enabled
|
||||
&& !config.google_oauth.id.is_empty()
|
||||
&& !config.google_oauth.secret.is_empty(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetLoginOptions, HeaderMap> for State {
|
||||
#[instrument(name = "GetLoginOptions", level = "debug", skip(self))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
_: GetLoginOptions,
|
||||
_: HeaderMap,
|
||||
) -> anyhow::Result<GetLoginOptionsResponse> {
|
||||
Ok(*login_options_reponse())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ExchangeForJwt, HeaderMap> for State {
|
||||
#[instrument(name = "ExchangeForJwt", level = "debug", skip(self))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
ExchangeForJwt { token }: ExchangeForJwt,
|
||||
_: HeaderMap,
|
||||
) -> anyhow::Result<ExchangeForJwtResponse> {
|
||||
let jwt = jwt_client().redeem_exchange_token(&token).await?;
|
||||
let res = ExchangeForJwtResponse { jwt };
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetUser, HeaderMap> for State {
|
||||
#[instrument(name = "GetUser", level = "debug", skip(self))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetUser {}: GetUser,
|
||||
headers: HeaderMap,
|
||||
) -> anyhow::Result<User> {
|
||||
let user_id = get_user_id_from_headers(&headers).await?;
|
||||
get_user(&user_id).await
|
||||
}
|
||||
}
|
||||
534
bin/core/src/api/execute/build.rs
Normal file
534
bin/core/src/api/execute/build.rs
Normal file
@@ -0,0 +1,534 @@
|
||||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
use futures::future::join_all;
|
||||
use monitor_client::{
|
||||
api::execute::{
|
||||
CancelBuild, CancelBuildResponse, Deploy, RunBuild,
|
||||
},
|
||||
entities::{
|
||||
all_logs_success,
|
||||
build::Build,
|
||||
builder::{AwsBuilderConfig, Builder, BuilderConfig},
|
||||
deployment::DockerContainerState,
|
||||
monitor_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
server_template::AwsServerTemplateConfig,
|
||||
update::{Log, Update},
|
||||
user::{auto_redeploy_user, User},
|
||||
Operation,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::bson::{doc, oid::ObjectId, to_bson},
|
||||
};
|
||||
use periphery_client::{
|
||||
api::{self, GetVersionResponse},
|
||||
PeripheryClient,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use serror::{serialize_error, serialize_error_pretty};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
cloud::{
|
||||
aws::{
|
||||
launch_ec2_instance, terminate_ec2_instance_with_retry,
|
||||
Ec2Instance,
|
||||
},
|
||||
BuildCleanupData,
|
||||
},
|
||||
config::core_config,
|
||||
helpers::{
|
||||
channel::build_cancel_channel,
|
||||
periphery_client,
|
||||
query::get_deployment_state,
|
||||
resource::StateResource,
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
state::{action_states, db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<RunBuild, User> for State {
|
||||
#[instrument(name = "RunBuild", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
RunBuild { build }: RunBuild,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let mut build = Build::get_resource_check_permissions(
|
||||
&build,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 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)?;
|
||||
|
||||
build.config.version.increment();
|
||||
|
||||
let mut update = make_update(&build, Operation::RunBuild, &user);
|
||||
update.in_progress();
|
||||
update.version = build.config.version.clone();
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
let cancel_clone = cancel.clone();
|
||||
let mut cancel_recv =
|
||||
build_cancel_channel().receiver.resubscribe();
|
||||
let build_id = build.id.clone();
|
||||
|
||||
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 {
|
||||
info!("build cancel acknowledged");
|
||||
update.push_simple_log(
|
||||
"cancel acknowledged",
|
||||
"the build cancellation has been queud, it may still take some time",
|
||||
);
|
||||
update.finalize();
|
||||
let id = update.id.clone();
|
||||
if let Err(e) = update_update(update).await {
|
||||
warn!("failed to update Update {id} | {e:#}");
|
||||
}
|
||||
cancel_clone.cancel();
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
#[allow(unreachable_code)]
|
||||
anyhow::Ok(())
|
||||
};
|
||||
tokio::select! {
|
||||
_ = cancel_clone.cancelled() => {}
|
||||
_ = poll => {}
|
||||
}
|
||||
});
|
||||
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
// GET BUILDER PERIPHERY
|
||||
|
||||
let (periphery, cleanup_data) =
|
||||
match get_build_builder(&build, &mut update).await {
|
||||
Ok(builder) => {
|
||||
info!("got builder for build");
|
||||
builder
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("failed to get builder | {e:#}");
|
||||
update.logs.push(Log::error(
|
||||
"get builder",
|
||||
serialize_error_pretty(&e),
|
||||
));
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
return Ok(update);
|
||||
}
|
||||
};
|
||||
|
||||
let core_config = core_config();
|
||||
|
||||
// CLONE REPO
|
||||
|
||||
let github_token = core_config
|
||||
.github_accounts
|
||||
.get(&build.config.github_account)
|
||||
.cloned();
|
||||
|
||||
let res = tokio::select! {
|
||||
res = periphery
|
||||
.request(api::git::CloneRepo {
|
||||
args: (&build).into(),
|
||||
github_token,
|
||||
}) => res,
|
||||
_ = cancel.cancelled() => {
|
||||
info!("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");
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
return Ok(update)
|
||||
},
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(clone_logs) => {
|
||||
info!("finished repo clone");
|
||||
update.logs.extend(clone_logs);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("failed build at clone repo | {e:#}");
|
||||
update.push_error_log("clone repo", serialize_error(&e));
|
||||
}
|
||||
}
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
if all_logs_success(&update.logs) {
|
||||
let docker_token = core_config
|
||||
.docker_accounts
|
||||
.get(&build.config.docker_account)
|
||||
.cloned();
|
||||
|
||||
let res = tokio::select! {
|
||||
res = periphery
|
||||
.request(api::build::Build {
|
||||
build: build.clone(),
|
||||
docker_token,
|
||||
}) => 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;
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
return Ok(update)
|
||||
},
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(logs) => {
|
||||
info!("finished build");
|
||||
update.logs.extend(logs);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("error in build | {e:#}");
|
||||
update.push_error_log("build", serialize_error(&e))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
|
||||
if update.success {
|
||||
let _ = db_client()
|
||||
.await
|
||||
.builds
|
||||
.update_one(
|
||||
doc! { "_id": ObjectId::from_str(&build.id)? },
|
||||
doc! {
|
||||
"$set": {
|
||||
"config.version": to_bson(&build.config.version)
|
||||
.context("failed at converting version to bson")?,
|
||||
"info.last_built_at": monitor_timestamp(),
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
cancel.cancel();
|
||||
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
|
||||
info!("builder instance cleaned up");
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
if update.success {
|
||||
handle_post_build_redeploy(&build.id).await;
|
||||
info!("post build redeploy handled");
|
||||
}
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CancelBuild, User> for State {
|
||||
#[instrument(name = "CancelBuild", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CancelBuild { build }: CancelBuild,
|
||||
user: User,
|
||||
) -> anyhow::Result<CancelBuildResponse> {
|
||||
let build = Build::get_resource_check_permissions(
|
||||
&build,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// check if theres already an open cancel build update
|
||||
if db_client()
|
||||
.await
|
||||
.updates
|
||||
.find_one(
|
||||
doc! {
|
||||
"operation": "CancelBuild",
|
||||
"status": "InProgress",
|
||||
"target.id": &build.id,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to query updates")?
|
||||
.is_some()
|
||||
{
|
||||
return Err(anyhow!("Build cancel is already in progress"));
|
||||
}
|
||||
|
||||
let mut update =
|
||||
make_update(&build, Operation::CancelBuild, &user);
|
||||
|
||||
update.push_simple_log(
|
||||
"cancel triggered",
|
||||
"the build cancel has been triggered",
|
||||
);
|
||||
update.in_progress();
|
||||
|
||||
update.id =
|
||||
add_update(make_update(&build, Operation::CancelBuild, &user))
|
||||
.await?;
|
||||
|
||||
build_cancel_channel()
|
||||
.sender
|
||||
.lock()
|
||||
.await
|
||||
.send((build.id, update))?;
|
||||
|
||||
Ok(CancelBuildResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
const BUILDER_POLL_RATE_SECS: u64 = 2;
|
||||
const BUILDER_POLL_MAX_TRIES: usize = 30;
|
||||
|
||||
#[instrument]
|
||||
async fn get_build_builder(
|
||||
build: &Build,
|
||||
update: &mut Update,
|
||||
) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {
|
||||
if build.config.builder_id.is_empty() {
|
||||
return Err(anyhow!("build has not configured a builder"));
|
||||
}
|
||||
let builder =
|
||||
Builder::get_resource(&build.config.builder_id).await?;
|
||||
match builder.config {
|
||||
BuilderConfig::Server(config) => {
|
||||
if config.server_id.is_empty() {
|
||||
return Err(anyhow!("builder has not configured a server"));
|
||||
}
|
||||
let server = Server::get_resource(&config.server_id).await?;
|
||||
let periphery = periphery_client(&server)?;
|
||||
Ok((
|
||||
periphery,
|
||||
BuildCleanupData::Server {
|
||||
repo_name: build.name.clone(),
|
||||
},
|
||||
))
|
||||
}
|
||||
BuilderConfig::Aws(config) => {
|
||||
get_aws_builder(build, config, update).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_aws_builder(
|
||||
build: &Build,
|
||||
config: AwsBuilderConfig,
|
||||
update: &mut Update,
|
||||
) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {
|
||||
let start_create_ts = monitor_timestamp();
|
||||
|
||||
let instance_name = format!(
|
||||
"BUILDER-{}-v{}",
|
||||
build.name,
|
||||
build.config.version.to_string()
|
||||
);
|
||||
let Ec2Instance { instance_id, ip } = launch_ec2_instance(
|
||||
&instance_name,
|
||||
AwsServerTemplateConfig::from_builder_config(&config),
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("ec2 instance launched");
|
||||
|
||||
let log = Log {
|
||||
stage: "start build instance".to_string(),
|
||||
success: true,
|
||||
stdout: start_aws_builder_log(&instance_id, &ip, &config),
|
||||
start_ts: start_create_ts,
|
||||
end_ts: monitor_timestamp(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let periphery_address = format!("http://{ip}:{}", config.port);
|
||||
let periphery =
|
||||
PeripheryClient::new(&periphery_address, &core_config().passkey);
|
||||
|
||||
let start_connect_ts = monitor_timestamp();
|
||||
let mut res = Ok(GetVersionResponse {
|
||||
version: String::new(),
|
||||
});
|
||||
for _ in 0..BUILDER_POLL_MAX_TRIES {
|
||||
let version = periphery
|
||||
.request(api::GetVersion {})
|
||||
.await
|
||||
.context("failed to reach periphery client on builder");
|
||||
if let Ok(GetVersionResponse { version }) = &version {
|
||||
let connect_log = Log {
|
||||
stage: "build instance connected".to_string(),
|
||||
success: true,
|
||||
stdout: format!(
|
||||
"established contact with periphery on builder\nperiphery version: v{}",
|
||||
version
|
||||
),
|
||||
start_ts: start_connect_ts,
|
||||
end_ts: monitor_timestamp(),
|
||||
..Default::default()
|
||||
};
|
||||
update.logs.push(connect_log);
|
||||
update_update(update.clone()).await?;
|
||||
return Ok((
|
||||
periphery,
|
||||
BuildCleanupData::Aws {
|
||||
instance_id,
|
||||
region: config.region,
|
||||
},
|
||||
));
|
||||
}
|
||||
res = version;
|
||||
tokio::time::sleep(Duration::from_secs(BUILDER_POLL_RATE_SECS))
|
||||
.await;
|
||||
}
|
||||
tokio::spawn(async move {
|
||||
let _ =
|
||||
terminate_ec2_instance_with_retry(config.region, &instance_id)
|
||||
.await;
|
||||
});
|
||||
|
||||
// Unwrap is safe, only way to get here is after check Ok / early return, so it must be err
|
||||
Err(res.err().unwrap())
|
||||
}
|
||||
|
||||
#[instrument(skip(periphery))]
|
||||
async fn cleanup_builder_instance(
|
||||
periphery: PeripheryClient,
|
||||
cleanup_data: BuildCleanupData,
|
||||
update: &mut Update,
|
||||
) {
|
||||
match cleanup_data {
|
||||
BuildCleanupData::Server { repo_name } => {
|
||||
let _ = periphery
|
||||
.request(api::git::DeleteRepo { name: repo_name })
|
||||
.await;
|
||||
}
|
||||
BuildCleanupData::Aws {
|
||||
instance_id,
|
||||
region,
|
||||
} => {
|
||||
let _instance_id = instance_id.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ =
|
||||
terminate_ec2_instance_with_retry(region, &_instance_id)
|
||||
.await;
|
||||
});
|
||||
update.push_simple_log(
|
||||
"terminate instance",
|
||||
format!("termination queued for instance id {instance_id}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn handle_post_build_redeploy(build_id: &str) {
|
||||
let Ok(redeploy_deployments) = find_collect(
|
||||
&db_client().await.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).await.unwrap_or_default();
|
||||
if state == DockerContainerState::Running {
|
||||
let res = State
|
||||
.resolve(
|
||||
Deploy {
|
||||
deployment: deployment.id.clone(),
|
||||
stop_signal: None,
|
||||
stop_time: None,
|
||||
},
|
||||
auto_redeploy_user().to_owned(),
|
||||
)
|
||||
.await;
|
||||
Some((deployment.id.clone(), res))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let redeploy_results = join_all(futures).await;
|
||||
|
||||
let mut redeploys = Vec::<String>::new();
|
||||
let mut redeploy_failures = Vec::<String>::new();
|
||||
|
||||
for res in redeploy_results {
|
||||
if res.is_none() {
|
||||
continue;
|
||||
}
|
||||
let (id, res) = res.unwrap();
|
||||
match res {
|
||||
Ok(_) => redeploys.push(id),
|
||||
Err(e) => redeploy_failures.push(format!("{id}: {e:#?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_aws_builder_log(
|
||||
instance_id: &str,
|
||||
ip: &str,
|
||||
config: &AwsBuilderConfig,
|
||||
) -> String {
|
||||
let AwsBuilderConfig {
|
||||
ami_id,
|
||||
instance_type,
|
||||
volume_gb,
|
||||
subnet_id,
|
||||
assign_public_ip,
|
||||
security_group_ids,
|
||||
use_public_ip,
|
||||
..
|
||||
} = config;
|
||||
|
||||
let readable_sec_group_ids = security_group_ids.join(", ");
|
||||
|
||||
format!("instance id: {instance_id}\nip: {ip}\nami id: {ami_id}\ninstance type: {instance_type}\nvolume size: {volume_gb} GB\nsubnet id: {subnet_id}\nsecurity groups: {readable_sec_group_ids}\nassign public ip: {assign_public_ip}\nuse public ip: {use_public_ip}")
|
||||
}
|
||||
455
bin/core/src/api/execute/deployment.rs
Normal file
455
bin/core/src/api/execute/deployment.rs
Normal file
@@ -0,0 +1,455 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
use futures::future::join_all;
|
||||
use monitor_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
build::Build,
|
||||
deployment::{Deployment, DeploymentImage},
|
||||
get_image_name, monitor_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::ServerStatus,
|
||||
update::{Log, ResourceTarget, Update, UpdateStatus},
|
||||
user::User,
|
||||
Operation, Version,
|
||||
},
|
||||
};
|
||||
use mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::get_server_with_status,
|
||||
resource::StateResource,
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
monitor::update_cache_for_server,
|
||||
state::{action_states, db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<Deploy, User> for State {
|
||||
#[instrument(name = "Deploy", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
Deploy {
|
||||
deployment,
|
||||
stop_signal,
|
||||
stop_time,
|
||||
}: Deploy,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let mut deployment = Deployment::get_resource_check_permissions(
|
||||
&deployment,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 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)?;
|
||||
|
||||
if deployment.config.server_id.is_empty() {
|
||||
return Err(anyhow!("deployment has no server configured"));
|
||||
}
|
||||
|
||||
let (server, status) =
|
||||
get_server_with_status(&deployment.config.server_id).await?;
|
||||
if status != ServerStatus::Ok {
|
||||
return Err(anyhow!(
|
||||
"cannot send action when server is unreachable or disabled"
|
||||
));
|
||||
}
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let version = match deployment.config.image {
|
||||
DeploymentImage::Build { build_id, version } => {
|
||||
let build = Build::get_resource(&build_id).await?;
|
||||
let image_name = get_image_name(&build);
|
||||
let version = if version.is_none() {
|
||||
build.config.version
|
||||
} else {
|
||||
version
|
||||
};
|
||||
deployment.config.image = DeploymentImage::Image {
|
||||
image: format!("{image_name}:{}", version.to_string()),
|
||||
};
|
||||
if deployment.config.docker_account.is_empty() {
|
||||
deployment.config.docker_account =
|
||||
build.config.docker_account;
|
||||
}
|
||||
version
|
||||
}
|
||||
DeploymentImage::Image { .. } => Version::default(),
|
||||
};
|
||||
|
||||
let mut update =
|
||||
make_update(&deployment, Operation::DeployContainer, &user);
|
||||
update.in_progress();
|
||||
update.version = version;
|
||||
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
let docker_token = core_config()
|
||||
.docker_accounts
|
||||
.get(&deployment.config.docker_account)
|
||||
.cloned();
|
||||
|
||||
match periphery
|
||||
.request(api::container::Deploy {
|
||||
deployment,
|
||||
stop_signal,
|
||||
stop_time,
|
||||
docker_token,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"deploy container",
|
||||
serialize_error_pretty(&e),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
update_cache_for_server(&server).await;
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<StartContainer, User> for State {
|
||||
#[instrument(name = "StartContainer", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
StartContainer { deployment }: StartContainer,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let deployment = Deployment::get_resource_check_permissions(
|
||||
&deployment,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 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)?;
|
||||
|
||||
if deployment.config.server_id.is_empty() {
|
||||
return Err(anyhow!("deployment has no server configured"));
|
||||
}
|
||||
|
||||
let (server, status) =
|
||||
get_server_with_status(&deployment.config.server_id).await?;
|
||||
if status != ServerStatus::Ok {
|
||||
return Err(anyhow!(
|
||||
"cannot send action when server is unreachable or disabled"
|
||||
));
|
||||
}
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let start_ts = monitor_timestamp();
|
||||
|
||||
let mut update = Update {
|
||||
target: ResourceTarget::Deployment(deployment.id.clone()),
|
||||
operation: Operation::StartContainer,
|
||||
start_ts,
|
||||
status: UpdateStatus::InProgress,
|
||||
success: true,
|
||||
operator: user.id.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
let log = match periphery
|
||||
.request(api::container::StartContainer {
|
||||
name: deployment.name.clone(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => {
|
||||
Log::error("start container", serialize_error_pretty(&e))
|
||||
}
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update.finalize();
|
||||
update_cache_for_server(&server).await;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<StopContainer, User> for State {
|
||||
#[instrument(name = "StopContainer", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
StopContainer {
|
||||
deployment,
|
||||
signal,
|
||||
time,
|
||||
}: StopContainer,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let deployment = Deployment::get_resource_check_permissions(
|
||||
&deployment,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 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)?;
|
||||
|
||||
if deployment.config.server_id.is_empty() {
|
||||
return Err(anyhow!("deployment has no server configured"));
|
||||
}
|
||||
|
||||
let (server, status) =
|
||||
get_server_with_status(&deployment.config.server_id).await?;
|
||||
if status != ServerStatus::Ok {
|
||||
return Err(anyhow!(
|
||||
"cannot send action when server is unreachable or disabled"
|
||||
));
|
||||
}
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let mut update =
|
||||
make_update(&deployment, Operation::StopContainer, &user);
|
||||
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
let log = match periphery
|
||||
.request(api::container::StopContainer {
|
||||
name: deployment.name.clone(),
|
||||
signal: signal
|
||||
.unwrap_or(deployment.config.termination_signal)
|
||||
.into(),
|
||||
time: time
|
||||
.unwrap_or(deployment.config.termination_timeout)
|
||||
.into(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => {
|
||||
Log::error("stop container", serialize_error_pretty(&e))
|
||||
}
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update.finalize();
|
||||
update_cache_for_server(&server).await;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<StopAllContainers, User> for State {
|
||||
#[instrument(name = "StopAllContainers", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
StopAllContainers { server }: StopAllContainers,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let (server, status) = get_server_with_status(&server).await?;
|
||||
if status != ServerStatus::Ok {
|
||||
return Err(anyhow!(
|
||||
"cannot send action when server is unreachable or disabled"
|
||||
));
|
||||
}
|
||||
|
||||
// get the action state for the server (or insert default).
|
||||
let action_state = action_states()
|
||||
.server
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure server 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_containers = true)?;
|
||||
|
||||
let deployments = find_collect(
|
||||
&db_client().await.deployments,
|
||||
doc! {
|
||||
"config.server_id": &server.id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to find deployments on server")?;
|
||||
|
||||
let mut update =
|
||||
make_update(&server, Operation::StopAllContainers, &user);
|
||||
update.in_progress();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
let futures = deployments.iter().map(|deployment| async {
|
||||
(
|
||||
self
|
||||
.resolve(
|
||||
StopContainer {
|
||||
deployment: deployment.id.clone(),
|
||||
signal: None,
|
||||
time: None,
|
||||
},
|
||||
user.clone(),
|
||||
)
|
||||
.await,
|
||||
deployment.name.clone(),
|
||||
deployment.id.clone(),
|
||||
)
|
||||
});
|
||||
let results = join_all(futures).await;
|
||||
let deployment_names = deployments
|
||||
.iter()
|
||||
.map(|d| format!("{} ({})", d.name, d.id))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
update.push_simple_log("stopping containers", deployment_names);
|
||||
for (res, name, id) in results {
|
||||
if let Err(e) = res {
|
||||
update.push_error_log(
|
||||
"stop container failure",
|
||||
format!(
|
||||
"failed to stop container {name} ({id})\n\n{}",
|
||||
serialize_error_pretty(&e)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<RemoveContainer, User> for State {
|
||||
#[instrument(name = "RemoveContainer", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
RemoveContainer {
|
||||
deployment,
|
||||
signal,
|
||||
time,
|
||||
}: RemoveContainer,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let deployment = Deployment::get_resource_check_permissions(
|
||||
&deployment,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 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.removing = true)?;
|
||||
|
||||
if deployment.config.server_id.is_empty() {
|
||||
return Err(anyhow!("deployment has no server configured"));
|
||||
}
|
||||
|
||||
let (server, status) =
|
||||
get_server_with_status(&deployment.config.server_id).await?;
|
||||
if status != ServerStatus::Ok {
|
||||
return Err(anyhow!(
|
||||
"cannot send action when server is unreachable or disabled"
|
||||
));
|
||||
}
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let start_ts = monitor_timestamp();
|
||||
|
||||
let mut update = Update {
|
||||
target: ResourceTarget::Deployment(deployment.id.clone()),
|
||||
operation: Operation::RemoveContainer,
|
||||
start_ts,
|
||||
status: UpdateStatus::InProgress,
|
||||
success: true,
|
||||
operator: user.id.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
let log = match periphery
|
||||
.request(api::container::RemoveContainer {
|
||||
name: deployment.name.clone(),
|
||||
signal: signal
|
||||
.unwrap_or(deployment.config.termination_signal)
|
||||
.into(),
|
||||
time: time
|
||||
.unwrap_or(deployment.config.termination_timeout)
|
||||
.into(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => {
|
||||
Log::error("stop container", serialize_error_pretty(&e))
|
||||
}
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update.finalize();
|
||||
update_cache_for_server(&server).await;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
109
bin/core/src/api/execute/mod.rs
Normal file
109
bin/core/src/api/execute/mod.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{middleware, routing::post, Extension, Router};
|
||||
use axum_extra::{headers::ContentType, TypedHeader};
|
||||
use monitor_client::{api::execute::*, entities::user::User};
|
||||
use resolver_api::{derive::Resolver, Resolve, Resolver};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serror::Json;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{auth::auth_request, state::State};
|
||||
|
||||
mod build;
|
||||
mod deployment;
|
||||
mod procedure;
|
||||
mod repo;
|
||||
mod server;
|
||||
mod server_template;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
|
||||
#[resolver_target(State)]
|
||||
#[resolver_args(User)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
enum ExecuteRequest {
|
||||
// ==== SERVER ====
|
||||
PruneContainers(PruneDockerContainers),
|
||||
PruneImages(PruneDockerImages),
|
||||
PruneNetworks(PruneDockerNetworks),
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
Deploy(Deploy),
|
||||
StartContainer(StartContainer),
|
||||
StopContainer(StopContainer),
|
||||
StopAllContainers(StopAllContainers),
|
||||
RemoveContainer(RemoveContainer),
|
||||
|
||||
// ==== BUILD ====
|
||||
RunBuild(RunBuild),
|
||||
CancelBuild(CancelBuild),
|
||||
|
||||
// ==== REPO ====
|
||||
CloneRepo(CloneRepo),
|
||||
PullRepo(PullRepo),
|
||||
|
||||
// ==== PROCEDURE ====
|
||||
RunProcedure(RunProcedure),
|
||||
|
||||
// ==== SERVER TEMPLATE ====
|
||||
LaunchServer(LaunchServer),
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.layer(middleware::from_fn(auth_request))
|
||||
}
|
||||
|
||||
async fn handler(
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<ExecuteRequest>,
|
||||
) -> serror::Result<(TypedHeader<ContentType>, String)> {
|
||||
let req_id = Uuid::new_v4();
|
||||
|
||||
let res = tokio::spawn(task(req_id, request, user))
|
||||
.await
|
||||
.context("failure in spawned execute task");
|
||||
|
||||
if let Err(e) = &res {
|
||||
warn!("/execute request {req_id} spawn error: {e:#}",);
|
||||
}
|
||||
|
||||
Ok((TypedHeader(ContentType::json()), res??))
|
||||
}
|
||||
|
||||
#[instrument(name = "ExecuteRequest", skip(user))]
|
||||
async fn task(
|
||||
req_id: Uuid,
|
||||
request: ExecuteRequest,
|
||||
user: User,
|
||||
) -> anyhow::Result<String> {
|
||||
info!(
|
||||
"/execute request {req_id} | user: {} ({})",
|
||||
user.username, user.id
|
||||
);
|
||||
let timer = Instant::now();
|
||||
|
||||
let res =
|
||||
State
|
||||
.resolve_request(request, user)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
resolver_api::Error::Serialization(e) => {
|
||||
anyhow!("{e:?}").context("response serialization error")
|
||||
}
|
||||
resolver_api::Error::Inner(e) => e,
|
||||
});
|
||||
|
||||
if let Err(e) = &res {
|
||||
warn!("/execute request {req_id} error: {e:#}");
|
||||
}
|
||||
|
||||
let elapsed = timer.elapsed();
|
||||
info!("/execute request {req_id} | resolve time: {elapsed:?}");
|
||||
|
||||
res
|
||||
}
|
||||
83
bin/core/src/api/execute/procedure.rs
Normal file
83
bin/core/src/api/execute/procedure.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::execute::RunProcedure,
|
||||
entities::{
|
||||
permission::PermissionLevel, procedure::Procedure,
|
||||
update::Update, user::User, Operation,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
procedure::execute_procedure,
|
||||
resource::StateResource,
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
state::{action_states, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<RunProcedure, User> for State {
|
||||
#[instrument(name = "RunProcedure", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
RunProcedure { procedure }: RunProcedure,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let procedure = Procedure::get_resource_check_permissions(
|
||||
&procedure,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 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)?;
|
||||
|
||||
let mut update =
|
||||
make_update(&procedure, Operation::RunProcedure, &user);
|
||||
update.in_progress();
|
||||
update.push_simple_log(
|
||||
"execute procedure",
|
||||
format!("Executing procedure: {}", procedure.name),
|
||||
);
|
||||
|
||||
update.id = add_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",
|
||||
"the procedure has completed with no errors",
|
||||
);
|
||||
}
|
||||
Err(e) => update.push_error_log(
|
||||
"execution error",
|
||||
serialize_error_pretty(&e),
|
||||
),
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
205
bin/core/src/api/execute/repo.rs
Normal file
205
bin/core/src/api/execute/repo.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
monitor_timestamp, optional_string,
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
update::{Log, ResourceTarget, Update, UpdateStatus},
|
||||
user::User,
|
||||
Operation,
|
||||
},
|
||||
};
|
||||
use mungos::mongodb::bson::{doc, oid::ObjectId};
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{
|
||||
periphery_client,
|
||||
resource::StateResource,
|
||||
update::{add_update, update_update},
|
||||
},
|
||||
state::{action_states, db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CloneRepo, User> for State {
|
||||
#[instrument(name = "CloneRepo", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CloneRepo { repo }: CloneRepo,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let repo = Repo::get_resource_check_permissions(
|
||||
&repo,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.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)?;
|
||||
|
||||
if repo.config.server_id.is_empty() {
|
||||
return Err(anyhow!("repo has no server attached"));
|
||||
}
|
||||
|
||||
let server = Server::get_resource(&repo.config.server_id).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let start_ts = monitor_timestamp();
|
||||
|
||||
let mut update = Update {
|
||||
operation: Operation::CloneRepo,
|
||||
target: ResourceTarget::Repo(repo.id.clone()),
|
||||
start_ts,
|
||||
status: UpdateStatus::InProgress,
|
||||
operator: user.id.clone(),
|
||||
success: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
let github_token = core_config()
|
||||
.github_accounts
|
||||
.get(&repo.config.github_account)
|
||||
.cloned();
|
||||
|
||||
let logs = match periphery
|
||||
.request(api::git::CloneRepo {
|
||||
args: (&repo).into(),
|
||||
github_token,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(logs) => logs,
|
||||
Err(e) => {
|
||||
vec![Log::error("clone repo", serialize_error_pretty(&e))]
|
||||
}
|
||||
};
|
||||
|
||||
update.logs.extend(logs);
|
||||
update.finalize();
|
||||
|
||||
if update.success {
|
||||
let res = db_client().await
|
||||
.repos
|
||||
.update_one(
|
||||
doc! { "_id": ObjectId::from_str(&repo.id)? },
|
||||
doc! { "$set": { "info.last_pulled_at": monitor_timestamp() } },
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"failed to update repo last_pulled_at | repo id: {} | {e:#}",
|
||||
repo.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<PullRepo, User> for State {
|
||||
#[instrument(name = "PullRepo", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
PullRepo { repo }: PullRepo,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let repo = Repo::get_resource_check_permissions(
|
||||
&repo,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.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)?;
|
||||
|
||||
if repo.config.server_id.is_empty() {
|
||||
return Err(anyhow!("repo has no server attached"));
|
||||
}
|
||||
|
||||
let server = Server::get_resource(&repo.config.server_id).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let start_ts = monitor_timestamp();
|
||||
|
||||
let mut update = Update {
|
||||
operation: Operation::PullRepo,
|
||||
target: ResourceTarget::Repo(repo.id.clone()),
|
||||
start_ts,
|
||||
status: UpdateStatus::InProgress,
|
||||
operator: user.id.clone(),
|
||||
success: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
let logs = match periphery
|
||||
.request(api::git::PullRepo {
|
||||
name: repo.name,
|
||||
branch: optional_string(&repo.config.branch),
|
||||
on_pull: repo.config.on_pull.into_option(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(logs) => logs,
|
||||
Err(e) => {
|
||||
vec![Log::error("pull repo", serialize_error_pretty(&e))]
|
||||
}
|
||||
};
|
||||
|
||||
update.logs.extend(logs);
|
||||
|
||||
update.finalize();
|
||||
|
||||
if update.success {
|
||||
let res = db_client().await
|
||||
.repos
|
||||
.update_one(
|
||||
doc! { "_id": ObjectId::from_str(&repo.id)? },
|
||||
doc! { "$set": { "info.last_pulled_at": monitor_timestamp() } },
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"failed to update repo last_pulled_at | repo id: {} | {e:#}",
|
||||
repo.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
194
bin/core/src/api/execute/server.rs
Normal file
194
bin/core/src/api/execute/server.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use anyhow::Context;
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
monitor_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
update::{Log, Update, UpdateStatus},
|
||||
user::User,
|
||||
Operation,
|
||||
},
|
||||
};
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
periphery_client,
|
||||
resource::StateResource,
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
state::{action_states, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<PruneDockerContainers, User> for State {
|
||||
#[instrument(name = "PruneDockerContainers", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
PruneDockerContainers { server }: PruneDockerContainers,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// get the action state for the server (or insert default).
|
||||
let action_state = action_states()
|
||||
.server
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure server 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.pruning_containers = true)?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let mut update =
|
||||
make_update(&server, Operation::PruneContainersServer, &user);
|
||||
update.in_progress();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
let log = match periphery
|
||||
.request(api::container::PruneContainers {})
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to prune containers on server {}",
|
||||
server.name
|
||||
)) {
|
||||
Ok(log) => log,
|
||||
Err(e) => {
|
||||
Log::error("prune containers", serialize_error_pretty(&e))
|
||||
}
|
||||
};
|
||||
|
||||
update.success = log.success;
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.end_ts = Some(monitor_timestamp());
|
||||
update.logs.push(log);
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<PruneDockerNetworks, User> for State {
|
||||
#[instrument(name = "PruneDockerNetworks", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
PruneDockerNetworks { server }: PruneDockerNetworks,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// get the action state for the server (or insert default).
|
||||
let action_state = action_states()
|
||||
.server
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure server 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.pruning_networks = true)?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let mut update =
|
||||
make_update(&server, Operation::PruneNetworksServer, &user);
|
||||
update.in_progress();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
let log = match periphery
|
||||
.request(api::network::PruneNetworks {})
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to prune networks on server {}",
|
||||
server.name
|
||||
)) {
|
||||
Ok(log) => log,
|
||||
Err(e) => {
|
||||
Log::error("prune networks", serialize_error_pretty(&e))
|
||||
}
|
||||
};
|
||||
|
||||
update.success = log.success;
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.end_ts = Some(monitor_timestamp());
|
||||
update.logs.push(log);
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<PruneDockerImages, User> for State {
|
||||
#[instrument(name = "PruneDockerImages", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
PruneDockerImages { server }: PruneDockerImages,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// get the action state for the server (or insert default).
|
||||
let action_state = action_states()
|
||||
.server
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure server 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.pruning_images = true)?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let mut update =
|
||||
make_update(&server, Operation::PruneImagesServer, &user);
|
||||
update.in_progress();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
let log =
|
||||
match periphery.request(api::build::PruneImages {}).await {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune images",
|
||||
format!(
|
||||
"failed to prune images on server {} | {e:#?}",
|
||||
server.name
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
|
||||
update.finalize();
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
89
bin/core/src/api/execute/server_template.rs
Normal file
89
bin/core/src/api/execute/server_template.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use axum::async_trait;
|
||||
use monitor_client::{
|
||||
api::{execute::LaunchServer, write::CreateServer},
|
||||
entities::{
|
||||
permission::PermissionLevel,
|
||||
server::PartialServerConfig,
|
||||
server_template::{ServerTemplate, ServerTemplateConfig},
|
||||
update::Update,
|
||||
user::User,
|
||||
Operation,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
cloud::aws::launch_ec2_instance,
|
||||
helpers::{
|
||||
resource::StateResource,
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
state::State,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<LaunchServer, User> for State {
|
||||
#[instrument(name = "LaunchServer", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
LaunchServer {
|
||||
name,
|
||||
server_template,
|
||||
}: LaunchServer,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let template = ServerTemplate::get_resource_check_permissions(
|
||||
&server_template,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
let mut update =
|
||||
make_update(&template, Operation::LaunchServer, &user);
|
||||
update.push_simple_log(
|
||||
"launching server",
|
||||
format!("{:#?}", template.config),
|
||||
);
|
||||
update.id = add_update(update.clone()).await?;
|
||||
match template.config {
|
||||
ServerTemplateConfig::Aws(config) => {
|
||||
let region = config.region.clone();
|
||||
let instance = launch_ec2_instance(&name, config).await;
|
||||
if let Err(e) = &instance {
|
||||
update.push_error_log(
|
||||
"launch server",
|
||||
format!("failed to launch aws instance\n\n{e:#?}"),
|
||||
);
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
return Ok(update);
|
||||
}
|
||||
let instance = instance.unwrap();
|
||||
update.push_simple_log(
|
||||
"launch server",
|
||||
format!(
|
||||
"successfully launched server {name} on ip {}",
|
||||
instance.ip
|
||||
),
|
||||
);
|
||||
let _ = self
|
||||
.resolve(
|
||||
CreateServer {
|
||||
name,
|
||||
config: PartialServerConfig {
|
||||
address: format!("http://{}:8120", instance.ip)
|
||||
.into(),
|
||||
region: region.into(),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
user,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
4
bin/core/src/api/mod.rs
Normal file
4
bin/core/src/api/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod auth;
|
||||
pub mod execute;
|
||||
pub mod read;
|
||||
pub mod write;
|
||||
80
bin/core/src/api/read/alert.rs
Normal file
80
bin/core/src/api/read/alert.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use anyhow::Context;
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::read::{
|
||||
GetAlert, GetAlertResponse, ListAlerts, ListAlertsResponse,
|
||||
},
|
||||
entities::{deployment::Deployment, server::Server, user::User},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::find_one_by_id,
|
||||
find::find_collect,
|
||||
mongodb::{bson::doc, options::FindOptions},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::resource::StateResource,
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
const NUM_ALERTS_PER_PAGE: u64 = 20;
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListAlerts, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListAlerts { query, page }: ListAlerts,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListAlertsResponse> {
|
||||
let mut query = query.unwrap_or_default();
|
||||
if !user.admin {
|
||||
let server_ids =
|
||||
Server::get_resource_ids_for_non_admin(&user.id).await?;
|
||||
let deployment_ids =
|
||||
Deployment::get_resource_ids_for_non_admin(&user.id).await?;
|
||||
query.extend(doc! {
|
||||
"$or": [
|
||||
{ "target.type": "Server", "target.id": { "$in": &server_ids } },
|
||||
{ "target.type": "Deployment", "target.id": { "$in": &deployment_ids } },
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
let alerts = find_collect(
|
||||
&db_client().await.alerts,
|
||||
query,
|
||||
FindOptions::builder()
|
||||
.sort(doc! { "ts": -1 })
|
||||
.limit(NUM_ALERTS_PER_PAGE as i64)
|
||||
.skip(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((page + 1) as i64)
|
||||
};
|
||||
|
||||
let res = ListAlertsResponse { next_page, alerts };
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetAlert, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetAlert { id }: GetAlert,
|
||||
_: User,
|
||||
) -> anyhow::Result<GetAlertResponse> {
|
||||
find_one_by_id(&db_client().await.alerts, &id)
|
||||
.await
|
||||
.context("failed to query db for alert")?
|
||||
.context("no alert found with given id")
|
||||
}
|
||||
}
|
||||
85
bin/core/src/api/read/alerter.rs
Normal file
85
bin/core/src/api/read/alerter.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Context;
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
alerter::{Alerter, AlerterListItem},
|
||||
permission::PermissionLevel,
|
||||
update::ResourceTargetVariant,
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mungos::mongodb::bson::{doc, oid::ObjectId};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::resource::{
|
||||
get_resource_ids_for_non_admin, StateResource,
|
||||
},
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetAlerter, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetAlerter { alerter }: GetAlerter,
|
||||
user: User,
|
||||
) -> anyhow::Result<Alerter> {
|
||||
Alerter::get_resource_check_permissions(
|
||||
&alerter,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListAlerters, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListAlerters { query }: ListAlerters,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<AlerterListItem>> {
|
||||
Alerter::list_resource_list_items_for_user(query, &user).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetAlertersSummary, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetAlertersSummary {}: GetAlertersSummary,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetAlertersSummaryResponse> {
|
||||
let query = if user.admin {
|
||||
None
|
||||
} else {
|
||||
let ids = get_resource_ids_for_non_admin(
|
||||
&user.id,
|
||||
ResourceTargetVariant::Alerter,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.flat_map(|id| ObjectId::from_str(&id))
|
||||
.collect::<Vec<_>>();
|
||||
let query = doc! {
|
||||
"_id": { "$in": ids }
|
||||
};
|
||||
Some(query)
|
||||
};
|
||||
let total = db_client()
|
||||
.await
|
||||
.alerters
|
||||
.count_documents(query, None)
|
||||
.await
|
||||
.context("failed to count all alerter documents")?;
|
||||
let res = GetAlertersSummaryResponse {
|
||||
total: total as u32,
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
262
bin/core/src/api/read/build.rs
Normal file
262
bin/core/src/api/read/build.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
use std::{collections::HashMap, str::FromStr, sync::OnceLock};
|
||||
|
||||
use anyhow::Context;
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use async_trait::async_trait;
|
||||
use futures::TryStreamExt;
|
||||
use monitor_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
build::{Build, BuildActionState, BuildListItem},
|
||||
permission::PermissionLevel,
|
||||
update::{ResourceTargetVariant, UpdateStatus},
|
||||
user::User,
|
||||
Operation,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::{
|
||||
bson::{doc, oid::ObjectId},
|
||||
options::FindOptions,
|
||||
},
|
||||
};
|
||||
use resolver_api::{Resolve, ResolveToString};
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::resource::{
|
||||
get_resource_ids_for_non_admin, StateResource,
|
||||
},
|
||||
state::{action_states, db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetBuild, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetBuild { build }: GetBuild,
|
||||
user: User,
|
||||
) -> anyhow::Result<Build> {
|
||||
Build::get_resource_check_permissions(
|
||||
&build,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListBuilds, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListBuilds { query }: ListBuilds,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<BuildListItem>> {
|
||||
Build::list_resource_list_items_for_user(query, &user).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetBuildActionState, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetBuildActionState { build }: GetBuildActionState,
|
||||
user: User,
|
||||
) -> anyhow::Result<BuildActionState> {
|
||||
let build = Build::get_resource_check_permissions(
|
||||
&build,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.build
|
||||
.get(&build.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetBuildsSummary, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetBuildsSummary {}: GetBuildsSummary,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetBuildsSummaryResponse> {
|
||||
let query = if user.admin {
|
||||
None
|
||||
} else {
|
||||
let ids = get_resource_ids_for_non_admin(
|
||||
&user.id,
|
||||
ResourceTargetVariant::Build,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.flat_map(|id| ObjectId::from_str(&id))
|
||||
.collect::<Vec<_>>();
|
||||
let query = doc! {
|
||||
"_id": { "$in": ids }
|
||||
};
|
||||
Some(query)
|
||||
};
|
||||
let total = db_client()
|
||||
.await
|
||||
.builds
|
||||
.count_documents(query, None)
|
||||
.await
|
||||
.context("failed to count all build documents")?;
|
||||
let res = GetBuildsSummaryResponse {
|
||||
total: total as u32,
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
const ONE_DAY_MS: i64 = 86400000;
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetBuildMonthlyStats, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetBuildMonthlyStats { page }: GetBuildMonthlyStats,
|
||||
_: User,
|
||||
) -> anyhow::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 - page as i64 * 30 * ONE_DAY_MS;
|
||||
let open_ts = close_ts - 30 * ONE_DAY_MS;
|
||||
|
||||
let mut build_updates = db_client()
|
||||
.await
|
||||
.updates
|
||||
.find(
|
||||
doc! {
|
||||
"start_ts": {
|
||||
"$gte": open_ts,
|
||||
"$lt": close_ts
|
||||
},
|
||||
"operation": Operation::RunBuild.to_string(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.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
|
||||
}
|
||||
|
||||
const NUM_VERSIONS_PER_PAGE: u64 = 10;
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetBuildVersions, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetBuildVersions {
|
||||
build,
|
||||
page,
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
}: GetBuildVersions,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<BuildVersionResponseItem>> {
|
||||
let build = Build::get_resource_check_permissions(
|
||||
&build,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.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().await.updates,
|
||||
filter,
|
||||
FindOptions::builder()
|
||||
.sort(doc! { "_id": -1 })
|
||||
.limit(NUM_VERSIONS_PER_PAGE as i64)
|
||||
.skip(page as u64 * NUM_VERSIONS_PER_PAGE)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
fn docker_organizations() -> &'static String {
|
||||
static DOCKER_ORGANIZATIONS: OnceLock<String> = OnceLock::new();
|
||||
DOCKER_ORGANIZATIONS.get_or_init(|| {
|
||||
serde_json::to_string(&core_config().docker_organizations)
|
||||
.expect("failed to serialize docker organizations")
|
||||
})
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ResolveToString<ListDockerOrganizations, User> for State {
|
||||
async fn resolve_to_string(
|
||||
&self,
|
||||
ListDockerOrganizations {}: ListDockerOrganizations,
|
||||
_: User,
|
||||
) -> anyhow::Result<String> {
|
||||
Ok(docker_organizations().clone())
|
||||
}
|
||||
}
|
||||
136
bin/core/src/api/read/builder.rs
Normal file
136
bin/core/src/api/read/builder.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use std::{collections::HashSet, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::read::{self, *},
|
||||
entities::{
|
||||
builder::{Builder, BuilderConfig, BuilderListItem},
|
||||
permission::PermissionLevel,
|
||||
update::ResourceTargetVariant,
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mungos::mongodb::bson::{doc, oid::ObjectId};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::resource::{
|
||||
get_resource_ids_for_non_admin, StateResource,
|
||||
},
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetBuilder, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetBuilder { builder }: GetBuilder,
|
||||
user: User,
|
||||
) -> anyhow::Result<Builder> {
|
||||
Builder::get_resource_check_permissions(
|
||||
&builder,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListBuilders, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListBuilders { query }: ListBuilders,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<BuilderListItem>> {
|
||||
Builder::list_resource_list_items_for_user(query, &user).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetBuildersSummary, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetBuildersSummary {}: GetBuildersSummary,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetBuildersSummaryResponse> {
|
||||
let query = if user.admin {
|
||||
None
|
||||
} else {
|
||||
let ids = get_resource_ids_for_non_admin(
|
||||
&user.id,
|
||||
ResourceTargetVariant::Builder,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.flat_map(|id| ObjectId::from_str(&id))
|
||||
.collect::<Vec<_>>();
|
||||
let query = doc! {
|
||||
"_id": { "$in": ids }
|
||||
};
|
||||
Some(query)
|
||||
};
|
||||
let total = db_client()
|
||||
.await
|
||||
.builders
|
||||
.count_documents(query, None)
|
||||
.await
|
||||
.context("failed to count all builder documents")?;
|
||||
let res = GetBuildersSummaryResponse {
|
||||
total: total as u32,
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetBuilderAvailableAccounts, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetBuilderAvailableAccounts { builder }: GetBuilderAvailableAccounts,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetBuilderAvailableAccountsResponse> {
|
||||
let builder = Builder::get_resource_check_permissions(
|
||||
&builder,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
let (github, docker) = match builder.config {
|
||||
BuilderConfig::Aws(config) => {
|
||||
(config.github_accounts, config.docker_accounts)
|
||||
}
|
||||
BuilderConfig::Server(config) => {
|
||||
let res = self
|
||||
.resolve(
|
||||
read::GetAvailableAccounts {
|
||||
server: config.server_id,
|
||||
},
|
||||
user,
|
||||
)
|
||||
.await?;
|
||||
(res.github, res.docker)
|
||||
}
|
||||
};
|
||||
|
||||
let mut github_set = HashSet::<String>::new();
|
||||
|
||||
github_set.extend(core_config().github_accounts.keys().cloned());
|
||||
github_set.extend(github);
|
||||
|
||||
let mut github = github_set.into_iter().collect::<Vec<_>>();
|
||||
github.sort();
|
||||
|
||||
let mut docker_set = HashSet::<String>::new();
|
||||
|
||||
docker_set.extend(core_config().docker_accounts.keys().cloned());
|
||||
docker_set.extend(docker);
|
||||
|
||||
let mut docker = docker_set.into_iter().collect::<Vec<_>>();
|
||||
docker.sort();
|
||||
|
||||
Ok(GetBuilderAvailableAccountsResponse { github, docker })
|
||||
}
|
||||
}
|
||||
281
bin/core/src/api/read/deployment.rs
Normal file
281
bin/core/src/api/read/deployment.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
use std::{cmp, collections::HashSet, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
deployment::{
|
||||
Deployment, DeploymentActionState, DeploymentConfig,
|
||||
DeploymentListItem, DockerContainerState, DockerContainerStats,
|
||||
},
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
update::{Log, ResourceTargetVariant},
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::bson::{doc, oid::ObjectId},
|
||||
};
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
periphery_client,
|
||||
resource::{get_resource_ids_for_non_admin, StateResource},
|
||||
},
|
||||
state::{action_states, db_client, deployment_status_cache, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetDeployment, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetDeployment { deployment }: GetDeployment,
|
||||
user: User,
|
||||
) -> anyhow::Result<Deployment> {
|
||||
Deployment::get_resource_check_permissions(
|
||||
&deployment,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListDeployments, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListDeployments { query }: ListDeployments,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<DeploymentListItem>> {
|
||||
Deployment::list_resource_list_items_for_user(query, &user).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetDeploymentContainer, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetDeploymentContainer { deployment }: GetDeploymentContainer,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetDeploymentContainerResponse> {
|
||||
let deployment = Deployment::get_resource_check_permissions(
|
||||
&deployment,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.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;
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetLog, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetLog { deployment, tail }: GetLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<Log> {
|
||||
let Deployment {
|
||||
name,
|
||||
config: DeploymentConfig { server_id, .. },
|
||||
..
|
||||
} = Deployment::get_resource_check_permissions(
|
||||
&deployment,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
if server_id.is_empty() {
|
||||
return Ok(Log::default());
|
||||
}
|
||||
let server = Server::get_resource(&server_id).await?;
|
||||
periphery_client(&server)?
|
||||
.request(api::container::GetContainerLog {
|
||||
name,
|
||||
tail: cmp::min(tail, MAX_LOG_LENGTH),
|
||||
})
|
||||
.await
|
||||
.context("failed at call to periphery")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<SearchLog, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
SearchLog {
|
||||
deployment,
|
||||
terms,
|
||||
combinator,
|
||||
}: SearchLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<Log> {
|
||||
let Deployment {
|
||||
name,
|
||||
config: DeploymentConfig { server_id, .. },
|
||||
..
|
||||
} = Deployment::get_resource_check_permissions(
|
||||
&deployment,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
if server_id.is_empty() {
|
||||
return Ok(Log::default());
|
||||
}
|
||||
let server = Server::get_resource(&server_id).await?;
|
||||
periphery_client(&server)?
|
||||
.request(api::container::GetContainerLogSearch {
|
||||
name,
|
||||
terms,
|
||||
combinator,
|
||||
})
|
||||
.await
|
||||
.context("failed at call to periphery")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetDeploymentStats, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetDeploymentStats { deployment }: GetDeploymentStats,
|
||||
user: User,
|
||||
) -> anyhow::Result<DockerContainerStats> {
|
||||
let Deployment {
|
||||
name,
|
||||
config: DeploymentConfig { server_id, .. },
|
||||
..
|
||||
} = Deployment::get_resource_check_permissions(
|
||||
&deployment,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
if server_id.is_empty() {
|
||||
return Err(anyhow!("deployment has no server attached"));
|
||||
}
|
||||
let server = Server::get_resource(&server_id).await?;
|
||||
periphery_client(&server)?
|
||||
.request(api::container::GetContainerStats { name })
|
||||
.await
|
||||
.context("failed to get stats from periphery")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetDeploymentActionState, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetDeploymentActionState { deployment }: GetDeploymentActionState,
|
||||
user: User,
|
||||
) -> anyhow::Result<DeploymentActionState> {
|
||||
let deployment = Deployment::get_resource_check_permissions(
|
||||
&deployment,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get(&deployment.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetDeploymentsSummary, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetDeploymentsSummary {}: GetDeploymentsSummary,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetDeploymentsSummaryResponse> {
|
||||
let query = if user.admin {
|
||||
None
|
||||
} else {
|
||||
let ids = get_resource_ids_for_non_admin(
|
||||
&user.id,
|
||||
ResourceTargetVariant::Deployment,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.flat_map(|id| ObjectId::from_str(&id))
|
||||
.collect::<Vec<_>>();
|
||||
let query = doc! {
|
||||
"_id": { "$in": ids }
|
||||
};
|
||||
Some(query)
|
||||
};
|
||||
|
||||
let deployments =
|
||||
find_collect(&db_client().await.deployments, query, None)
|
||||
.await
|
||||
.context("failed to count all deployment documents")?;
|
||||
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 {
|
||||
DockerContainerState::Running => {
|
||||
res.running += 1;
|
||||
}
|
||||
DockerContainerState::Unknown => {
|
||||
res.unknown += 1;
|
||||
}
|
||||
DockerContainerState::NotDeployed => {
|
||||
res.not_deployed += 1;
|
||||
}
|
||||
_ => {
|
||||
res.stopped += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListCommonExtraArgs, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListCommonExtraArgs { query }: ListCommonExtraArgs,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListCommonExtraArgsResponse> {
|
||||
let deployments =
|
||||
Deployment::list_resources_for_user(query, &user)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res.into_iter().collect())
|
||||
}
|
||||
}
|
||||
215
bin/core/src/api/read/mod.rs
Normal file
215
bin/core/src/api/read/mod.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use async_trait::async_trait;
|
||||
use axum::{middleware, routing::post, Extension, Router};
|
||||
use axum_extra::{headers::ContentType, TypedHeader};
|
||||
use monitor_client::{api::read::*, entities::user::User};
|
||||
use resolver_api::{
|
||||
derive::Resolver, Resolve, ResolveToString, Resolver,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serror::Json;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{auth::auth_request, config::core_config, state::State};
|
||||
|
||||
mod alert;
|
||||
mod alerter;
|
||||
mod build;
|
||||
mod builder;
|
||||
mod deployment;
|
||||
mod permission;
|
||||
mod procedure;
|
||||
mod repo;
|
||||
mod search;
|
||||
mod server;
|
||||
mod server_template;
|
||||
mod tag;
|
||||
mod toml;
|
||||
mod update;
|
||||
mod user;
|
||||
mod user_group;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
|
||||
#[resolver_target(State)]
|
||||
#[resolver_args(User)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
enum ReadRequest {
|
||||
GetVersion(GetVersion),
|
||||
GetCoreInfo(GetCoreInfo),
|
||||
|
||||
// ==== USER ====
|
||||
ListUsers(ListUsers),
|
||||
GetUsername(GetUsername),
|
||||
ListApiKeys(ListApiKeys),
|
||||
ListApiKeysForServiceUser(ListApiKeysForServiceUser),
|
||||
ListPermissions(ListPermissions),
|
||||
GetPermissionLevel(GetPermissionLevel),
|
||||
ListUserTargetPermissions(ListUserTargetPermissions),
|
||||
|
||||
// ==== USER GROUP ====
|
||||
GetUserGroup(GetUserGroup),
|
||||
ListUserGroups(ListUserGroups),
|
||||
|
||||
// ==== SEARCH ====
|
||||
FindResources(FindResources),
|
||||
|
||||
// ==== PROCEDURE ====
|
||||
GetProceduresSummary(GetProceduresSummary),
|
||||
GetProcedure(GetProcedure),
|
||||
GetProcedureActionState(GetProcedureActionState),
|
||||
ListProcedures(ListProcedures),
|
||||
|
||||
// ==== SERVER TEMPLATE ====
|
||||
GetServerTemplate(GetServerTemplate),
|
||||
ListServerTemplates(ListServerTemplates),
|
||||
GetServerTemplateSummary(GetServerTemplatesSummary),
|
||||
|
||||
// ==== SERVER ====
|
||||
GetServersSummary(GetServersSummary),
|
||||
GetServer(GetServer),
|
||||
ListServers(ListServers),
|
||||
GetServerStatus(GetServerStatus),
|
||||
GetPeripheryVersion(GetPeripheryVersion),
|
||||
GetDockerContainers(GetDockerContainers),
|
||||
GetDockerImages(GetDockerImages),
|
||||
GetDockerNetworks(GetDockerNetworks),
|
||||
GetServerActionState(GetServerActionState),
|
||||
GetHistoricalServerStats(GetHistoricalServerStats),
|
||||
GetAvailableAccounts(GetAvailableAccounts),
|
||||
GetAvailableSecrets(GetAvailableSecrets),
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
GetDeploymentsSummary(GetDeploymentsSummary),
|
||||
GetDeployment(GetDeployment),
|
||||
ListDeployments(ListDeployments),
|
||||
GetDeploymentContainer(GetDeploymentContainer),
|
||||
GetDeploymentActionState(GetDeploymentActionState),
|
||||
GetDeploymentStats(GetDeploymentStats),
|
||||
GetLog(GetLog),
|
||||
SearchLog(SearchLog),
|
||||
ListCommonExtraArgs(ListCommonExtraArgs),
|
||||
|
||||
// ==== BUILD ====
|
||||
GetBuildsSummary(GetBuildsSummary),
|
||||
GetBuild(GetBuild),
|
||||
ListBuilds(ListBuilds),
|
||||
GetBuildActionState(GetBuildActionState),
|
||||
GetBuildMonthlyStats(GetBuildMonthlyStats),
|
||||
GetBuildVersions(GetBuildVersions),
|
||||
#[to_string_resolver]
|
||||
ListDockerOrganizations(ListDockerOrganizations),
|
||||
|
||||
// ==== REPO ====
|
||||
GetReposSummary(GetReposSummary),
|
||||
GetRepo(GetRepo),
|
||||
ListRepos(ListRepos),
|
||||
GetRepoActionState(GetRepoActionState),
|
||||
|
||||
// ==== BUILDER ====
|
||||
GetBuildersSummary(GetBuildersSummary),
|
||||
GetBuilder(GetBuilder),
|
||||
ListBuilders(ListBuilders),
|
||||
GetBuilderAvailableAccounts(GetBuilderAvailableAccounts),
|
||||
|
||||
// ==== ALERTER ====
|
||||
GetAlertersSummary(GetAlertersSummary),
|
||||
GetAlerter(GetAlerter),
|
||||
ListAlerters(ListAlerters),
|
||||
|
||||
// ==== TOML ====
|
||||
ExportAllResourcesToToml(ExportAllResourcesToToml),
|
||||
ExportResourcesToToml(ExportResourcesToToml),
|
||||
|
||||
// ==== TAG ====
|
||||
GetTag(GetTag),
|
||||
ListTags(ListTags),
|
||||
|
||||
// ==== UPDATE ====
|
||||
GetUpdate(GetUpdate),
|
||||
ListUpdates(ListUpdates),
|
||||
|
||||
// ==== ALERT ====
|
||||
ListAlerts(ListAlerts),
|
||||
GetAlert(GetAlert),
|
||||
|
||||
// ==== SERVER STATS ====
|
||||
#[to_string_resolver]
|
||||
GetSystemInformation(GetSystemInformation),
|
||||
#[to_string_resolver]
|
||||
GetSystemStats(GetSystemStats),
|
||||
#[to_string_resolver]
|
||||
GetSystemProcesses(GetSystemProcesses),
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.layer(middleware::from_fn(auth_request))
|
||||
}
|
||||
|
||||
#[instrument(name = "ReadHandler", level = "debug", skip(user))]
|
||||
async fn handler(
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<ReadRequest>,
|
||||
) -> serror::Result<(TypedHeader<ContentType>, String)> {
|
||||
let timer = Instant::now();
|
||||
let req_id = Uuid::new_v4();
|
||||
debug!(
|
||||
"/read request {req_id} | user: {} ({})",
|
||||
user.username, user.id
|
||||
);
|
||||
let res =
|
||||
State
|
||||
.resolve_request(request, user)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
resolver_api::Error::Serialization(e) => {
|
||||
anyhow!("{e:?}").context("response serialization error")
|
||||
}
|
||||
resolver_api::Error::Inner(e) => e,
|
||||
});
|
||||
if let Err(e) = &res {
|
||||
warn!("/read request {req_id} error: {e:#}");
|
||||
}
|
||||
let elapsed = timer.elapsed();
|
||||
debug!("/read request {req_id} | resolve time: {elapsed:?}");
|
||||
Ok((TypedHeader(ContentType::json()), res?))
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetVersion, User> for State {
|
||||
#[instrument(name = "GetVersion", level = "debug", skip(self))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetVersion {}: GetVersion,
|
||||
_: User,
|
||||
) -> anyhow::Result<GetVersionResponse> {
|
||||
Ok(GetVersionResponse {
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetCoreInfo, User> for State {
|
||||
#[instrument(name = "GetCoreInfo", level = "debug", skip(self))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetCoreInfo {}: GetCoreInfo,
|
||||
_: User,
|
||||
) -> anyhow::Result<GetCoreInfoResponse> {
|
||||
let config = core_config();
|
||||
Ok(GetCoreInfoResponse {
|
||||
title: config.title.clone(),
|
||||
monitoring_interval: config.monitoring_interval,
|
||||
github_webhook_base_url: config
|
||||
.github_webhook_base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| config.host.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
76
bin/core/src/api/read/permission.rs
Normal file
76
bin/core/src/api/read/permission.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::async_trait;
|
||||
use monitor_client::{
|
||||
api::read::{
|
||||
GetPermissionLevel, GetPermissionLevelResponse, ListPermissions,
|
||||
ListPermissionsResponse, ListUserTargetPermissions,
|
||||
ListUserTargetPermissionsResponse,
|
||||
},
|
||||
entities::{permission::PermissionLevel, user::User},
|
||||
};
|
||||
use mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::resource::get_user_permission_on_resource,
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListPermissions, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListPermissions {}: ListPermissions,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListPermissionsResponse> {
|
||||
find_collect(
|
||||
&db_client().await.permissions,
|
||||
doc! {
|
||||
"user_target.type": "User",
|
||||
"user_target.id": &user.id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to query db for permissions")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetPermissionLevel, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetPermissionLevel { target }: GetPermissionLevel,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetPermissionLevelResponse> {
|
||||
if user.admin {
|
||||
return Ok(PermissionLevel::Write);
|
||||
}
|
||||
let (variant, id) = target.extract_variant_id();
|
||||
get_user_permission_on_resource(&user.id, variant, id).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListUserTargetPermissions, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListUserTargetPermissions { user_target }: ListUserTargetPermissions,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListUserTargetPermissionsResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("this method is admin only"));
|
||||
}
|
||||
let (variant, id) = user_target.extract_variant_id();
|
||||
find_collect(
|
||||
&db_client().await.permissions,
|
||||
doc! {
|
||||
"user_target.type": variant.as_ref(),
|
||||
"user_target.id": id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to query db for permissions")
|
||||
}
|
||||
}
|
||||
111
bin/core/src/api/read/procedure.rs
Normal file
111
bin/core/src/api/read/procedure.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Context;
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::read::{
|
||||
GetProcedure, GetProcedureActionState,
|
||||
GetProcedureActionStateResponse, GetProcedureResponse,
|
||||
GetProceduresSummary, GetProceduresSummaryResponse,
|
||||
ListProcedures, ListProceduresResponse,
|
||||
},
|
||||
entities::{
|
||||
permission::PermissionLevel, procedure::Procedure,
|
||||
update::ResourceTargetVariant, user::User,
|
||||
},
|
||||
};
|
||||
use mungos::mongodb::bson::{doc, oid::ObjectId};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::resource::{
|
||||
get_resource_ids_for_non_admin, StateResource,
|
||||
},
|
||||
state::{action_states, db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetProcedure, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetProcedure { procedure }: GetProcedure,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetProcedureResponse> {
|
||||
Procedure::get_resource_check_permissions(
|
||||
&procedure,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListProcedures, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListProcedures { query }: ListProcedures,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListProceduresResponse> {
|
||||
Procedure::list_resource_list_items_for_user(query, &user).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetProceduresSummary, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetProceduresSummary {}: GetProceduresSummary,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetProceduresSummaryResponse> {
|
||||
let query = if user.admin {
|
||||
None
|
||||
} else {
|
||||
let ids = get_resource_ids_for_non_admin(
|
||||
&user.id,
|
||||
ResourceTargetVariant::Procedure,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.flat_map(|id| ObjectId::from_str(&id))
|
||||
.collect::<Vec<_>>();
|
||||
let query = doc! {
|
||||
"_id": { "$in": ids }
|
||||
};
|
||||
Some(query)
|
||||
};
|
||||
let total = db_client()
|
||||
.await
|
||||
.procedures
|
||||
.count_documents(query, None)
|
||||
.await
|
||||
.context("failed to count all procedure documents")?;
|
||||
let res = GetProceduresSummaryResponse {
|
||||
total: total as u32,
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetProcedureActionState, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetProcedureActionState { procedure }: GetProcedureActionState,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetProcedureActionStateResponse> {
|
||||
let procedure = Procedure::get_resource_check_permissions(
|
||||
&procedure,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.procedure
|
||||
.get(&procedure.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
108
bin/core/src/api/read/repo.rs
Normal file
108
bin/core/src/api/read/repo.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Context;
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
permission::PermissionLevel,
|
||||
repo::{Repo, RepoActionState, RepoListItem},
|
||||
update::ResourceTargetVariant,
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mungos::mongodb::bson::{doc, oid::ObjectId};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::resource::{
|
||||
get_resource_ids_for_non_admin, StateResource,
|
||||
},
|
||||
state::{action_states, db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetRepo, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetRepo { repo }: GetRepo,
|
||||
user: User,
|
||||
) -> anyhow::Result<Repo> {
|
||||
Repo::get_resource_check_permissions(
|
||||
&repo,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListRepos, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListRepos { query }: ListRepos,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<RepoListItem>> {
|
||||
Repo::list_resource_list_items_for_user(query, &user).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetRepoActionState, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetRepoActionState { repo }: GetRepoActionState,
|
||||
user: User,
|
||||
) -> anyhow::Result<RepoActionState> {
|
||||
let repo = Repo::get_resource_check_permissions(
|
||||
&repo,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.repo
|
||||
.get(&repo.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetReposSummary, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetReposSummary {}: GetReposSummary,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetReposSummaryResponse> {
|
||||
let query = if user.admin {
|
||||
None
|
||||
} else {
|
||||
let ids = get_resource_ids_for_non_admin(
|
||||
&user.id,
|
||||
ResourceTargetVariant::Alerter,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.flat_map(|id| ObjectId::from_str(&id))
|
||||
.collect::<Vec<_>>();
|
||||
let query = doc! {
|
||||
"_id": { "$in": ids }
|
||||
};
|
||||
Some(query)
|
||||
};
|
||||
let total = db_client()
|
||||
.await
|
||||
.repos
|
||||
.count_documents(query, None)
|
||||
.await
|
||||
.context("failed to count all build documents")?;
|
||||
let res = GetReposSummaryResponse {
|
||||
total: total as u32,
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
79
bin/core/src/api/read/search.rs
Normal file
79
bin/core/src/api/read/search.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::read::{FindResources, FindResourcesResponse},
|
||||
entities::{
|
||||
build, deployment, procedure, repo, server,
|
||||
update::ResourceTargetVariant::{self, *},
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{helpers::resource::StateResource, state::State};
|
||||
|
||||
const FIND_RESOURCE_TYPES: [ResourceTargetVariant; 5] =
|
||||
[Server, Build, Deployment, Repo, Procedure];
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<FindResources, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
FindResources { query, resources }: FindResources,
|
||||
user: User,
|
||||
) -> anyhow::Result<FindResourcesResponse> {
|
||||
let mut res = FindResourcesResponse::default();
|
||||
let resource_types = if resources.is_empty() {
|
||||
FIND_RESOURCE_TYPES.to_vec()
|
||||
} else {
|
||||
resources
|
||||
.into_iter()
|
||||
.filter(|r| !matches!(r, System | Builder | Alerter))
|
||||
.collect()
|
||||
};
|
||||
for resource_type in resource_types {
|
||||
match resource_type {
|
||||
Server => {
|
||||
res.servers =
|
||||
server::Server::query_resource_list_items_for_user(
|
||||
query.clone(),
|
||||
&user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Deployment => {
|
||||
res.deployments =
|
||||
deployment::Deployment::query_resource_list_items_for_user(
|
||||
query.clone(),
|
||||
&user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Build => {
|
||||
res.builds =
|
||||
build::Build::query_resource_list_items_for_user(
|
||||
query.clone(),
|
||||
&user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Repo => {
|
||||
res.repos = repo::Repo::query_resource_list_items_for_user(
|
||||
query.clone(),
|
||||
&user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Procedure => {
|
||||
res.procedures =
|
||||
procedure::Procedure::query_resource_list_items_for_user(
|
||||
query.clone(),
|
||||
&user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
449
bin/core/src/api/read/server.rs
Normal file
449
bin/core/src/api/read/server.rs
Normal file
@@ -0,0 +1,449 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_timing_util::{
|
||||
get_timelength_in_ms, unix_timestamp_ms, FIFTEEN_SECONDS_MS,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
deployment::ContainerSummary,
|
||||
permission::PermissionLevel,
|
||||
server::{
|
||||
docker_image::ImageSummary, docker_network::DockerNetwork,
|
||||
Server, ServerActionState, ServerListItem, ServerStatus,
|
||||
},
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::{bson::doc, options::FindOptions},
|
||||
};
|
||||
use periphery_client::api::{self, GetAccountsResponse};
|
||||
use resolver_api::{Resolve, ResolveToString};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
config::core_config, helpers::{periphery_client, resource::StateResource}, state::{action_states, db_client, server_status_cache, State}
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetServersSummary, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetServersSummary {}: GetServersSummary,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetServersSummaryResponse> {
|
||||
let servers = Server::list_resource_list_items_for_user(
|
||||
Default::default(),
|
||||
&user,
|
||||
)
|
||||
.await?;
|
||||
let mut res = GetServersSummaryResponse::default();
|
||||
for server in servers {
|
||||
res.total += 1;
|
||||
match server.info.status {
|
||||
ServerStatus::Ok => {
|
||||
res.healthy += 1;
|
||||
}
|
||||
ServerStatus::NotOk => {
|
||||
res.unhealthy += 1;
|
||||
}
|
||||
ServerStatus::Disabled => {
|
||||
res.disabled += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetPeripheryVersion, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
req: GetPeripheryVersion,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetPeripheryVersionResponse> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&req.server,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
let version = server_status_cache()
|
||||
.get(&server.id)
|
||||
.await
|
||||
.map(|s| s.version.clone())
|
||||
.unwrap_or(String::from("unknown"));
|
||||
Ok(GetPeripheryVersionResponse { version })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetServer, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
req: GetServer,
|
||||
user: User,
|
||||
) -> anyhow::Result<Server> {
|
||||
Server::get_resource_check_permissions(
|
||||
&req.server,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListServers, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListServers { query }: ListServers,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<ServerListItem>> {
|
||||
Server::list_resource_list_items_for_user(query, &user).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetServerStatus, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetServerStatus { server }: GetServerStatus,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetServerStatusResponse> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
let status = server_status_cache()
|
||||
.get(&server.id)
|
||||
.await
|
||||
.ok_or(anyhow!("did not find cached status for server"))?;
|
||||
let response = GetServerStatusResponse {
|
||||
status: status.status,
|
||||
};
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetServerActionState, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetServerActionState { server }: GetServerActionState,
|
||||
user: User,
|
||||
) -> anyhow::Result<ServerActionState> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.server
|
||||
.get(&server.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
|
||||
// This protects the peripheries from spam requests
|
||||
const SYSTEM_INFO_EXPIRY: u128 = FIFTEEN_SECONDS_MS;
|
||||
type SystemInfoCache = Mutex<HashMap<String, Arc<(String, u128)>>>;
|
||||
fn system_info_cache() -> &'static SystemInfoCache {
|
||||
static SYSTEM_INFO_CACHE: OnceLock<SystemInfoCache> =
|
||||
OnceLock::new();
|
||||
SYSTEM_INFO_CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ResolveToString<GetSystemInformation, User> for State {
|
||||
async fn resolve_to_string(
|
||||
&self,
|
||||
GetSystemInformation { server }: GetSystemInformation,
|
||||
user: User,
|
||||
) -> anyhow::Result<String> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut lock = system_info_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)?
|
||||
.request(api::stats::GetSystemInformation {})
|
||||
.await?;
|
||||
let res = serde_json::to_string(&stats)?;
|
||||
lock.insert(
|
||||
server.id,
|
||||
(res.clone(), unix_timestamp_ms() + SYSTEM_INFO_EXPIRY)
|
||||
.into(),
|
||||
);
|
||||
res
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ResolveToString<GetSystemStats, User> for State {
|
||||
async fn resolve_to_string(
|
||||
&self,
|
||||
GetSystemStats { server }: GetSystemStats,
|
||||
user: User,
|
||||
) -> anyhow::Result<String> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
let status =
|
||||
server_status_cache().get(&server.id).await.with_context(
|
||||
|| format!("did not find status for server at {}", server.id),
|
||||
)?;
|
||||
let stats = status
|
||||
.stats
|
||||
.as_ref()
|
||||
.context("server stats not available")?;
|
||||
let stats = serde_json::to_string(&stats)?;
|
||||
Ok(stats)
|
||||
}
|
||||
}
|
||||
|
||||
// This protects the peripheries from spam requests
|
||||
const PROCESSES_EXPIRY: u128 = FIFTEEN_SECONDS_MS;
|
||||
type ProcessesCache = Mutex<HashMap<String, Arc<(String, u128)>>>;
|
||||
fn processes_cache() -> &'static ProcessesCache {
|
||||
static PROCESSES_CACHE: OnceLock<ProcessesCache> = OnceLock::new();
|
||||
PROCESSES_CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ResolveToString<GetSystemProcesses, User> for State {
|
||||
async fn resolve_to_string(
|
||||
&self,
|
||||
GetSystemProcesses { server }: GetSystemProcesses,
|
||||
user: User,
|
||||
) -> anyhow::Result<String> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.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)?
|
||||
.request(api::stats::GetSystemProcesses {})
|
||||
.await?;
|
||||
let res = serde_json::to_string(&stats)?;
|
||||
lock.insert(
|
||||
server.id,
|
||||
(res.clone(), unix_timestamp_ms() + PROCESSES_EXPIRY)
|
||||
.into(),
|
||||
);
|
||||
res
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
const STATS_PER_PAGE: i64 = 500;
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetHistoricalServerStats, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetHistoricalServerStats {
|
||||
server,
|
||||
granularity,
|
||||
page,
|
||||
}: GetHistoricalServerStats,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetHistoricalServerStatsResponse> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.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().await.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)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetDockerImages, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetDockerImages { server }: GetDockerImages,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<ImageSummary>> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
periphery_client(&server)?
|
||||
.request(api::build::GetImageList {})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetDockerNetworks, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetDockerNetworks { server }: GetDockerNetworks,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<DockerNetwork>> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
periphery_client(&server)?
|
||||
.request(api::network::GetNetworkList {})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetDockerContainers, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetDockerContainers { server }: GetDockerContainers,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<ContainerSummary>> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
periphery_client(&server)?
|
||||
.request(api::container::GetContainerList {})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetAvailableAccounts, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetAvailableAccounts { server }: GetAvailableAccounts,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetAvailableAccountsResponse> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let GetAccountsResponse { github, docker } =
|
||||
periphery_client(&server)?
|
||||
.request(api::GetAccounts {})
|
||||
.await
|
||||
.context("failed to get accounts from periphery")?;
|
||||
|
||||
let mut github_set = HashSet::<String>::new();
|
||||
|
||||
github_set.extend(core_config().github_accounts.keys().cloned());
|
||||
github_set.extend(github);
|
||||
|
||||
let mut github = github_set.into_iter().collect::<Vec<_>>();
|
||||
github.sort();
|
||||
|
||||
let mut docker_set = HashSet::<String>::new();
|
||||
|
||||
docker_set.extend(core_config().docker_accounts.keys().cloned());
|
||||
docker_set.extend(docker);
|
||||
|
||||
let mut docker = docker_set.into_iter().collect::<Vec<_>>();
|
||||
docker.sort();
|
||||
|
||||
let res = GetAvailableAccountsResponse { github, docker };
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetAvailableSecrets, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetAvailableSecrets { server }: GetAvailableSecrets,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetAvailableSecretsResponse> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
let secrets = periphery_client(&server)?
|
||||
.request(api::GetSecrets {})
|
||||
.await
|
||||
.context("failed to get accounts from periphery")?;
|
||||
Ok(secrets)
|
||||
}
|
||||
}
|
||||
88
bin/core/src/api/read/server_template.rs
Normal file
88
bin/core/src/api/read/server_template.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::async_trait;
|
||||
use monitor_client::{
|
||||
api::read::{
|
||||
GetServerTemplate, GetServerTemplateResponse,
|
||||
GetServerTemplatesSummary, GetServerTemplatesSummaryResponse,
|
||||
ListServerTemplates, ListServerTemplatesResponse,
|
||||
},
|
||||
entities::{
|
||||
permission::PermissionLevel, server_template::ServerTemplate,
|
||||
update::ResourceTargetVariant, user::User,
|
||||
},
|
||||
};
|
||||
use mungos::mongodb::bson::{doc, oid::ObjectId};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::resource::{
|
||||
get_resource_ids_for_non_admin, StateResource,
|
||||
},
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetServerTemplate, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetServerTemplate { server_template }: GetServerTemplate,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetServerTemplateResponse> {
|
||||
ServerTemplate::get_resource_check_permissions(
|
||||
&server_template,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListServerTemplates, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListServerTemplates { query }: ListServerTemplates,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListServerTemplatesResponse> {
|
||||
ServerTemplate::list_resource_list_items_for_user(query, &user)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetServerTemplatesSummary, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetServerTemplatesSummary {}: GetServerTemplatesSummary,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetServerTemplatesSummaryResponse> {
|
||||
let query = if user.admin {
|
||||
None
|
||||
} else {
|
||||
let ids = get_resource_ids_for_non_admin(
|
||||
&user.id,
|
||||
ResourceTargetVariant::ServerTemplate,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.flat_map(|id| ObjectId::from_str(&id))
|
||||
.collect::<Vec<_>>();
|
||||
let query = doc! {
|
||||
"_id": { "$in": ids }
|
||||
};
|
||||
Some(query)
|
||||
};
|
||||
let total = db_client()
|
||||
.await
|
||||
.builders
|
||||
.count_documents(query, None)
|
||||
.await
|
||||
.context("failed to count all builder documents")?;
|
||||
let res = GetServerTemplatesSummaryResponse {
|
||||
total: total as u32,
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
37
bin/core/src/api/read/tag.rs
Normal file
37
bin/core/src/api/read/tag.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use anyhow::Context;
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::read::{GetTag, ListTags},
|
||||
entities::{tag::Tag, user::User},
|
||||
};
|
||||
use mungos::find::find_collect;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_tag,
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetTag, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetTag { tag }: GetTag,
|
||||
_: User,
|
||||
) -> anyhow::Result<Tag> {
|
||||
get_tag(&tag).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListTags, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListTags { query }: ListTags,
|
||||
_: User,
|
||||
) -> anyhow::Result<Vec<Tag>> {
|
||||
find_collect(&db_client().await.tags, query, None)
|
||||
.await
|
||||
.context("failed to get tags from db")
|
||||
}
|
||||
}
|
||||
481
bin/core/src/api/read/toml.rs
Normal file
481
bin/core/src/api/read/toml.rs
Normal file
@@ -0,0 +1,481 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::async_trait;
|
||||
use monitor_client::{
|
||||
api::{
|
||||
execute::Execution,
|
||||
read::{
|
||||
ExportAllResourcesToToml, ExportAllResourcesToTomlResponse,
|
||||
ExportResourcesToToml, ExportResourcesToTomlResponse,
|
||||
GetUserGroup, ListUserTargetPermissions,
|
||||
},
|
||||
},
|
||||
entities::{
|
||||
alerter::Alerter,
|
||||
build::Build,
|
||||
builder::{Builder, BuilderConfig},
|
||||
deployment::{Deployment, DeploymentImage},
|
||||
permission::{PermissionLevel, UserTarget},
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
resource::Resource,
|
||||
server::Server,
|
||||
server_template::ServerTemplate,
|
||||
toml::{
|
||||
PermissionToml, ResourceToml, ResourcesToml, UserGroupToml,
|
||||
},
|
||||
update::ResourceTarget,
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mungos::find::find_collect;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
query::get_user_user_group_ids, resource::StateResource,
|
||||
},
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ExportAllResourcesToToml, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ExportAllResourcesToToml {}: ExportAllResourcesToToml,
|
||||
user: User,
|
||||
) -> anyhow::Result<ExportAllResourcesToTomlResponse> {
|
||||
let mut targets = Vec::<ResourceTarget>::new();
|
||||
|
||||
targets.extend(
|
||||
Alerter::list_resource_list_items_for_user(
|
||||
Default::default(),
|
||||
&user,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Alerter(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
Builder::list_resource_list_items_for_user(
|
||||
Default::default(),
|
||||
&user,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Builder(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
Server::list_resource_list_items_for_user(
|
||||
Default::default(),
|
||||
&user,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Server(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
Deployment::list_resource_list_items_for_user(
|
||||
Default::default(),
|
||||
&user,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Deployment(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
Build::list_resource_list_items_for_user(
|
||||
Default::default(),
|
||||
&user,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Build(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
Repo::list_resource_list_items_for_user(
|
||||
Default::default(),
|
||||
&user,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Repo(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
Procedure::list_resource_list_items_for_user(
|
||||
Default::default(),
|
||||
&user,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Procedure(resource.id)),
|
||||
);
|
||||
|
||||
let user_groups = if user.admin {
|
||||
find_collect(&db_client().await.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(&user.id).await?
|
||||
};
|
||||
|
||||
self
|
||||
.resolve(
|
||||
ExportResourcesToToml {
|
||||
targets,
|
||||
user_groups,
|
||||
},
|
||||
user,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ExportResourcesToToml, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ExportResourcesToToml {
|
||||
targets,
|
||||
user_groups,
|
||||
}: ExportResourcesToToml,
|
||||
user: User,
|
||||
) -> anyhow::Result<ExportResourcesToTomlResponse> {
|
||||
let mut res = ResourcesToml::default();
|
||||
let names = ResourceNames::new()
|
||||
.await
|
||||
.context("failed to init resource name maps")?;
|
||||
for target in targets {
|
||||
match target {
|
||||
ResourceTarget::Alerter(id) => {
|
||||
let alerter = Alerter::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
res.alerters.push(convert_resource(alerter, &names.tags))
|
||||
}
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
let template =
|
||||
ServerTemplate::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
res
|
||||
.server_templates
|
||||
.push(convert_resource(template, &names.tags))
|
||||
}
|
||||
ResourceTarget::Server(id) => {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
res.servers.push(convert_resource(server, &names.tags))
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
let mut builder = Builder::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
// replace server id of builder
|
||||
if let BuilderConfig::Server(config) = &mut builder.config {
|
||||
config.server_id.clone_from(
|
||||
names.servers.get(&id).unwrap_or(&String::new()),
|
||||
)
|
||||
}
|
||||
res.builders.push(convert_resource(builder, &names.tags))
|
||||
}
|
||||
ResourceTarget::Build(id) => {
|
||||
let mut build = Build::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
// replace builder id of build
|
||||
build.config.builder_id.clone_from(
|
||||
names
|
||||
.builders
|
||||
.get(&build.config.builder_id)
|
||||
.unwrap_or(&String::new()),
|
||||
);
|
||||
res.builds.push(convert_resource(build, &names.tags))
|
||||
}
|
||||
ResourceTarget::Deployment(id) => {
|
||||
let mut deployment =
|
||||
Deployment::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
// replace deployment server with name
|
||||
deployment.config.server_id.clone_from(
|
||||
names
|
||||
.servers
|
||||
.get(&deployment.config.server_id)
|
||||
.unwrap_or(&String::new()),
|
||||
);
|
||||
// replace deployment build id with name
|
||||
if let DeploymentImage::Build { build_id, .. } =
|
||||
&mut deployment.config.image
|
||||
{
|
||||
build_id.clone_from(
|
||||
names.builds.get(build_id).unwrap_or(&String::new()),
|
||||
);
|
||||
}
|
||||
res
|
||||
.deployments
|
||||
.push(convert_resource(deployment, &names.tags))
|
||||
}
|
||||
ResourceTarget::Repo(id) => {
|
||||
let mut repo = Repo::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
// replace repo server with name
|
||||
repo.config.server_id.clone_from(
|
||||
names
|
||||
.servers
|
||||
.get(&repo.config.server_id)
|
||||
.unwrap_or(&String::new()),
|
||||
);
|
||||
res.repos.push(convert_resource(repo, &names.tags))
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
add_procedure(&id, &mut res, &user, &names)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to add procedure {id}")
|
||||
})?;
|
||||
}
|
||||
ResourceTarget::System(_) => continue,
|
||||
};
|
||||
}
|
||||
|
||||
add_user_groups(user_groups, &mut res, &user)
|
||||
.await
|
||||
.context("failed to add user groups")?;
|
||||
|
||||
let toml = toml::to_string_pretty(&res)
|
||||
.context("failed to serialize resources to toml")?;
|
||||
|
||||
Ok(ExportResourcesToTomlResponse { toml })
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_procedure(
|
||||
id: &str,
|
||||
res: &mut ResourcesToml,
|
||||
user: &User,
|
||||
names: &ResourceNames,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut procedure = Procedure::get_resource_check_permissions(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
for execution in &mut procedure.config.executions {
|
||||
match &mut execution.execution {
|
||||
Execution::RunProcedure(exec) => exec.procedure.clone_from(
|
||||
names
|
||||
.procedures
|
||||
.get(&exec.procedure)
|
||||
.unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::RunBuild(exec) => exec.build.clone_from(
|
||||
names.builds.get(&exec.build).unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::Deploy(exec) => exec.deployment.clone_from(
|
||||
names
|
||||
.deployments
|
||||
.get(&exec.deployment)
|
||||
.unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::StartContainer(exec) => exec.deployment.clone_from(
|
||||
names
|
||||
.deployments
|
||||
.get(&exec.deployment)
|
||||
.unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::StopContainer(exec) => exec.deployment.clone_from(
|
||||
names
|
||||
.deployments
|
||||
.get(&exec.deployment)
|
||||
.unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::RemoveContainer(exec) => exec.deployment.clone_from(
|
||||
names
|
||||
.deployments
|
||||
.get(&exec.deployment)
|
||||
.unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::CloneRepo(exec) => exec.repo.clone_from(
|
||||
names.repos.get(&exec.repo).unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::PullRepo(exec) => exec.repo.clone_from(
|
||||
names.repos.get(&exec.repo).unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::StopAllContainers(exec) => exec.server.clone_from(
|
||||
names.servers.get(&exec.server).unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::PruneDockerNetworks(exec) => exec.server.clone_from(
|
||||
names.servers.get(&exec.server).unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::PruneDockerImages(exec) => exec.server.clone_from(
|
||||
names.servers.get(&exec.server).unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::PruneDockerContainers(exec) => {
|
||||
exec.server.clone_from(
|
||||
names.servers.get(&exec.server).unwrap_or(&String::new()),
|
||||
)
|
||||
}
|
||||
Execution::None(_) => continue,
|
||||
}
|
||||
}
|
||||
res
|
||||
.procedures
|
||||
.push(convert_resource(procedure, &names.tags));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct ResourceNames {
|
||||
tags: HashMap<String, String>,
|
||||
servers: HashMap<String, String>,
|
||||
builders: HashMap<String, String>,
|
||||
builds: HashMap<String, String>,
|
||||
repos: HashMap<String, String>,
|
||||
deployments: HashMap<String, String>,
|
||||
procedures: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl ResourceNames {
|
||||
async fn new() -> anyhow::Result<ResourceNames> {
|
||||
let db = db_client().await;
|
||||
Ok(ResourceNames {
|
||||
tags: find_collect(&db.tags, None, None)
|
||||
.await
|
||||
.context("failed to get all tags")?
|
||||
.into_iter()
|
||||
.map(|t| (t.id, t.name))
|
||||
.collect::<HashMap<_, _>>(),
|
||||
servers: find_collect(&db.servers, None, None)
|
||||
.await
|
||||
.context("failed to get all servers")?
|
||||
.into_iter()
|
||||
.map(|t| (t.id, t.name))
|
||||
.collect::<HashMap<_, _>>(),
|
||||
builders: find_collect(&db.builders, None, None)
|
||||
.await
|
||||
.context("failed to get all builders")?
|
||||
.into_iter()
|
||||
.map(|t| (t.id, t.name))
|
||||
.collect::<HashMap<_, _>>(),
|
||||
builds: find_collect(&db.builds, None, None)
|
||||
.await
|
||||
.context("failed to get all builds")?
|
||||
.into_iter()
|
||||
.map(|t| (t.id, t.name))
|
||||
.collect::<HashMap<_, _>>(),
|
||||
repos: find_collect(&db.repos, None, None)
|
||||
.await
|
||||
.context("failed to get all repos")?
|
||||
.into_iter()
|
||||
.map(|t| (t.id, t.name))
|
||||
.collect::<HashMap<_, _>>(),
|
||||
deployments: find_collect(&db.deployments, None, None)
|
||||
.await
|
||||
.context("failed to get all deployments")?
|
||||
.into_iter()
|
||||
.map(|t| (t.id, t.name))
|
||||
.collect::<HashMap<_, _>>(),
|
||||
procedures: find_collect(&db.procedures, None, None)
|
||||
.await
|
||||
.context("failed to get all procedures")?
|
||||
.into_iter()
|
||||
.map(|t| (t.id, t.name))
|
||||
.collect::<HashMap<_, _>>(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_user_groups(
|
||||
user_groups: Vec<String>,
|
||||
res: &mut ResourcesToml,
|
||||
user: &User,
|
||||
) -> anyhow::Result<()> {
|
||||
let db = db_client().await;
|
||||
|
||||
let usernames = find_collect(&db.users, None, None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|user| (user.id, user.username))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
for user_group in user_groups {
|
||||
let ug = State
|
||||
.resolve(GetUserGroup { user_group }, user.clone())
|
||||
.await?;
|
||||
// this method is admin only, but we already know user can see user group if above does not return Err
|
||||
let permissions = State
|
||||
.resolve(
|
||||
ListUserTargetPermissions {
|
||||
user_target: UserTarget::UserGroup(ug.id),
|
||||
},
|
||||
User {
|
||||
admin: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|permission| PermissionToml {
|
||||
target: permission.resource_target,
|
||||
level: permission.level,
|
||||
})
|
||||
.collect();
|
||||
res.user_groups.push(UserGroupToml {
|
||||
name: ug.name,
|
||||
users: ug
|
||||
.users
|
||||
.into_iter()
|
||||
.filter_map(|user_id| usernames.get(&user_id).cloned())
|
||||
.collect(),
|
||||
permissions,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_resource<Config, Info: Default, PartialConfig>(
|
||||
resource: Resource<Config, Info>,
|
||||
tag_names: &HashMap<String, String>,
|
||||
) -> ResourceToml<PartialConfig>
|
||||
where
|
||||
Config: Into<PartialConfig>,
|
||||
{
|
||||
ResourceToml {
|
||||
name: resource.name,
|
||||
tags: resource
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|t| tag_names.get(t).cloned())
|
||||
.collect(),
|
||||
description: resource.description,
|
||||
config: resource.config.into(),
|
||||
}
|
||||
}
|
||||
214
bin/core/src/api/read/update.rs
Normal file
214
bin/core/src/api/read/update.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::read::{GetUpdate, ListUpdates, ListUpdatesResponse},
|
||||
entities::{
|
||||
alerter::Alerter,
|
||||
build::Build,
|
||||
builder::Builder,
|
||||
deployment::Deployment,
|
||||
permission::PermissionLevel,
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
server_template::ServerTemplate,
|
||||
update::{ResourceTarget, Update, UpdateListItem},
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::find_one_by_id,
|
||||
find::find_collect,
|
||||
mongodb::{bson::doc, options::FindOptions},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::resource::StateResource,
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
const UPDATES_PER_PAGE: i64 = 20;
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListUpdates, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListUpdates { query, page }: ListUpdates,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListUpdatesResponse> {
|
||||
let query = if user.admin {
|
||||
query
|
||||
} else {
|
||||
let server_ids =
|
||||
Server::get_resource_ids_for_non_admin(&user.id).await?;
|
||||
let deployment_ids =
|
||||
Deployment::get_resource_ids_for_non_admin(&user.id).await?;
|
||||
let build_ids =
|
||||
Build::get_resource_ids_for_non_admin(&user.id).await?;
|
||||
let repo_ids =
|
||||
Repo::get_resource_ids_for_non_admin(&user.id).await?;
|
||||
let procedure_ids =
|
||||
Procedure::get_resource_ids_for_non_admin(&user.id).await?;
|
||||
let builder_ids =
|
||||
Builder::get_resource_ids_for_non_admin(&user.id).await?;
|
||||
let alerter_ids =
|
||||
Alerter::get_resource_ids_for_non_admin(&user.id).await?;
|
||||
let mut query = query.unwrap_or_default();
|
||||
query.extend(doc! {
|
||||
"$or": [
|
||||
{ "target.type": "Server", "target.id": { "$in": &server_ids } },
|
||||
{ "target.type": "Deployment", "target.id": { "$in": &deployment_ids } },
|
||||
{ "target.type": "Build", "target.id": { "$in": &build_ids } },
|
||||
{ "target.type": "Repo", "target.id": { "$in": &repo_ids } },
|
||||
{ "target.type": "Procedure", "target.id": { "$in": &procedure_ids } },
|
||||
{ "target.type": "Builder", "target.id": { "$in": &builder_ids } },
|
||||
{ "target.type": "Alerter", "target.id": { "$in": &alerter_ids } },
|
||||
]
|
||||
});
|
||||
query.into()
|
||||
};
|
||||
|
||||
let usernames =
|
||||
find_collect(&db_client().await.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().await.updates,
|
||||
query,
|
||||
FindOptions::builder()
|
||||
.sort(doc! { "start_ts": -1 })
|
||||
.skip(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,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let next_page = if updates.len() == UPDATES_PER_PAGE as usize {
|
||||
Some(page + 1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ListUpdatesResponse { updates, next_page })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetUpdate, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetUpdate { id }: GetUpdate,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let update = find_one_by_id(&db_client().await.updates, &id)
|
||||
.await
|
||||
.context("failed to query to db")?
|
||||
.context("no update exists with given id")?;
|
||||
if user.admin {
|
||||
return Ok(update);
|
||||
}
|
||||
match &update.target {
|
||||
ResourceTarget::System(_) => {
|
||||
return Err(anyhow!(
|
||||
"user must be admin to view system updates"
|
||||
))
|
||||
}
|
||||
ResourceTarget::Server(id) => {
|
||||
Server::get_resource_check_permissions(
|
||||
id,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Deployment(id) => {
|
||||
Deployment::get_resource_check_permissions(
|
||||
id,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Build(id) => {
|
||||
Build::get_resource_check_permissions(
|
||||
id,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Repo(id) => {
|
||||
Repo::get_resource_check_permissions(
|
||||
id,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
Builder::get_resource_check_permissions(
|
||||
id,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Alerter(id) => {
|
||||
Alerter::get_resource_check_permissions(
|
||||
id,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
Procedure::get_resource_check_permissions(
|
||||
id,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
ServerTemplate::get_resource_check_permissions(
|
||||
id,
|
||||
&user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
111
bin/core/src/api/read/user.rs
Normal file
111
bin/core/src/api/read/user.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::read::{
|
||||
GetUsername, GetUsernameResponse, ListApiKeys,
|
||||
ListApiKeysForServiceUser, ListApiKeysForServiceUserResponse,
|
||||
ListApiKeysResponse, ListUsers, ListUsersResponse,
|
||||
},
|
||||
entities::user::{User, UserConfig},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::find_one_by_id, find::find_collect, mongodb::bson::doc,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::state::{db_client, State};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetUsername, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetUsername { user_id }: GetUsername,
|
||||
_: User,
|
||||
) -> anyhow::Result<GetUsernameResponse> {
|
||||
let user = find_one_by_id(&db_client().await.users, &user_id)
|
||||
.await
|
||||
.context("failed at mongo query for user")?
|
||||
.context("no user found with id")?;
|
||||
|
||||
Ok(GetUsernameResponse {
|
||||
username: user.username,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListUsers, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListUsers {}: ListUsers,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListUsersResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("this route is only accessable by admins"));
|
||||
}
|
||||
let mut users =
|
||||
find_collect(&db_client().await.users, None, None)
|
||||
.await
|
||||
.context("failed to pull users from db")?;
|
||||
users.iter_mut().for_each(|user| user.sanitize());
|
||||
Ok(users)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListApiKeys, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListApiKeys {}: ListApiKeys,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListApiKeysResponse> {
|
||||
let api_keys = find_collect(
|
||||
&db_client().await.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)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListApiKeysForServiceUser, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListApiKeysForServiceUser { user_id }: ListApiKeysForServiceUser,
|
||||
admin: User,
|
||||
) -> anyhow::Result<ListApiKeysForServiceUserResponse> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This method is admin only."));
|
||||
}
|
||||
let user = find_one_by_id(&db_client().await.users, &user_id)
|
||||
.await
|
||||
.context("failed to query db for users")?
|
||||
.context("user at id not found")?;
|
||||
let UserConfig::Service { .. } = user.config else {
|
||||
return Err(anyhow!("Given user is not service user"));
|
||||
};
|
||||
let api_keys = find_collect(
|
||||
&db_client().await.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)
|
||||
}
|
||||
}
|
||||
61
bin/core/src/api/read/user_group.rs
Normal file
61
bin/core/src/api/read/user_group.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Context;
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::read::{
|
||||
GetUserGroup, GetUserGroupResponse, ListUserGroups,
|
||||
ListUserGroupsResponse,
|
||||
},
|
||||
entities::user::User,
|
||||
};
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::bson::{doc, oid::ObjectId, Document},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::state::{db_client, State};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<GetUserGroup, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetUserGroup { user_group }: GetUserGroup,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetUserGroupResponse> {
|
||||
let mut filter = match ObjectId::from_str(&user_group) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": &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);
|
||||
}
|
||||
db_client()
|
||||
.await
|
||||
.user_groups
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query db for user groups")?
|
||||
.context("no UserGroup found with given name or id")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<ListUserGroups, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListUserGroups {}: ListUserGroups,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListUserGroupsResponse> {
|
||||
let mut filter = Document::new();
|
||||
if !user.admin {
|
||||
filter.insert("users", &user.id);
|
||||
}
|
||||
find_collect(&db_client().await.user_groups, filter, None)
|
||||
.await
|
||||
.context("failed to query db for UserGroups")
|
||||
}
|
||||
}
|
||||
240
bin/core/src/api/write/alerter.rs
Normal file
240
bin/core/src/api/write/alerter.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::write::{
|
||||
CopyAlerter, CreateAlerter, DeleteAlerter, UpdateAlerter,
|
||||
},
|
||||
entities::{
|
||||
alerter::{Alerter, AlerterInfo},
|
||||
monitor_timestamp,
|
||||
permission::PermissionLevel,
|
||||
user::User,
|
||||
Operation,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::{delete_one_by_id, update_one_by_id},
|
||||
mongodb::bson::{doc, oid::ObjectId, to_bson},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
create_permission, remove_from_recently_viewed,
|
||||
resource::{delete_all_permissions_on_resource, StateResource},
|
||||
update::{add_update, make_update},
|
||||
},
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CreateAlerter, User> for State {
|
||||
#[instrument(name = "CreateAlerter", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CreateAlerter { name, config }: CreateAlerter,
|
||||
user: User,
|
||||
) -> anyhow::Result<Alerter> {
|
||||
if ObjectId::from_str(&name).is_ok() {
|
||||
return Err(anyhow!("valid ObjectIds cannot be used as names"));
|
||||
}
|
||||
let start_ts = monitor_timestamp();
|
||||
let is_default = db_client()
|
||||
.await
|
||||
.alerters
|
||||
.find_one(None, None)
|
||||
.await?
|
||||
.is_none();
|
||||
let alerter = Alerter {
|
||||
id: Default::default(),
|
||||
name,
|
||||
updated_at: start_ts,
|
||||
description: Default::default(),
|
||||
tags: Default::default(),
|
||||
config: config.into(),
|
||||
info: AlerterInfo { is_default },
|
||||
};
|
||||
let alerter_id = db_client()
|
||||
.await
|
||||
.alerters
|
||||
.insert_one(alerter, None)
|
||||
.await
|
||||
.context("failed to add alerter to db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
let alerter = Alerter::get_resource(&alerter_id).await?;
|
||||
|
||||
create_permission(&user, &alerter, PermissionLevel::Write).await;
|
||||
|
||||
let mut update =
|
||||
make_update(&alerter, Operation::CreateAlerter, &user);
|
||||
|
||||
update.push_simple_log(
|
||||
"create alerter",
|
||||
format!(
|
||||
"created alerter\nid: {}\nname: {}",
|
||||
alerter.id, alerter.name
|
||||
),
|
||||
);
|
||||
update
|
||||
.push_simple_log("config", format!("{:#?}", alerter.config));
|
||||
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(alerter)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CopyAlerter, User> for State {
|
||||
#[instrument(name = "CopyAlerter", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CopyAlerter { name, id }: CopyAlerter,
|
||||
user: User,
|
||||
) -> anyhow::Result<Alerter> {
|
||||
let Alerter {
|
||||
config,
|
||||
description,
|
||||
..
|
||||
} = Alerter::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
let start_ts = monitor_timestamp();
|
||||
let alerter = Alerter {
|
||||
id: Default::default(),
|
||||
name,
|
||||
updated_at: start_ts,
|
||||
description,
|
||||
config,
|
||||
tags: Default::default(),
|
||||
info: Default::default(),
|
||||
};
|
||||
let alerter_id = db_client()
|
||||
.await
|
||||
.alerters
|
||||
.insert_one(alerter, None)
|
||||
.await
|
||||
.context("failed to add alerter to db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
let alerter = Alerter::get_resource(&alerter_id).await?;
|
||||
|
||||
create_permission(&user, &alerter, PermissionLevel::Write).await;
|
||||
|
||||
let mut update =
|
||||
make_update(&alerter, Operation::CreateAlerter, &user);
|
||||
|
||||
update.push_simple_log(
|
||||
"create alerter",
|
||||
format!(
|
||||
"created alerter\nid: {}\nname: {}",
|
||||
alerter.id, alerter.name
|
||||
),
|
||||
);
|
||||
|
||||
update
|
||||
.push_simple_log("config", format!("{:#?}", alerter.config));
|
||||
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(alerter)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<DeleteAlerter, User> for State {
|
||||
#[instrument(name = "DeleteAlerter", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeleteAlerter { id }: DeleteAlerter,
|
||||
user: User,
|
||||
) -> anyhow::Result<Alerter> {
|
||||
let alerter = Alerter::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut update =
|
||||
make_update(&alerter, Operation::DeleteAlerter, &user);
|
||||
|
||||
delete_one_by_id(&db_client().await.alerters, &id, None)
|
||||
.await
|
||||
.context("failed to delete alerter from database")?;
|
||||
|
||||
delete_all_permissions_on_resource(&alerter).await;
|
||||
|
||||
update.push_simple_log(
|
||||
"delete alerter",
|
||||
format!("deleted alerter {}", alerter.name),
|
||||
);
|
||||
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
remove_from_recently_viewed(&alerter).await?;
|
||||
|
||||
Ok(alerter)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<UpdateAlerter, User> for State {
|
||||
#[instrument(name = "UpdateAlerter", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateAlerter { id, config }: UpdateAlerter,
|
||||
user: User,
|
||||
) -> anyhow::Result<Alerter> {
|
||||
let alerter = Alerter::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut update =
|
||||
make_update(&alerter, Operation::UpdateAlerter, &user);
|
||||
|
||||
update.push_simple_log(
|
||||
"alerter update",
|
||||
serde_json::to_string_pretty(&config)?,
|
||||
);
|
||||
|
||||
let config = alerter.config.merge_partial(config);
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.alerters,
|
||||
&id,
|
||||
mungos::update::Update::FlattenSet(
|
||||
doc! { "config": to_bson(&config)? },
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to update alerter {id}"))?;
|
||||
|
||||
let alerter = Alerter::get_resource(&id).await?;
|
||||
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(alerter)
|
||||
}
|
||||
}
|
||||
152
bin/core/src/api/write/api_key.rs
Normal file
152
bin/core/src/api/write/api_key.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
api_key::ApiKey,
|
||||
monitor_timestamp,
|
||||
user::{User, UserConfig},
|
||||
},
|
||||
};
|
||||
use mungos::{by_id::find_one_by_id, mongodb::bson::doc};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
auth::random_string,
|
||||
helpers::query::get_user,
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
const SECRET_LENGTH: usize = 40;
|
||||
const BCRYPT_COST: u32 = 10;
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CreateApiKey, User> for State {
|
||||
#[instrument(
|
||||
name = "CreateApiKey",
|
||||
level = "debug",
|
||||
skip(self, user)
|
||||
)]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CreateApiKey { name, expires }: CreateApiKey,
|
||||
user: User,
|
||||
) -> anyhow::Result<CreateApiKeyResponse> {
|
||||
let user = get_user(&user.id).await?;
|
||||
|
||||
let key = format!("K-{}", random_string(SECRET_LENGTH));
|
||||
let secret = format!("S-{}", random_string(SECRET_LENGTH));
|
||||
let secret_hash = bcrypt::hash(&secret, BCRYPT_COST)
|
||||
.context("failed at hashing secret string")?;
|
||||
|
||||
let api_key = ApiKey {
|
||||
name,
|
||||
key: key.clone(),
|
||||
secret: secret_hash,
|
||||
user_id: user.id.clone(),
|
||||
created_at: monitor_timestamp(),
|
||||
expires,
|
||||
};
|
||||
db_client()
|
||||
.await
|
||||
.api_keys
|
||||
.insert_one(api_key, None)
|
||||
.await
|
||||
.context("failed to create api key on db")?;
|
||||
Ok(CreateApiKeyResponse { key, secret })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<DeleteApiKey, User> for State {
|
||||
#[instrument(
|
||||
name = "DeleteApiKey",
|
||||
level = "debug",
|
||||
skip(self, user)
|
||||
)]
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeleteApiKey { key }: DeleteApiKey,
|
||||
user: User,
|
||||
) -> anyhow::Result<DeleteApiKeyResponse> {
|
||||
let client = db_client().await;
|
||||
let key = client
|
||||
.api_keys
|
||||
.find_one(doc! { "key": &key }, None)
|
||||
.await
|
||||
.context("failed at db query")?
|
||||
.context("no api key with key found")?;
|
||||
if user.id != key.user_id {
|
||||
return Err(anyhow!("api key does not belong to user"));
|
||||
}
|
||||
client
|
||||
.api_keys
|
||||
.delete_one(doc! { "key": key.key }, None)
|
||||
.await
|
||||
.context("failed to delete api key from db")?;
|
||||
Ok(DeleteApiKeyResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CreateApiKeyForServiceUser, User> for State {
|
||||
#[instrument(name = "CreateApiKeyForServiceUser", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CreateApiKeyForServiceUser {
|
||||
user_id,
|
||||
name,
|
||||
expires,
|
||||
}: CreateApiKeyForServiceUser,
|
||||
user: User,
|
||||
) -> anyhow::Result<CreateApiKeyForServiceUserResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("user not admin"));
|
||||
}
|
||||
let service_user =
|
||||
find_one_by_id(&db_client().await.users, &user_id)
|
||||
.await
|
||||
.context("failed to query db for user")?
|
||||
.context("no user found with id")?;
|
||||
let UserConfig::Service { .. } = &service_user.config else {
|
||||
return Err(anyhow!("user is not service user"));
|
||||
};
|
||||
self
|
||||
.resolve(CreateApiKey { name, expires }, service_user)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<DeleteApiKeyForServiceUser, User> for State {
|
||||
#[instrument(name = "DeleteApiKeyForServiceUser", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeleteApiKeyForServiceUser { key }: DeleteApiKeyForServiceUser,
|
||||
user: User,
|
||||
) -> anyhow::Result<DeleteApiKeyForServiceUserResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("user not admin"));
|
||||
}
|
||||
let db = db_client().await;
|
||||
let api_key = db
|
||||
.api_keys
|
||||
.find_one(doc! { "key": &key }, None)
|
||||
.await
|
||||
.context("failed to query db for api key")?
|
||||
.context("did not find matching api key")?;
|
||||
let service_user =
|
||||
find_one_by_id(&db_client().await.users, &api_key.user_id)
|
||||
.await
|
||||
.context("failed to query db for user")?
|
||||
.context("no user found with id")?;
|
||||
let UserConfig::Service { .. } = &service_user.config else {
|
||||
return Err(anyhow!("user is not service user"));
|
||||
};
|
||||
db.api_keys
|
||||
.delete_one(doc! { "key": key }, None)
|
||||
.await
|
||||
.context("failed to delete api key on db")?;
|
||||
Ok(DeleteApiKeyForServiceUserResponse {})
|
||||
}
|
||||
}
|
||||
309
bin/core/src/api/write/build.rs
Normal file
309
bin/core/src/api/write/build.rs
Normal file
@@ -0,0 +1,309 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
build::Build,
|
||||
builder::Builder,
|
||||
monitor_timestamp,
|
||||
permission::PermissionLevel,
|
||||
to_monitor_name,
|
||||
update::{Log, UpdateStatus},
|
||||
user::User,
|
||||
Operation,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::update_one_by_id,
|
||||
mongodb::bson::{doc, oid::ObjectId, to_document},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
create_permission, empty_or_only_spaces,
|
||||
remove_from_recently_viewed,
|
||||
resource::{delete_all_permissions_on_resource, StateResource},
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
state::{action_states, db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CreateBuild, User> for State {
|
||||
#[instrument(name = "CreateBuild", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CreateBuild { name, mut config }: CreateBuild,
|
||||
user: User,
|
||||
) -> anyhow::Result<Build> {
|
||||
if !user.admin && !user.create_build_permissions {
|
||||
return Err(anyhow!(
|
||||
"User does not have create build permissions."
|
||||
));
|
||||
}
|
||||
let name = to_monitor_name(&name);
|
||||
if ObjectId::from_str(&name).is_ok() {
|
||||
return Err(anyhow!("valid ObjectIds cannot be used as names"));
|
||||
}
|
||||
if let Some(builder_id) = &config.builder_id {
|
||||
let builder = Builder::get_resource_check_permissions(builder_id, &user, PermissionLevel::Read)
|
||||
.await
|
||||
.context("cannot create build using this builder. user must have at least read permissions on the builder.")?;
|
||||
config.builder_id = Some(builder.id)
|
||||
}
|
||||
let start_ts = monitor_timestamp();
|
||||
let build = Build {
|
||||
id: Default::default(),
|
||||
name,
|
||||
updated_at: start_ts,
|
||||
description: Default::default(),
|
||||
tags: Default::default(),
|
||||
config: config.into(),
|
||||
info: Default::default(),
|
||||
};
|
||||
let build_id = db_client()
|
||||
.await
|
||||
.builds
|
||||
.insert_one(build, None)
|
||||
.await
|
||||
.context("failed to add build to db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
let build = Build::get_resource(&build_id).await?;
|
||||
|
||||
create_permission(&user, &build, PermissionLevel::Write).await;
|
||||
|
||||
let mut update =
|
||||
make_update(&build, Operation::CreateBuild, &user);
|
||||
|
||||
update.push_simple_log(
|
||||
"create build",
|
||||
format!(
|
||||
"created build\nid: {}\nname: {}",
|
||||
build.id, build.name
|
||||
),
|
||||
);
|
||||
|
||||
update.push_simple_log("config", format!("{:#?}", build.config));
|
||||
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(build)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CopyBuild, User> for State {
|
||||
#[instrument(name = "CopyBuild", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CopyBuild { name, id }: CopyBuild,
|
||||
user: User,
|
||||
) -> anyhow::Result<Build> {
|
||||
if !user.admin && !user.create_build_permissions {
|
||||
return Err(anyhow!(
|
||||
"User does not have create build permissions."
|
||||
));
|
||||
}
|
||||
let name = to_monitor_name(&name);
|
||||
let Build {
|
||||
config,
|
||||
description,
|
||||
tags,
|
||||
..
|
||||
} = Build::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
Builder::get_resource_check_permissions(&config.builder_id, &user, PermissionLevel::Read)
|
||||
.await
|
||||
.context("cannot create build using this builder. user must have at least read permissions on the builder.")?;
|
||||
let start_ts = monitor_timestamp();
|
||||
let build = Build {
|
||||
id: Default::default(),
|
||||
name,
|
||||
updated_at: start_ts,
|
||||
description,
|
||||
tags,
|
||||
config,
|
||||
info: Default::default(),
|
||||
};
|
||||
let build_id = db_client()
|
||||
.await
|
||||
.builds
|
||||
.insert_one(build, None)
|
||||
.await
|
||||
.context("failed to add build to db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
let build = Build::get_resource(&build_id).await?;
|
||||
|
||||
create_permission(&user, &build, PermissionLevel::Write).await;
|
||||
|
||||
let mut update =
|
||||
make_update(&build, Operation::CreateBuild, &user);
|
||||
|
||||
update.push_simple_log(
|
||||
"create build",
|
||||
format!(
|
||||
"created build\nid: {}\nname: {}",
|
||||
build.id, build.name
|
||||
),
|
||||
);
|
||||
update.push_simple_log(
|
||||
"config",
|
||||
serde_json::to_string_pretty(&build)?,
|
||||
);
|
||||
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(build)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<DeleteBuild, User> for State {
|
||||
#[instrument(name = "DeleteBuild", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeleteBuild { id }: DeleteBuild,
|
||||
user: User,
|
||||
) -> anyhow::Result<Build> {
|
||||
if action_states()
|
||||
.build
|
||||
.get(&id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.busy()?
|
||||
{
|
||||
return Err(anyhow!("build busy"));
|
||||
}
|
||||
|
||||
let build = Build::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut update =
|
||||
make_update(&build, Operation::DeleteBuild, &user);
|
||||
update.status = UpdateStatus::InProgress;
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
let res = db_client()
|
||||
.await
|
||||
.builds
|
||||
.delete_one(doc! { "_id": ObjectId::from_str(&id)? }, None)
|
||||
.await
|
||||
.context("failed to delete build from database");
|
||||
|
||||
delete_all_permissions_on_resource(&build).await;
|
||||
|
||||
let log = match res {
|
||||
Ok(_) => Log::simple(
|
||||
"delete build",
|
||||
format!("deleted build {}", build.name),
|
||||
),
|
||||
Err(e) => Log::error(
|
||||
"delete build",
|
||||
format!("failed to delete build\n{e:#?}"),
|
||||
),
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update.finalize();
|
||||
update_update(update).await?;
|
||||
|
||||
remove_from_recently_viewed(&build).await?;
|
||||
|
||||
Ok(build)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<UpdateBuild, User> for State {
|
||||
#[instrument(name = "UpdateBuild", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateBuild { id, mut config }: UpdateBuild,
|
||||
user: User,
|
||||
) -> anyhow::Result<Build> {
|
||||
if action_states()
|
||||
.build
|
||||
.get(&id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.busy()?
|
||||
{
|
||||
return Err(anyhow!("build busy"));
|
||||
}
|
||||
|
||||
let build = Build::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(builder_id) = &config.builder_id {
|
||||
let builder = Builder::get_resource_check_permissions(builder_id, &user, PermissionLevel::Read)
|
||||
.await
|
||||
.context("cannot create build using this builder. user must have at least read permissions on the builder.")?;
|
||||
config.builder_id = Some(builder.id)
|
||||
}
|
||||
|
||||
if let Some(build_args) = &mut config.build_args {
|
||||
build_args.retain(|v| {
|
||||
!empty_or_only_spaces(&v.variable)
|
||||
&& !empty_or_only_spaces(&v.value)
|
||||
})
|
||||
}
|
||||
if let Some(extra_args) = &mut config.extra_args {
|
||||
extra_args.retain(|v| !empty_or_only_spaces(v))
|
||||
}
|
||||
|
||||
let config_doc = to_document(&config)
|
||||
.context("failed to serialize config to bson document")?;
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.builds,
|
||||
&build.id,
|
||||
mungos::update::Update::FlattenSet(
|
||||
doc! { "config": config_doc },
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to update build on database")?;
|
||||
|
||||
let mut update =
|
||||
make_update(&build, Operation::UpdateBuild, &user);
|
||||
|
||||
update.push_simple_log(
|
||||
"build update",
|
||||
serde_json::to_string_pretty(&config)
|
||||
.context("failed to serialize config to json")?,
|
||||
);
|
||||
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
let build = Build::get_resource(&build.id).await?;
|
||||
Ok(build)
|
||||
}
|
||||
}
|
||||
283
bin/core/src/api/write/builder.rs
Normal file
283
bin/core/src/api/write/builder.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
builder::{
|
||||
Builder, PartialBuilderConfig, PartialServerBuilderConfig,
|
||||
},
|
||||
monitor_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
update::{Log, ResourceTarget, Update},
|
||||
user::User,
|
||||
Operation,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::{delete_one_by_id, update_one_by_id},
|
||||
mongodb::bson::{doc, oid::ObjectId, to_document},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
create_permission, remove_from_recently_viewed,
|
||||
resource::{delete_all_permissions_on_resource, StateResource},
|
||||
update::{add_update, make_update},
|
||||
},
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
#[instrument(skip(user))]
|
||||
async fn validate_config(
|
||||
config: &mut PartialBuilderConfig,
|
||||
user: &User,
|
||||
) -> anyhow::Result<()> {
|
||||
match config {
|
||||
PartialBuilderConfig::Server(PartialServerBuilderConfig {
|
||||
server_id: Some(server_id),
|
||||
}) if !server_id.is_empty() => {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
server_id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
*server_id = server.id;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CreateBuilder, User> for State {
|
||||
#[instrument(name = "CreateBuilder", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CreateBuilder { name, mut config }: CreateBuilder,
|
||||
user: User,
|
||||
) -> anyhow::Result<Builder> {
|
||||
let start_ts = monitor_timestamp();
|
||||
if ObjectId::from_str(&name).is_ok() {
|
||||
return Err(anyhow!("valid ObjectIds cannot be used as names"));
|
||||
}
|
||||
validate_config(&mut config, &user).await?;
|
||||
|
||||
let builder = Builder {
|
||||
id: Default::default(),
|
||||
name,
|
||||
updated_at: start_ts,
|
||||
description: Default::default(),
|
||||
tags: Default::default(),
|
||||
config: config.into(),
|
||||
info: Default::default(),
|
||||
};
|
||||
let builder_id = db_client()
|
||||
.await
|
||||
.builders
|
||||
.insert_one(builder, None)
|
||||
.await
|
||||
.context("failed to add builder to db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
let builder = Builder::get_resource(&builder_id).await?;
|
||||
create_permission(&user, &builder, PermissionLevel::Write).await;
|
||||
let update = Update {
|
||||
target: ResourceTarget::Builder(builder_id),
|
||||
operation: Operation::CreateBuilder,
|
||||
start_ts,
|
||||
end_ts: Some(monitor_timestamp()),
|
||||
operator: user.id.clone(),
|
||||
success: true,
|
||||
logs: vec![
|
||||
Log::simple(
|
||||
"create builder",
|
||||
format!(
|
||||
"created builder\nid: {}\nname: {}",
|
||||
builder.id, builder.name
|
||||
),
|
||||
),
|
||||
Log::simple("config", format!("{:#?}", builder.config)),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(builder)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CopyBuilder, User> for State {
|
||||
#[instrument(name = "CopyBuilder", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CopyBuilder { name, id }: CopyBuilder,
|
||||
user: User,
|
||||
) -> anyhow::Result<Builder> {
|
||||
let Builder {
|
||||
config,
|
||||
description,
|
||||
..
|
||||
} = Builder::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
let start_ts = monitor_timestamp();
|
||||
let builder = Builder {
|
||||
id: Default::default(),
|
||||
name,
|
||||
updated_at: start_ts,
|
||||
description,
|
||||
tags: Default::default(),
|
||||
config,
|
||||
info: (),
|
||||
};
|
||||
let builder_id = db_client()
|
||||
.await
|
||||
.builders
|
||||
.insert_one(builder, None)
|
||||
.await
|
||||
.context("failed to add builder to db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
let builder = Builder::get_resource(&builder_id).await?;
|
||||
create_permission(&user, &builder, PermissionLevel::Write).await;
|
||||
let update = Update {
|
||||
target: ResourceTarget::Builder(builder_id),
|
||||
operation: Operation::CreateBuilder,
|
||||
start_ts,
|
||||
end_ts: Some(monitor_timestamp()),
|
||||
operator: user.id.clone(),
|
||||
success: true,
|
||||
logs: vec![
|
||||
Log::simple(
|
||||
"create builder",
|
||||
format!(
|
||||
"created builder\nid: {}\nname: {}",
|
||||
builder.id, builder.name
|
||||
),
|
||||
),
|
||||
Log::simple("config", format!("{:#?}", builder.config)),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(builder)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<DeleteBuilder, User> for State {
|
||||
#[instrument(name = "DeleteBuilder", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeleteBuilder { id }: DeleteBuilder,
|
||||
user: User,
|
||||
) -> anyhow::Result<Builder> {
|
||||
let builder = Builder::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// remove the builder from any attached builds
|
||||
db_client()
|
||||
.await
|
||||
.builds
|
||||
.update_many(
|
||||
doc! { "config.builder.params.builder_id": &id },
|
||||
mungos::update::Update::Set(
|
||||
doc! { "config.builder.params.builder_id": "" },
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
delete_one_by_id(&db_client().await.builders, &id, None)
|
||||
.await
|
||||
.context("failed to delete builder from database")?;
|
||||
|
||||
delete_all_permissions_on_resource(&builder).await;
|
||||
|
||||
let mut update =
|
||||
make_update(&builder, Operation::DeleteBuilder, &user);
|
||||
|
||||
update.push_simple_log(
|
||||
"delete builder",
|
||||
format!("deleted builder {}", builder.name),
|
||||
);
|
||||
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
|
||||
remove_from_recently_viewed(&builder).await?;
|
||||
|
||||
Ok(builder)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<UpdateBuilder, User> for State {
|
||||
#[instrument(name = "UpdateBuilder", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateBuilder { id, mut config }: UpdateBuilder,
|
||||
user: User,
|
||||
) -> anyhow::Result<Builder> {
|
||||
let builder = Builder::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
validate_config(&mut config, &user).await?;
|
||||
|
||||
let mut update = Update {
|
||||
target: ResourceTarget::Builder(id.clone()),
|
||||
operation: Operation::UpdateBuilder,
|
||||
start_ts: monitor_timestamp(),
|
||||
logs: vec![Log::simple(
|
||||
"builder update",
|
||||
serde_json::to_string_pretty(&config)
|
||||
.context("failed to serialize config update")?,
|
||||
)],
|
||||
operator: user.id.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let config = builder.config.merge_partial(config);
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.builders,
|
||||
&id,
|
||||
mungos::update::Update::FlattenSet(
|
||||
doc! { "config": to_document(&config)? },
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let builder = Builder::get_resource(&id).await?;
|
||||
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(builder)
|
||||
}
|
||||
}
|
||||
472
bin/core/src/api/write/deployment.rs
Normal file
472
bin/core/src/api/write/deployment.rs
Normal file
@@ -0,0 +1,472 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
all_logs_success,
|
||||
build::Build,
|
||||
deployment::{
|
||||
Deployment, DeploymentImage, DockerContainerState,
|
||||
PartialDeploymentConfig,
|
||||
},
|
||||
monitor_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
to_monitor_name,
|
||||
update::{Log, Update, UpdateStatus},
|
||||
user::User,
|
||||
Operation,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::{delete_one_by_id, update_one_by_id},
|
||||
mongodb::bson::{doc, oid::ObjectId, to_document},
|
||||
};
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
create_permission, empty_or_only_spaces, periphery_client,
|
||||
query::get_deployment_state,
|
||||
remove_from_recently_viewed,
|
||||
resource::{delete_all_permissions_on_resource, StateResource},
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
state::{action_states, db_client, State},
|
||||
};
|
||||
|
||||
#[instrument(skip(user))]
|
||||
async fn validate_config(
|
||||
config: &mut PartialDeploymentConfig,
|
||||
user: &User,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(server_id) = &config.server_id {
|
||||
if !server_id.is_empty() {
|
||||
let server = Server::get_resource_check_permissions(server_id, user, PermissionLevel::Write)
|
||||
.await
|
||||
.context("cannot create deployment on this server. user must have update permissions on the server to perform this action.")?;
|
||||
config.server_id = Some(server.id);
|
||||
}
|
||||
}
|
||||
if let Some(DeploymentImage::Build { build_id, version }) =
|
||||
&config.image
|
||||
{
|
||||
if !build_id.is_empty() {
|
||||
let build = Build::get_resource_check_permissions(build_id, user, PermissionLevel::Read)
|
||||
.await
|
||||
.context("cannot create deployment with this build attached. user must have at least read permissions on the build to perform this action.")?;
|
||||
config.image = Some(DeploymentImage::Build {
|
||||
build_id: build.id,
|
||||
version: version.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(volumes) = &mut config.volumes {
|
||||
volumes.retain(|v| {
|
||||
!empty_or_only_spaces(&v.local)
|
||||
&& !empty_or_only_spaces(&v.container)
|
||||
})
|
||||
}
|
||||
if let Some(ports) = &mut config.ports {
|
||||
ports.retain(|v| {
|
||||
!empty_or_only_spaces(&v.local)
|
||||
&& !empty_or_only_spaces(&v.container)
|
||||
})
|
||||
}
|
||||
if let Some(environment) = &mut config.environment {
|
||||
environment.retain(|v| {
|
||||
!empty_or_only_spaces(&v.variable)
|
||||
&& !empty_or_only_spaces(&v.value)
|
||||
})
|
||||
}
|
||||
if let Some(extra_args) = &mut config.extra_args {
|
||||
extra_args.retain(|v| !empty_or_only_spaces(v))
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CreateDeployment, User> for State {
|
||||
#[instrument(name = "CreateDeployment", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CreateDeployment { name, mut config }: CreateDeployment,
|
||||
user: User,
|
||||
) -> anyhow::Result<Deployment> {
|
||||
let name = to_monitor_name(&name);
|
||||
if ObjectId::from_str(&name).is_ok() {
|
||||
return Err(anyhow!("valid ObjectIds cannot be used as names"));
|
||||
}
|
||||
validate_config(&mut config, &user).await?;
|
||||
let start_ts = monitor_timestamp();
|
||||
let deployment = Deployment {
|
||||
id: Default::default(),
|
||||
name,
|
||||
updated_at: start_ts,
|
||||
description: Default::default(),
|
||||
tags: Default::default(),
|
||||
config: config.into(),
|
||||
info: (),
|
||||
};
|
||||
let deployment_id = db_client()
|
||||
.await
|
||||
.deployments
|
||||
.insert_one(&deployment, None)
|
||||
.await
|
||||
.context("failed to add deployment to db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
let deployment = Deployment::get_resource(&deployment_id).await?;
|
||||
create_permission(&user, &deployment, PermissionLevel::Write)
|
||||
.await;
|
||||
|
||||
let mut update =
|
||||
make_update(&deployment, Operation::CreateDeployment, &user);
|
||||
update.push_simple_log(
|
||||
"create deployment",
|
||||
format!(
|
||||
"created deployment\nid: {}\nname: {}",
|
||||
deployment.id, deployment.name
|
||||
),
|
||||
);
|
||||
update
|
||||
.push_simple_log("config", format!("{:#?}", deployment.config));
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(deployment)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CopyDeployment, User> for State {
|
||||
#[instrument(name = "CopyDeployment", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CopyDeployment { name, id }: CopyDeployment,
|
||||
user: User,
|
||||
) -> anyhow::Result<Deployment> {
|
||||
let name = to_monitor_name(&name);
|
||||
let Deployment {
|
||||
config,
|
||||
description,
|
||||
tags,
|
||||
..
|
||||
} = Deployment::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
if !config.server_id.is_empty() {
|
||||
Server::get_resource_check_permissions(&config.server_id, &user, PermissionLevel::Write)
|
||||
.await
|
||||
.context("cannot create deployment on this server. user must have update permissions on the server to perform this action.")?;
|
||||
}
|
||||
if let DeploymentImage::Build { build_id, .. } = &config.image {
|
||||
if !build_id.is_empty() {
|
||||
Build::get_resource_check_permissions(build_id, &user, PermissionLevel::Read)
|
||||
.await
|
||||
.context("cannot create deployment with this build attached. user must have at least read permissions on the build to perform this action.")?;
|
||||
}
|
||||
}
|
||||
let start_ts = monitor_timestamp();
|
||||
let deployment = Deployment {
|
||||
id: Default::default(),
|
||||
name,
|
||||
updated_at: start_ts,
|
||||
description,
|
||||
tags,
|
||||
config,
|
||||
info: (),
|
||||
};
|
||||
let deployment_id = db_client()
|
||||
.await
|
||||
.deployments
|
||||
.insert_one(&deployment, None)
|
||||
.await
|
||||
.context("failed to add deployment to db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
let deployment = Deployment::get_resource(&deployment_id).await?;
|
||||
|
||||
create_permission(&user, &deployment, PermissionLevel::Write)
|
||||
.await;
|
||||
|
||||
let mut update =
|
||||
make_update(&deployment, Operation::CreateDeployment, &user);
|
||||
update.push_simple_log(
|
||||
"create deployment",
|
||||
format!(
|
||||
"created deployment\nid: {}\nname: {}",
|
||||
deployment.id, deployment.name
|
||||
),
|
||||
);
|
||||
update
|
||||
.push_simple_log("config", format!("{:#?}", deployment.config));
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(deployment)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<DeleteDeployment, User> for State {
|
||||
#[instrument(name = "DeleteDeployment", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeleteDeployment { id }: DeleteDeployment,
|
||||
user: User,
|
||||
) -> anyhow::Result<Deployment> {
|
||||
let deployment = Deployment::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 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.deleting = true)?;
|
||||
|
||||
let state = get_deployment_state(&deployment)
|
||||
.await
|
||||
.context("failed to get container state")?;
|
||||
|
||||
let mut update =
|
||||
make_update(&deployment, Operation::DeleteDeployment, &user);
|
||||
update.in_progress();
|
||||
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
if !matches!(
|
||||
state,
|
||||
DockerContainerState::NotDeployed
|
||||
| DockerContainerState::Unknown
|
||||
) {
|
||||
// container needs to be destroyed
|
||||
let server =
|
||||
Server::get_resource(&deployment.config.server_id).await;
|
||||
if let Err(e) = server {
|
||||
update.logs.push(Log::error(
|
||||
"remove container",
|
||||
format!(
|
||||
"failed to retrieve server at {} from db | {e:#?}",
|
||||
deployment.config.server_id
|
||||
),
|
||||
));
|
||||
} else if let Ok(server) = server {
|
||||
match periphery_client(&server) {
|
||||
Ok(periphery) => match periphery
|
||||
.request(api::container::RemoveContainer {
|
||||
name: deployment.name.clone(),
|
||||
signal: deployment.config.termination_signal.into(),
|
||||
time: deployment.config.termination_timeout.into(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update.push_error_log(
|
||||
"remove container",
|
||||
format!(
|
||||
"failed to remove container on periphery | {e:#?}"
|
||||
),
|
||||
),
|
||||
},
|
||||
Err(e) => update.push_error_log(
|
||||
"remove container",
|
||||
format!(
|
||||
"failed to remove container on periphery | {e:#?}"
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let res = delete_one_by_id(
|
||||
&db_client().await.deployments,
|
||||
&deployment.id,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to delete deployment from mongo");
|
||||
|
||||
let log = match res {
|
||||
Ok(_) => Log::simple(
|
||||
"delete deployment",
|
||||
format!("deleted deployment {}", deployment.name),
|
||||
),
|
||||
Err(e) => Log::error(
|
||||
"delete deployment",
|
||||
format!("failed to delete deployment\n{e:#?}"),
|
||||
),
|
||||
};
|
||||
|
||||
delete_all_permissions_on_resource(&deployment).await;
|
||||
|
||||
update.logs.push(log);
|
||||
update.end_ts = Some(monitor_timestamp());
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.success = all_logs_success(&update.logs);
|
||||
|
||||
update_update(update).await?;
|
||||
|
||||
remove_from_recently_viewed(&deployment).await?;
|
||||
|
||||
Ok(deployment)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<UpdateDeployment, User> for State {
|
||||
#[instrument(name = "UpdateDeployment", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateDeployment { id, mut config }: UpdateDeployment,
|
||||
user: User,
|
||||
) -> anyhow::Result<Deployment> {
|
||||
if action_states()
|
||||
.deployment
|
||||
.get(&id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.busy()?
|
||||
{
|
||||
return Err(anyhow!("deployment busy"));
|
||||
}
|
||||
|
||||
let deployment = Deployment::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut update =
|
||||
make_update(&deployment, Operation::UpdateDeployment, &user);
|
||||
|
||||
validate_config(&mut config, &user).await?;
|
||||
|
||||
let config_doc = to_document(&config)
|
||||
.context("failed to serialize config to bson")?;
|
||||
update_one_by_id(
|
||||
&db_client().await.deployments,
|
||||
&id,
|
||||
mungos::update::Update::FlattenSet(
|
||||
doc! { "config": config_doc },
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to update server on mongo")?;
|
||||
|
||||
update.push_simple_log(
|
||||
"deployment update",
|
||||
serde_json::to_string_pretty(&config)
|
||||
.context("failed to serialize config to json")?,
|
||||
);
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
let deployment: Deployment =
|
||||
Deployment::get_resource(&id).await?;
|
||||
|
||||
Ok(deployment)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<RenameDeployment, User> for State {
|
||||
#[instrument(name = "RenameDeployment", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
RenameDeployment { id, name }: RenameDeployment,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let deployment = Deployment::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 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.renaming = true)?;
|
||||
|
||||
let name = to_monitor_name(&name);
|
||||
|
||||
let container_state = get_deployment_state(&deployment).await?;
|
||||
|
||||
if container_state == DockerContainerState::Unknown {
|
||||
return Err(anyhow!(
|
||||
"cannot rename deployment when container status is unknown"
|
||||
));
|
||||
}
|
||||
|
||||
let mut update =
|
||||
make_update(&deployment, Operation::RenameDeployment, &user);
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.deployments,
|
||||
&deployment.id,
|
||||
mungos::update::Update::Set(
|
||||
doc! { "name": &name, "updated_at": monitor_timestamp() },
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to update deployment name on db")?;
|
||||
|
||||
if container_state != DockerContainerState::NotDeployed {
|
||||
let server =
|
||||
Server::get_resource(&deployment.config.server_id).await?;
|
||||
let log = periphery_client(&server)?
|
||||
.request(api::container::RenameContainer {
|
||||
curr_name: deployment.name.clone(),
|
||||
new_name: name.clone(),
|
||||
})
|
||||
.await
|
||||
.context("failed to rename container on server")?;
|
||||
update.logs.push(log);
|
||||
}
|
||||
|
||||
update.push_simple_log(
|
||||
"rename deployment",
|
||||
format!(
|
||||
"renamed deployment from {} to {}",
|
||||
deployment.name, name
|
||||
),
|
||||
);
|
||||
update.finalize();
|
||||
|
||||
add_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
63
bin/core/src/api/write/description.rs
Normal file
63
bin/core/src/api/write/description.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use anyhow::anyhow;
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::write::{UpdateDescription, UpdateDescriptionResponse},
|
||||
entities::{
|
||||
alerter::Alerter, build::Build, builder::Builder,
|
||||
deployment::Deployment, procedure::Procedure, repo::Repo,
|
||||
server::Server, server_template::ServerTemplate,
|
||||
update::ResourceTarget, user::User,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{helpers::resource::StateResource, state::State};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<UpdateDescription, User> for State {
|
||||
#[instrument(name = "UpdateDescription", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateDescription {
|
||||
target,
|
||||
description,
|
||||
}: UpdateDescription,
|
||||
user: User,
|
||||
) -> anyhow::Result<UpdateDescriptionResponse> {
|
||||
match target {
|
||||
ResourceTarget::System(_) => {
|
||||
return Err(anyhow!(
|
||||
"cannot update description of System resource target"
|
||||
))
|
||||
}
|
||||
ResourceTarget::Server(id) => {
|
||||
Server::update_description(&id, &description, &user).await?;
|
||||
}
|
||||
ResourceTarget::Deployment(id) => {
|
||||
Deployment::update_description(&id, &description, &user)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Build(id) => {
|
||||
Build::update_description(&id, &description, &user).await?;
|
||||
}
|
||||
ResourceTarget::Repo(id) => {
|
||||
Repo::update_description(&id, &description, &user).await?;
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
Builder::update_description(&id, &description, &user).await?;
|
||||
}
|
||||
ResourceTarget::Alerter(id) => {
|
||||
Alerter::update_description(&id, &description, &user).await?;
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
Procedure::update_description(&id, &description, &user)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
ServerTemplate::update_description(&id, &description, &user)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(UpdateDescriptionResponse {})
|
||||
}
|
||||
}
|
||||
176
bin/core/src/api/write/mod.rs
Normal file
176
bin/core/src/api/write/mod.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{middleware, routing::post, Extension, Router};
|
||||
use axum_extra::{headers::ContentType, TypedHeader};
|
||||
use monitor_client::{api::write::*, entities::user::User};
|
||||
use resolver_api::{derive::Resolver, Resolve, Resolver};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serror::Json;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{auth::auth_request, state::State};
|
||||
|
||||
mod alerter;
|
||||
mod api_key;
|
||||
mod build;
|
||||
mod builder;
|
||||
mod deployment;
|
||||
mod description;
|
||||
mod permissions;
|
||||
mod procedure;
|
||||
mod repo;
|
||||
mod server;
|
||||
mod server_template;
|
||||
mod tag;
|
||||
mod user;
|
||||
mod user_group;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
|
||||
#[resolver_target(State)]
|
||||
#[resolver_args(User)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
enum WriteRequest {
|
||||
// ==== API KEY ====
|
||||
CreateApiKey(CreateApiKey),
|
||||
DeleteApiKey(DeleteApiKey),
|
||||
CreateApiKeyForServiceUser(CreateApiKeyForServiceUser),
|
||||
DeleteApiKeyForServiceUser(DeleteApiKeyForServiceUser),
|
||||
|
||||
// ==== USER ====
|
||||
PushRecentlyViewed(PushRecentlyViewed),
|
||||
SetLastSeenUpdate(SetLastSeenUpdate),
|
||||
CreateServiceUser(CreateServiceUser),
|
||||
UpdateServiceUserDescription(UpdateServiceUserDescription),
|
||||
|
||||
// ==== USER GROUP ====
|
||||
CreateUserGroup(CreateUserGroup),
|
||||
RenameUserGroup(RenameUserGroup),
|
||||
DeleteUserGroup(DeleteUserGroup),
|
||||
AddUserToUserGroup(AddUserToUserGroup),
|
||||
RemoveUserFromUserGroup(RemoveUserFromUserGroup),
|
||||
SetUsersInUserGroup(SetUsersInUserGroup),
|
||||
|
||||
// ==== PERMISSIONS ====
|
||||
UpdateUserBasePermissions(UpdateUserBasePermissions),
|
||||
UpdatePermissionOnTarget(UpdatePermissionOnTarget),
|
||||
|
||||
// ==== DESCRIPTION ====
|
||||
UpdateDescription(UpdateDescription),
|
||||
|
||||
// ==== SERVER ====
|
||||
CreateServer(CreateServer),
|
||||
DeleteServer(DeleteServer),
|
||||
UpdateServer(UpdateServer),
|
||||
RenameServer(RenameServer),
|
||||
CreateNetwork(CreateNetwork),
|
||||
DeleteNetwork(DeleteNetwork),
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
CreateDeployment(CreateDeployment),
|
||||
CopyDeployment(CopyDeployment),
|
||||
DeleteDeployment(DeleteDeployment),
|
||||
UpdateDeployment(UpdateDeployment),
|
||||
RenameDeployment(RenameDeployment),
|
||||
|
||||
// ==== BUILD ====
|
||||
CreateBuild(CreateBuild),
|
||||
CopyBuild(CopyBuild),
|
||||
DeleteBuild(DeleteBuild),
|
||||
UpdateBuild(UpdateBuild),
|
||||
|
||||
// ==== BUILDER ====
|
||||
CreateBuilder(CreateBuilder),
|
||||
CopyBuilder(CopyBuilder),
|
||||
DeleteBuilder(DeleteBuilder),
|
||||
UpdateBuilder(UpdateBuilder),
|
||||
|
||||
// ==== SERVER TEMPLATE ====
|
||||
CreateServerTemplate(CreateServerTemplate),
|
||||
CopyServerTemplate(CopyServerTemplate),
|
||||
DeleteServerTemplate(DeleteServerTemplate),
|
||||
UpdateServerTemplate(UpdateServerTemplate),
|
||||
|
||||
// ==== REPO ====
|
||||
CreateRepo(CreateRepo),
|
||||
CopyRepo(CopyRepo),
|
||||
DeleteRepo(DeleteRepo),
|
||||
UpdateRepo(UpdateRepo),
|
||||
|
||||
// ==== ALERTER ====
|
||||
CreateAlerter(CreateAlerter),
|
||||
CopyAlerter(CopyAlerter),
|
||||
DeleteAlerter(DeleteAlerter),
|
||||
UpdateAlerter(UpdateAlerter),
|
||||
|
||||
// ==== PROCEDURE ====
|
||||
CreateProcedure(CreateProcedure),
|
||||
CopyProcedure(CopyProcedure),
|
||||
DeleteProcedure(DeleteProcedure),
|
||||
UpdateProcedure(UpdateProcedure),
|
||||
|
||||
// ==== TAG ====
|
||||
CreateTag(CreateTag),
|
||||
DeleteTag(DeleteTag),
|
||||
RenameTag(RenameTag),
|
||||
UpdateTagsOnResource(UpdateTagsOnResource),
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.layer(middleware::from_fn(auth_request))
|
||||
}
|
||||
|
||||
async fn handler(
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<WriteRequest>,
|
||||
) -> serror::Result<(TypedHeader<ContentType>, String)> {
|
||||
let req_id = Uuid::new_v4();
|
||||
|
||||
let res = tokio::spawn(task(req_id, request, user))
|
||||
.await
|
||||
.context("failure in spawned task");
|
||||
|
||||
if let Err(e) = &res {
|
||||
warn!("/write request {req_id} spawn error: {e:#}");
|
||||
}
|
||||
|
||||
Ok((TypedHeader(ContentType::json()), res??))
|
||||
}
|
||||
|
||||
#[instrument(name = "WriteRequest", skip(user))]
|
||||
async fn task(
|
||||
req_id: Uuid,
|
||||
request: WriteRequest,
|
||||
user: User,
|
||||
) -> anyhow::Result<String> {
|
||||
info!(
|
||||
"/write request {req_id} | user: {} ({})",
|
||||
user.username, user.id
|
||||
);
|
||||
|
||||
let timer = Instant::now();
|
||||
|
||||
let res =
|
||||
State
|
||||
.resolve_request(request, user)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
resolver_api::Error::Serialization(e) => {
|
||||
anyhow!("{e:?}").context("response serialization error")
|
||||
}
|
||||
resolver_api::Error::Inner(e) => e,
|
||||
});
|
||||
|
||||
if let Err(e) = &res {
|
||||
warn!("/write request {req_id} error: {e:#}");
|
||||
}
|
||||
|
||||
let elapsed = timer.elapsed();
|
||||
info!("/write request {req_id} | resolve time: {elapsed:?}");
|
||||
|
||||
res
|
||||
}
|
||||
311
bin/core/src/api/write/permissions.rs
Normal file
311
bin/core/src/api/write/permissions.rs
Normal file
@@ -0,0 +1,311 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::write::{
|
||||
UpdatePermissionOnTarget, UpdatePermissionOnTargetResponse,
|
||||
UpdateUserBasePermissions, UpdateUserBasePermissionsResponse,
|
||||
},
|
||||
entities::{
|
||||
permission::{UserTarget, UserTargetVariant},
|
||||
update::{ResourceTarget, ResourceTargetVariant},
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::{find_one_by_id, update_one_by_id},
|
||||
mongodb::{
|
||||
bson::{doc, oid::ObjectId, Document},
|
||||
options::UpdateOptions,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_user,
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<UpdateUserBasePermissions, User> for State {
|
||||
#[instrument(name = "UpdateUserBasePermissions", skip(self, admin))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateUserBasePermissions {
|
||||
user_id,
|
||||
enabled,
|
||||
create_servers,
|
||||
create_builds,
|
||||
}: UpdateUserBasePermissions,
|
||||
admin: User,
|
||||
) -> anyhow::Result<UpdateUserBasePermissionsResponse> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("this method is admin only"));
|
||||
}
|
||||
let user = find_one_by_id(&db_client().await.users, &user_id)
|
||||
.await
|
||||
.context("failed to query mongo for user")?
|
||||
.context("did not find user with given id")?;
|
||||
if user.admin {
|
||||
return Err(anyhow!(
|
||||
"cannot use this method to update other admins permissions"
|
||||
));
|
||||
}
|
||||
let mut update_doc = Document::new();
|
||||
if let Some(enabled) = enabled {
|
||||
update_doc.insert("enabled", enabled);
|
||||
}
|
||||
if let Some(create_servers) = create_servers {
|
||||
update_doc.insert("create_server_permissions", create_servers);
|
||||
}
|
||||
if let Some(create_builds) = create_builds {
|
||||
update_doc.insert("create_build_permissions", create_builds);
|
||||
}
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.users,
|
||||
&user_id,
|
||||
mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(UpdateUserBasePermissionsResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<UpdatePermissionOnTarget, User> for State {
|
||||
#[instrument(name = "UpdatePermissionOnTarget", skip(self, admin))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdatePermissionOnTarget {
|
||||
user_target,
|
||||
resource_target,
|
||||
permission,
|
||||
}: UpdatePermissionOnTarget,
|
||||
admin: User,
|
||||
) -> anyhow::Result<UpdatePermissionOnTargetResponse> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("this method is admin only"));
|
||||
}
|
||||
|
||||
// Some extra checks if user target is an actual User
|
||||
if let UserTarget::User(user_id) = &user_target {
|
||||
let user = get_user(user_id).await?;
|
||||
if user.admin {
|
||||
return Err(anyhow!(
|
||||
"cannot use this method to update other admins permissions"
|
||||
));
|
||||
}
|
||||
if !user.enabled {
|
||||
return Err(anyhow!("user not enabled"));
|
||||
}
|
||||
}
|
||||
|
||||
let (user_target_variant, user_target_id) =
|
||||
extract_user_target_with_validation(&user_target).await?;
|
||||
let (resource_variant, resource_id) =
|
||||
extract_resource_target_with_validation(&resource_target)
|
||||
.await?;
|
||||
|
||||
let (user_target_variant, resource_variant) =
|
||||
(user_target_variant.as_ref(), resource_variant.as_ref());
|
||||
|
||||
db_client()
|
||||
.await
|
||||
.permissions
|
||||
.update_one(
|
||||
doc! {
|
||||
"user_target.type": user_target_variant,
|
||||
"user_target.id": &user_target_id,
|
||||
"resource_target.type": resource_variant,
|
||||
"resource_target.id": &resource_id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"user_target.type": user_target_variant,
|
||||
"user_target.id": user_target_id,
|
||||
"resource_target.type": resource_variant,
|
||||
"resource_target.id": resource_id,
|
||||
"level": permission.as_ref(),
|
||||
}
|
||||
},
|
||||
UpdateOptions::builder().upsert(true).build(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(UpdatePermissionOnTargetResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
/// checks if inner id is actually a `name`, and replaces it with id if so.
|
||||
async fn extract_user_target_with_validation(
|
||||
user_target: &UserTarget,
|
||||
) -> anyhow::Result<(UserTargetVariant, String)> {
|
||||
match user_target {
|
||||
UserTarget::User(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "username": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.users
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query db for users")?
|
||||
.context("no matching user found")?
|
||||
.id;
|
||||
Ok((UserTargetVariant::User, id))
|
||||
}
|
||||
UserTarget::UserGroup(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.user_groups
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query db for user_groups")?
|
||||
.context("no matching user_group found")?
|
||||
.id;
|
||||
Ok((UserTargetVariant::UserGroup, id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// checks if inner id is actually a `name`, and replaces it with id if so.
|
||||
async fn extract_resource_target_with_validation(
|
||||
resource_target: &ResourceTarget,
|
||||
) -> anyhow::Result<(ResourceTargetVariant, String)> {
|
||||
match resource_target {
|
||||
ResourceTarget::System(_) => {
|
||||
let res = resource_target.extract_variant_id();
|
||||
Ok((res.0, res.1.clone()))
|
||||
}
|
||||
ResourceTarget::Build(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.builds
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query db for builds")?
|
||||
.context("no matching build found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Build, id))
|
||||
}
|
||||
ResourceTarget::Builder(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.builders
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query db for builders")?
|
||||
.context("no matching builder found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Builder, id))
|
||||
}
|
||||
ResourceTarget::Deployment(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.deployments
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query db for deployments")?
|
||||
.context("no matching deployment found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Deployment, id))
|
||||
}
|
||||
ResourceTarget::Server(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.servers
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query db for servers")?
|
||||
.context("no matching server found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Server, id))
|
||||
}
|
||||
ResourceTarget::Repo(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.repos
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query db for repos")?
|
||||
.context("no matching repo found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Repo, id))
|
||||
}
|
||||
ResourceTarget::Alerter(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.alerters
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query db for alerters")?
|
||||
.context("no matching alerter found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Alerter, id))
|
||||
}
|
||||
ResourceTarget::Procedure(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.procedures
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query db for procedures")?
|
||||
.context("no matching procedure found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Procedure, id))
|
||||
}
|
||||
ResourceTarget::ServerTemplate(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.await
|
||||
.server_templates
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query db for server templates")?
|
||||
.context("no matching server template found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::ServerTemplate, id))
|
||||
}
|
||||
}
|
||||
}
|
||||
405
bin/core/src/api/write/procedure.rs
Normal file
405
bin/core/src/api/write/procedure.rs
Normal file
@@ -0,0 +1,405 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::{execute::Execution, write::*},
|
||||
entities::{
|
||||
build::Build,
|
||||
deployment::Deployment,
|
||||
monitor_timestamp,
|
||||
permission::PermissionLevel,
|
||||
procedure::{PartialProcedureConfig, Procedure},
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
update::Log,
|
||||
user::User,
|
||||
Operation,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::{delete_one_by_id, update_one_by_id},
|
||||
mongodb::bson::{doc, oid::ObjectId, to_document},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
create_permission, remove_from_recently_viewed,
|
||||
resource::{delete_all_permissions_on_resource, StateResource},
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
state::{action_states, db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CreateProcedure, User> for State {
|
||||
#[instrument(name = "CreateProcedure", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CreateProcedure { name, mut config }: CreateProcedure,
|
||||
user: User,
|
||||
) -> anyhow::Result<CreateProcedureResponse> {
|
||||
if ObjectId::from_str(&name).is_ok() {
|
||||
return Err(anyhow!("valid ObjectIds cannot be used as names"));
|
||||
}
|
||||
|
||||
let start_ts = monitor_timestamp();
|
||||
|
||||
validate_procedure_config(&mut config, &user, None).await?;
|
||||
|
||||
let procedure = Procedure {
|
||||
id: Default::default(),
|
||||
name,
|
||||
updated_at: start_ts,
|
||||
description: Default::default(),
|
||||
tags: Default::default(),
|
||||
info: Default::default(),
|
||||
config: config.into(),
|
||||
};
|
||||
let procedure_id = db_client()
|
||||
.await
|
||||
.procedures
|
||||
.insert_one(procedure, None)
|
||||
.await
|
||||
.context("failed to add procedure to db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
let procedure = Procedure::get_resource(&procedure_id).await?;
|
||||
|
||||
create_permission(&user, &procedure, PermissionLevel::Write)
|
||||
.await;
|
||||
|
||||
let mut update =
|
||||
make_update(&procedure, Operation::CreateProcedure, &user);
|
||||
|
||||
update.push_simple_log(
|
||||
"create procedure",
|
||||
format!(
|
||||
"created procedure\nid: {}\nname: {}",
|
||||
procedure.id, procedure.name
|
||||
),
|
||||
);
|
||||
|
||||
update
|
||||
.push_simple_log("config", format!("{:#?}", procedure.config));
|
||||
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(procedure)
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn validate_procedure_config(
|
||||
config: &mut PartialProcedureConfig,
|
||||
user: &User,
|
||||
id: Option<&str>,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(executions) = &mut config.executions else {
|
||||
return Ok(());
|
||||
};
|
||||
for exec in executions {
|
||||
match &mut exec.execution {
|
||||
Execution::None(_) => {}
|
||||
Execution::RunProcedure(params) => {
|
||||
let procedure = Procedure::get_resource_check_permissions(
|
||||
¶ms.procedure,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
match id {
|
||||
Some(id) if procedure.id == id => {
|
||||
return Err(anyhow!(
|
||||
"Cannot have self-referential procedure"
|
||||
))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
params.procedure = procedure.id;
|
||||
}
|
||||
Execution::RunBuild(params) => {
|
||||
let build = Build::get_resource_check_permissions(
|
||||
¶ms.build,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
params.build = build.id;
|
||||
}
|
||||
Execution::Deploy(params) => {
|
||||
let deployment = Deployment::get_resource_check_permissions(
|
||||
¶ms.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
params.deployment = deployment.id;
|
||||
}
|
||||
Execution::StartContainer(params) => {
|
||||
let deployment = Deployment::get_resource_check_permissions(
|
||||
¶ms.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
params.deployment = deployment.id;
|
||||
}
|
||||
Execution::StopContainer(params) => {
|
||||
let deployment = Deployment::get_resource_check_permissions(
|
||||
¶ms.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
params.deployment = deployment.id;
|
||||
}
|
||||
Execution::StopAllContainers(params) => {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
¶ms.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
params.server = server.id;
|
||||
}
|
||||
Execution::RemoveContainer(params) => {
|
||||
let deployment = Deployment::get_resource_check_permissions(
|
||||
¶ms.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
params.deployment = deployment.id;
|
||||
}
|
||||
Execution::CloneRepo(params) => {
|
||||
let repo = Repo::get_resource_check_permissions(
|
||||
¶ms.repo,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
params.repo = repo.id;
|
||||
}
|
||||
Execution::PullRepo(params) => {
|
||||
let repo = Repo::get_resource_check_permissions(
|
||||
¶ms.repo,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
params.repo = repo.id;
|
||||
}
|
||||
Execution::PruneDockerNetworks(params) => {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
¶ms.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
params.server = server.id;
|
||||
}
|
||||
Execution::PruneDockerImages(params) => {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
¶ms.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
params.server = server.id;
|
||||
}
|
||||
Execution::PruneDockerContainers(params) => {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
¶ms.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
params.server = server.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CopyProcedure, User> for State {
|
||||
#[instrument(name = "CopyProcedure", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CopyProcedure { name, id }: CopyProcedure,
|
||||
user: User,
|
||||
) -> anyhow::Result<CopyProcedureResponse> {
|
||||
let Procedure {
|
||||
config,
|
||||
description,
|
||||
tags,
|
||||
..
|
||||
} = Procedure::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
let start_ts = monitor_timestamp();
|
||||
let build = Procedure {
|
||||
id: Default::default(),
|
||||
name,
|
||||
updated_at: start_ts,
|
||||
description,
|
||||
tags,
|
||||
config,
|
||||
info: Default::default(),
|
||||
};
|
||||
let procedure_id = db_client()
|
||||
.await
|
||||
.procedures
|
||||
.insert_one(build, None)
|
||||
.await
|
||||
.context("failed to add build to db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
let procedure = Procedure::get_resource(&procedure_id).await?;
|
||||
|
||||
create_permission(&user, &procedure, PermissionLevel::Write)
|
||||
.await;
|
||||
|
||||
let mut update =
|
||||
make_update(&procedure, Operation::CreateProcedure, &user);
|
||||
|
||||
update.push_simple_log(
|
||||
"create procedure",
|
||||
format!(
|
||||
"created procedure\nid: {}\nname: {}",
|
||||
procedure.id, procedure.name
|
||||
),
|
||||
);
|
||||
update.push_simple_log(
|
||||
"config",
|
||||
serde_json::to_string_pretty(&procedure)?,
|
||||
);
|
||||
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(procedure)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<UpdateProcedure, User> for State {
|
||||
#[instrument(name = "UpdateProcedure", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateProcedure { id, mut config }: UpdateProcedure,
|
||||
user: User,
|
||||
) -> anyhow::Result<UpdateProcedureResponse> {
|
||||
let procedure = Procedure::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
validate_procedure_config(
|
||||
&mut config,
|
||||
&user,
|
||||
Some(&procedure.id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.procedures,
|
||||
&procedure.id,
|
||||
mungos::update::Update::FlattenSet(
|
||||
doc! { "config": to_document(&config)? },
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to update procedure on database")?;
|
||||
|
||||
let mut update =
|
||||
make_update(&procedure, Operation::UpdateProcedure, &user);
|
||||
|
||||
update.push_simple_log(
|
||||
"procedure update",
|
||||
serde_json::to_string_pretty(&config)?,
|
||||
);
|
||||
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
let procedure = Procedure::get_resource(&procedure.id).await?;
|
||||
|
||||
Ok(procedure)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<DeleteProcedure, User> for State {
|
||||
#[instrument(name = "DeleteProcedure", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeleteProcedure { id }: DeleteProcedure,
|
||||
user: User,
|
||||
) -> anyhow::Result<DeleteProcedureResponse> {
|
||||
// needs to pull its id from all container procedures
|
||||
if action_states()
|
||||
.procedure
|
||||
.get(&id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.busy()?
|
||||
{
|
||||
return Err(anyhow!("procedure busy"));
|
||||
}
|
||||
|
||||
let procedure = Procedure::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut update =
|
||||
make_update(&procedure, Operation::DeleteProcedure, &user);
|
||||
update.in_progress();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
let res =
|
||||
delete_one_by_id(&db_client().await.procedures, &id, None)
|
||||
.await
|
||||
.context("failed to delete build from database");
|
||||
|
||||
delete_all_permissions_on_resource(&procedure).await;
|
||||
|
||||
let log = match res {
|
||||
Ok(_) => Log::simple(
|
||||
"delete procedure",
|
||||
format!("deleted procedure {}", procedure.name),
|
||||
),
|
||||
Err(e) => Log::error(
|
||||
"delete procedure",
|
||||
format!("failed to delete procedure\n{e:#?}"),
|
||||
),
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update.finalize();
|
||||
update_update(update).await?;
|
||||
|
||||
remove_from_recently_viewed(&procedure).await?;
|
||||
|
||||
Ok(procedure)
|
||||
}
|
||||
}
|
||||
358
bin/core/src/api/write/repo.rs
Normal file
358
bin/core/src/api/write/repo.rs
Normal file
@@ -0,0 +1,358 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::{execute, write::*},
|
||||
entities::{
|
||||
monitor_timestamp,
|
||||
permission::PermissionLevel,
|
||||
repo::{PartialRepoConfig, Repo},
|
||||
server::Server,
|
||||
to_monitor_name,
|
||||
update::{Log, ResourceTarget, Update},
|
||||
user::User,
|
||||
Operation,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::{delete_one_by_id, update_one_by_id},
|
||||
mongodb::bson::{doc, oid::ObjectId, to_bson},
|
||||
};
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
create_permission, periphery_client, remove_from_recently_viewed,
|
||||
resource::{delete_all_permissions_on_resource, StateResource},
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
state::{action_states, db_client, State},
|
||||
};
|
||||
|
||||
#[instrument(skip(user))]
|
||||
async fn validate_config(
|
||||
config: &mut PartialRepoConfig,
|
||||
user: &User,
|
||||
) -> anyhow::Result<()> {
|
||||
match &config.server_id {
|
||||
Some(server_id) if !server_id.is_empty() => {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
server_id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await
|
||||
.context("cannot create repo on this server. user must have update permissions on the server.")?;
|
||||
config.server_id = Some(server.id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CreateRepo, User> for State {
|
||||
#[instrument(name = "CreateRepo", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CreateRepo { name, mut config }: CreateRepo,
|
||||
user: User,
|
||||
) -> anyhow::Result<Repo> {
|
||||
let name = to_monitor_name(&name);
|
||||
if ObjectId::from_str(&name).is_ok() {
|
||||
return Err(anyhow!("valid ObjectIds cannot be used as names"));
|
||||
}
|
||||
validate_config(&mut config, &user).await?;
|
||||
let start_ts = monitor_timestamp();
|
||||
let repo = Repo {
|
||||
id: Default::default(),
|
||||
name,
|
||||
updated_at: start_ts,
|
||||
description: Default::default(),
|
||||
tags: Default::default(),
|
||||
config: config.into(),
|
||||
info: Default::default(),
|
||||
};
|
||||
let repo_id = db_client()
|
||||
.await
|
||||
.repos
|
||||
.insert_one(repo, None)
|
||||
.await
|
||||
.context("failed to add repo to db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
|
||||
let repo = Repo::get_resource(&repo_id).await?;
|
||||
|
||||
create_permission(&user, &repo, PermissionLevel::Write).await;
|
||||
|
||||
let update = Update {
|
||||
target: ResourceTarget::Repo(repo_id),
|
||||
operation: Operation::CreateRepo,
|
||||
start_ts,
|
||||
end_ts: Some(monitor_timestamp()),
|
||||
operator: user.id.clone(),
|
||||
success: true,
|
||||
logs: vec![
|
||||
Log::simple(
|
||||
"create repo",
|
||||
format!(
|
||||
"created repo\nid: {}\nname: {}",
|
||||
repo.id, repo.name
|
||||
),
|
||||
),
|
||||
Log::simple("config", format!("{:#?}", repo.config)),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
if !repo.config.repo.is_empty()
|
||||
&& !repo.config.server_id.is_empty()
|
||||
{
|
||||
let _ = self
|
||||
.resolve(
|
||||
execute::CloneRepo {
|
||||
repo: repo.id.clone(),
|
||||
},
|
||||
user,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(repo)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CopyRepo, User> for State {
|
||||
#[instrument(name = "CopyRepo", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CopyRepo { name, id }: CopyRepo,
|
||||
user: User,
|
||||
) -> anyhow::Result<Repo> {
|
||||
let Repo {
|
||||
config,
|
||||
description,
|
||||
tags,
|
||||
..
|
||||
} = Repo::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
if !config.server_id.is_empty() {
|
||||
Server::get_resource_check_permissions(
|
||||
&config.server_id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await
|
||||
.context("cannot create repo on this server. user must have update permissions on the server.")?;
|
||||
}
|
||||
let start_ts = monitor_timestamp();
|
||||
let repo = Repo {
|
||||
id: Default::default(),
|
||||
name,
|
||||
updated_at: start_ts,
|
||||
description,
|
||||
tags,
|
||||
config,
|
||||
info: Default::default(),
|
||||
};
|
||||
let repo_id = db_client()
|
||||
.await
|
||||
.repos
|
||||
.insert_one(repo, None)
|
||||
.await
|
||||
.context("failed to add repo to db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
let repo = Repo::get_resource(&repo_id).await?;
|
||||
create_permission(&user, &repo, PermissionLevel::Write).await;
|
||||
let mut update = make_update(&repo, Operation::CreateRepo, &user);
|
||||
update.push_simple_log(
|
||||
"create repo",
|
||||
format!("created repo\nid: {}\nname: {}", repo.id, repo.name),
|
||||
);
|
||||
update.push_simple_log("config", format!("{:#?}", repo.config));
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(repo)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<DeleteRepo, User> for State {
|
||||
#[instrument(name = "DeleteRepo", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeleteRepo { id }: DeleteRepo,
|
||||
user: User,
|
||||
) -> anyhow::Result<Repo> {
|
||||
let repo = Repo::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.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.deleting = true)?;
|
||||
|
||||
let periphery = if repo.config.server_id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let server =
|
||||
Server::get_resource(&repo.config.server_id).await?;
|
||||
let periphery = periphery_client(&server)?;
|
||||
Some(periphery)
|
||||
};
|
||||
|
||||
let mut update = make_update(&repo, Operation::DeleteRepo, &user);
|
||||
update.in_progress();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
let res =
|
||||
delete_one_by_id(&db_client().await.repos, &repo.id, None)
|
||||
.await
|
||||
.context("failed to delete repo from database");
|
||||
|
||||
delete_all_permissions_on_resource(&repo).await;
|
||||
|
||||
let log = match res {
|
||||
Ok(_) => Log::simple(
|
||||
"delete repo",
|
||||
format!("deleted repo {}", repo.name),
|
||||
),
|
||||
Err(e) => Log::error(
|
||||
"delete repo",
|
||||
format!("failed to delete repo\n{e:#?}"),
|
||||
),
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
|
||||
if let Some(periphery) = periphery {
|
||||
match periphery
|
||||
.request(api::git::DeleteRepo {
|
||||
name: repo.name.clone(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update.logs.push(Log::error(
|
||||
"delete repo on periphery",
|
||||
serialize_error_pretty(&e),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update_update(update).await?;
|
||||
|
||||
remove_from_recently_viewed(&repo).await?;
|
||||
|
||||
Ok(repo)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<UpdateRepo, User> for State {
|
||||
#[instrument(name = "UpdateRepo", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateRepo { id, mut config }: UpdateRepo,
|
||||
user: User,
|
||||
) -> anyhow::Result<Repo> {
|
||||
validate_config(&mut config, &user).await?;
|
||||
|
||||
let repo = Repo::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.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.updating = true)?;
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.repos,
|
||||
&repo.id,
|
||||
mungos::update::Update::FlattenSet(
|
||||
doc! { "config": to_bson(&config)? },
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to update repo on database")?;
|
||||
|
||||
let mut update = make_update(&repo, Operation::UpdateRepo, &user);
|
||||
update.in_progress();
|
||||
update.push_simple_log(
|
||||
"repo update",
|
||||
serde_json::to_string_pretty(&config).unwrap(),
|
||||
);
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
if let Some(new_server_id) = config.server_id {
|
||||
if !repo.config.server_id.is_empty()
|
||||
&& new_server_id != repo.config.server_id
|
||||
{
|
||||
let old_server: anyhow::Result<Server> =
|
||||
Server::get_resource(&repo.config.server_id).await;
|
||||
let periphery =
|
||||
old_server.and_then(|server| periphery_client(&server));
|
||||
match periphery {
|
||||
Ok(periphery) => match periphery
|
||||
.request(api::git::DeleteRepo { name: repo.name })
|
||||
.await
|
||||
{
|
||||
Ok(mut log) => {
|
||||
log.stage = String::from("cleanup previous server");
|
||||
update.logs.push(log);
|
||||
}
|
||||
Err(e) => update.push_error_log(
|
||||
"cleanup previous server",
|
||||
format!("failed to cleanup previous server | {e:#?}"),
|
||||
),
|
||||
},
|
||||
Err(e) => update.push_error_log(
|
||||
"cleanup previous server",
|
||||
format!("failed to cleanup previous server | {e:#?}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update_update(update).await?;
|
||||
|
||||
Repo::get_resource(&repo.id).await
|
||||
}
|
||||
}
|
||||
356
bin/core/src/api/write/server.rs
Normal file
356
bin/core/src/api/write/server.rs
Normal file
@@ -0,0 +1,356 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
monitor_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
update::{Log, ResourceTarget, Update, UpdateStatus},
|
||||
user::User,
|
||||
Operation,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::{delete_one_by_id, update_one_by_id},
|
||||
mongodb::bson::{doc, oid::ObjectId, to_bson},
|
||||
};
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
create_permission, periphery_client, remove_from_recently_viewed,
|
||||
resource::{delete_all_permissions_on_resource, StateResource},
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
monitor::update_cache_for_server,
|
||||
state::{action_states, db_client, server_status_cache, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CreateServer, User> for State {
|
||||
#[instrument(name = "CreateServer", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CreateServer { name, config }: CreateServer,
|
||||
user: User,
|
||||
) -> anyhow::Result<Server> {
|
||||
if !user.admin && !user.create_server_permissions {
|
||||
return Err(anyhow!(
|
||||
"user does not have create server permissions"
|
||||
));
|
||||
}
|
||||
if ObjectId::from_str(&name).is_ok() {
|
||||
return Err(anyhow!("valid ObjectIds cannot be used as names"));
|
||||
}
|
||||
let start_ts = monitor_timestamp();
|
||||
let server = Server {
|
||||
id: Default::default(),
|
||||
name,
|
||||
updated_at: start_ts,
|
||||
description: Default::default(),
|
||||
tags: Default::default(),
|
||||
config: config.into(),
|
||||
info: (),
|
||||
};
|
||||
let server_id = db_client()
|
||||
.await
|
||||
.servers
|
||||
.insert_one(&server, None)
|
||||
.await
|
||||
.context("failed to add server to db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
let server = Server::get_resource(&server_id).await?;
|
||||
create_permission(&user, &server, PermissionLevel::Write).await;
|
||||
let update = Update {
|
||||
target: ResourceTarget::Server(server_id),
|
||||
operation: Operation::CreateServer,
|
||||
start_ts,
|
||||
end_ts: Some(monitor_timestamp()),
|
||||
operator: user.id.clone(),
|
||||
success: true,
|
||||
logs: vec![
|
||||
Log::simple(
|
||||
"create server",
|
||||
format!(
|
||||
"created server\nid: {}\nname: {}",
|
||||
server.id, server.name
|
||||
),
|
||||
),
|
||||
Log::simple("config", format!("{:#?}", server.config)),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
update_cache_for_server(&server).await;
|
||||
|
||||
Ok(server)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<DeleteServer, User> for State {
|
||||
#[instrument(name = "DeleteServer", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeleteServer { id }: DeleteServer,
|
||||
user: User,
|
||||
) -> anyhow::Result<Server> {
|
||||
if action_states()
|
||||
.server
|
||||
.get(&id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.busy()?
|
||||
{
|
||||
return Err(anyhow!("server busy"));
|
||||
}
|
||||
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
db_client()
|
||||
.await
|
||||
.builders
|
||||
.update_many(
|
||||
doc! { "config.params.server_id": &id },
|
||||
doc! { "$set": { "config.params.server_id": "" } },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to detach server from builders")?;
|
||||
|
||||
db_client()
|
||||
.await
|
||||
.deployments
|
||||
.update_many(
|
||||
doc! { "config.server_id": &id },
|
||||
doc! { "$set": { "config.server_id": "" } },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to detach server from deployments")?;
|
||||
|
||||
db_client()
|
||||
.await
|
||||
.repos
|
||||
.update_many(
|
||||
doc! { "config.server_id": &id },
|
||||
doc! { "$set": { "config.server_id": "" } },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to detach server from repos")?;
|
||||
|
||||
db_client()
|
||||
.await
|
||||
.alerts
|
||||
.update_many(
|
||||
doc! { "target.type": "Server", "target.id": &id },
|
||||
doc! { "$set": {
|
||||
"resolved": true,
|
||||
"resolved_ts": unix_timestamp_ms() as i64
|
||||
} },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to detach server from repos")?;
|
||||
|
||||
delete_one_by_id(&db_client().await.servers, &id, None)
|
||||
.await
|
||||
.context("failed to delete server from mongo")?;
|
||||
|
||||
delete_all_permissions_on_resource(&server).await;
|
||||
|
||||
let mut update =
|
||||
make_update(&server, Operation::DeleteServer, &user);
|
||||
update.push_simple_log(
|
||||
"delete server",
|
||||
format!("deleted server {}", server.name),
|
||||
);
|
||||
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
|
||||
server_status_cache().remove(&id).await;
|
||||
|
||||
remove_from_recently_viewed(&server).await?;
|
||||
|
||||
Ok(server)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<UpdateServer, User> for State {
|
||||
#[instrument(name = "UpdateServer", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateServer { id, config }: UpdateServer,
|
||||
user: User,
|
||||
) -> anyhow::Result<Server> {
|
||||
if action_states()
|
||||
.server
|
||||
.get(&id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.busy()?
|
||||
{
|
||||
return Err(anyhow!("server busy"));
|
||||
}
|
||||
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
let mut update =
|
||||
make_update(&server, Operation::UpdateServer, &user);
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.servers,
|
||||
&id,
|
||||
mungos::update::Update::FlattenSet(
|
||||
doc! { "config": to_bson(&config)? },
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to update server on mongo")?;
|
||||
|
||||
update.push_simple_log(
|
||||
"server update",
|
||||
serde_json::to_string_pretty(&config)?,
|
||||
);
|
||||
|
||||
let new_server = Server::get_resource(&id).await?;
|
||||
|
||||
update_cache_for_server(&new_server).await;
|
||||
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(new_server)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<RenameServer, User> for State {
|
||||
#[instrument(name = "RenameServer", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
RenameServer { id, name }: RenameServer,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
let mut update =
|
||||
make_update(&server, Operation::RenameServer, &user);
|
||||
|
||||
update_one_by_id(&db_client().await.servers, &id, mungos::update::Update::Set(doc! { "name": &name, "updated_at": monitor_timestamp() }), None)
|
||||
.await
|
||||
.context("failed to update server on db. this name may already be taken.")?;
|
||||
update.push_simple_log(
|
||||
"rename server",
|
||||
format!("renamed server {id} from {} to {name}", server.name),
|
||||
);
|
||||
update.finalize();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CreateNetwork, User> for State {
|
||||
#[instrument(name = "CreateNetwork", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CreateNetwork { server, name }: CreateNetwork,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let mut update =
|
||||
make_update(&server, Operation::CreateNetwork, &user);
|
||||
update.status = UpdateStatus::InProgress;
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
match periphery
|
||||
.request(api::network::CreateNetwork { name, driver: None })
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update
|
||||
.push_error_log("create network", serialize_error_pretty(&e)),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<DeleteNetwork, User> for State {
|
||||
#[instrument(name = "DeleteNetwork", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeleteNetwork { server, name }: DeleteNetwork,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let server = Server::get_resource_check_permissions(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let mut update =
|
||||
make_update(&server, Operation::DeleteNetwork, &user);
|
||||
update.status = UpdateStatus::InProgress;
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
match periphery
|
||||
.request(api::network::DeleteNetwork { name })
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update
|
||||
.push_error_log("delete network", serialize_error_pretty(&e)),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
249
bin/core/src/api/write/server_template.rs
Normal file
249
bin/core/src/api/write/server_template.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::async_trait;
|
||||
use monitor_client::{
|
||||
api::write::{
|
||||
CopyServerTemplate, CreateServerTemplate, DeleteServerTemplate,
|
||||
UpdateServerTemplate,
|
||||
},
|
||||
entities::{
|
||||
monitor_timestamp, permission::PermissionLevel,
|
||||
server_template::ServerTemplate, user::User, Operation,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::{delete_one_by_id, update_one_by_id},
|
||||
mongodb::bson::{doc, oid::ObjectId, to_document},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
create_permission, remove_from_recently_viewed,
|
||||
resource::{delete_all_permissions_on_resource, StateResource},
|
||||
update::{add_update, make_update},
|
||||
},
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CreateServerTemplate, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
CreateServerTemplate { name, config }: CreateServerTemplate,
|
||||
user: User,
|
||||
) -> anyhow::Result<ServerTemplate> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("only admins can create server templates"));
|
||||
}
|
||||
if ObjectId::from_str(&name).is_ok() {
|
||||
return Err(anyhow!("valid ObjectIds cannot be used as names"));
|
||||
}
|
||||
let server_template = ServerTemplate {
|
||||
id: Default::default(),
|
||||
name,
|
||||
updated_at: monitor_timestamp(),
|
||||
description: Default::default(),
|
||||
tags: Default::default(),
|
||||
config: config.into(),
|
||||
info: (),
|
||||
};
|
||||
let server_template_id = db_client()
|
||||
.await
|
||||
.server_templates
|
||||
.insert_one(server_template, None)
|
||||
.await
|
||||
.context("failed to add server_template to db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
let server_template =
|
||||
ServerTemplate::get_resource(&server_template_id).await?;
|
||||
create_permission(
|
||||
&user,
|
||||
&server_template,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await;
|
||||
let mut update = make_update(
|
||||
&server_template,
|
||||
Operation::CreateServerTemplate,
|
||||
&user,
|
||||
);
|
||||
update.push_simple_log(
|
||||
"create server template",
|
||||
format!(
|
||||
"created server template\nid: {}\nname: {}",
|
||||
server_template.id, server_template.name
|
||||
),
|
||||
);
|
||||
update.push_simple_log(
|
||||
"config",
|
||||
format!("{:#?}", server_template.config),
|
||||
);
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(server_template)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CopyServerTemplate, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
CopyServerTemplate { name, id }: CopyServerTemplate,
|
||||
user: User,
|
||||
) -> anyhow::Result<ServerTemplate> {
|
||||
let ServerTemplate {
|
||||
config,
|
||||
description,
|
||||
..
|
||||
} = ServerTemplate::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
let server_template = ServerTemplate {
|
||||
id: Default::default(),
|
||||
name,
|
||||
updated_at: monitor_timestamp(),
|
||||
description,
|
||||
tags: Default::default(),
|
||||
config,
|
||||
info: (),
|
||||
};
|
||||
let server_template_id = db_client()
|
||||
.await
|
||||
.server_templates
|
||||
.insert_one(server_template, None)
|
||||
.await
|
||||
.context("failed to add server_template to db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
let server_template =
|
||||
ServerTemplate::get_resource(&server_template_id).await?;
|
||||
create_permission(
|
||||
&user,
|
||||
&server_template,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await;
|
||||
let mut update = make_update(
|
||||
&server_template,
|
||||
Operation::CreateServerTemplate,
|
||||
&user,
|
||||
);
|
||||
update.push_simple_log(
|
||||
"create server template",
|
||||
format!(
|
||||
"created server template\nid: {}\nname: {}",
|
||||
server_template.id, server_template.name
|
||||
),
|
||||
);
|
||||
update.push_simple_log(
|
||||
"config",
|
||||
format!("{:#?}", server_template.config),
|
||||
);
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(server_template)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<DeleteServerTemplate, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeleteServerTemplate { id }: DeleteServerTemplate,
|
||||
user: User,
|
||||
) -> anyhow::Result<ServerTemplate> {
|
||||
let server_template =
|
||||
ServerTemplate::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
delete_one_by_id(&db_client().await.server_templates, &id, None)
|
||||
.await
|
||||
.context("failed to delete server templates from database")?;
|
||||
|
||||
delete_all_permissions_on_resource(&server_template).await;
|
||||
|
||||
let mut update = make_update(
|
||||
&server_template,
|
||||
Operation::DeleteServerTemplate,
|
||||
&user,
|
||||
);
|
||||
|
||||
update.push_simple_log(
|
||||
"delete server template",
|
||||
format!("deleted server template {}", server_template.name),
|
||||
);
|
||||
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
|
||||
remove_from_recently_viewed(&server_template).await?;
|
||||
|
||||
Ok(server_template)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<UpdateServerTemplate, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateServerTemplate { id, config }: UpdateServerTemplate,
|
||||
user: User,
|
||||
) -> anyhow::Result<ServerTemplate> {
|
||||
let server_template =
|
||||
ServerTemplate::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut update = make_update(
|
||||
&server_template,
|
||||
Operation::UpdateServerTemplate,
|
||||
&user,
|
||||
);
|
||||
|
||||
update.push_simple_log(
|
||||
"server template update",
|
||||
serde_json::to_string_pretty(&config)
|
||||
.context("failed to serialize config update")?,
|
||||
);
|
||||
|
||||
let config = server_template.config.merge_partial(config);
|
||||
let config = to_document(&config)
|
||||
.context("failed to serialize update to bson document")?;
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.server_templates,
|
||||
&id,
|
||||
mungos::update::Update::FlattenSet(doc! { "config": config }),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let server_template = ServerTemplate::get_resource(&id).await?;
|
||||
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
|
||||
Ok(server_template)
|
||||
}
|
||||
}
|
||||
203
bin/core/src/api/write/tag.rs
Normal file
203
bin/core/src/api/write/tag.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::write::{
|
||||
CreateTag, DeleteTag, RenameTag, UpdateTagsOnResource,
|
||||
UpdateTagsOnResourceResponse,
|
||||
},
|
||||
entities::{
|
||||
alerter::Alerter, build::Build, builder::Builder,
|
||||
deployment::Deployment, permission::PermissionLevel,
|
||||
procedure::Procedure, repo::Repo, server::Server,
|
||||
server_template::ServerTemplate, tag::Tag,
|
||||
update::ResourceTarget, user::User,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::{delete_one_by_id, update_one_by_id},
|
||||
mongodb::bson::{doc, oid::ObjectId},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
query::{get_tag, get_tag_check_owner},
|
||||
resource::StateResource,
|
||||
},
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CreateTag, User> for State {
|
||||
#[instrument(name = "CreateTag", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CreateTag { name }: CreateTag,
|
||||
user: User,
|
||||
) -> anyhow::Result<Tag> {
|
||||
if ObjectId::from_str(&name).is_ok() {
|
||||
return Err(anyhow!("tag name cannot be ObjectId"));
|
||||
}
|
||||
|
||||
let mut tag = Tag {
|
||||
id: Default::default(),
|
||||
name,
|
||||
owner: user.id.clone(),
|
||||
};
|
||||
|
||||
tag.id = db_client()
|
||||
.await
|
||||
.tags
|
||||
.insert_one(&tag, None)
|
||||
.await
|
||||
.context("failed to create tag on db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
|
||||
Ok(tag)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<RenameTag, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
RenameTag { id, name }: RenameTag,
|
||||
user: User,
|
||||
) -> anyhow::Result<Tag> {
|
||||
if ObjectId::from_str(&name).is_ok() {
|
||||
return Err(anyhow!("tag name cannot be ObjectId"));
|
||||
}
|
||||
|
||||
get_tag_check_owner(&id, &user).await?;
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.tags,
|
||||
&id,
|
||||
doc! { "$set": { "name": name } },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to rename tag on db")?;
|
||||
|
||||
get_tag(&id).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<DeleteTag, User> for State {
|
||||
#[instrument(name = "DeleteTag", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeleteTag { id }: DeleteTag,
|
||||
user: User,
|
||||
) -> anyhow::Result<Tag> {
|
||||
let tag = get_tag_check_owner(&id, &user).await?;
|
||||
|
||||
tokio::try_join!(
|
||||
Server::remove_tag_from_resources(&id,),
|
||||
Deployment::remove_tag_from_resources(&id,),
|
||||
Build::remove_tag_from_resources(&id,),
|
||||
Repo::remove_tag_from_resources(&id,),
|
||||
Builder::remove_tag_from_resources(&id,),
|
||||
Alerter::remove_tag_from_resources(&id,),
|
||||
Procedure::remove_tag_from_resources(&id,),
|
||||
)?;
|
||||
|
||||
delete_one_by_id(&db_client().await.tags, &id, None).await?;
|
||||
|
||||
Ok(tag)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<UpdateTagsOnResource, User> for State {
|
||||
#[instrument(name = "UpdateTagsOnResource", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateTagsOnResource { target, tags }: UpdateTagsOnResource,
|
||||
user: User,
|
||||
) -> anyhow::Result<UpdateTagsOnResourceResponse> {
|
||||
match target {
|
||||
ResourceTarget::System(_) => return Err(anyhow!("")),
|
||||
ResourceTarget::Build(id) => {
|
||||
Build::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
Build::update_tags_on_resource(&id, tags, user).await?;
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
Builder::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
Builder::update_tags_on_resource(&id, tags, user).await?
|
||||
}
|
||||
ResourceTarget::Deployment(id) => {
|
||||
Deployment::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
Deployment::update_tags_on_resource(&id, tags, user).await?
|
||||
}
|
||||
ResourceTarget::Server(id) => {
|
||||
Server::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
Server::update_tags_on_resource(&id, tags, user).await?
|
||||
}
|
||||
ResourceTarget::Repo(id) => {
|
||||
Repo::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
Repo::update_tags_on_resource(&id, tags, user).await?
|
||||
}
|
||||
ResourceTarget::Alerter(id) => {
|
||||
Alerter::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
Alerter::update_tags_on_resource(&id, tags, user).await?
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
Procedure::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
Procedure::update_tags_on_resource(&id, tags, user).await?
|
||||
}
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
ServerTemplate::get_resource_check_permissions(
|
||||
&id,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
ServerTemplate::update_tags_on_resource(&id, tags, user)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
Ok(UpdateTagsOnResourceResponse {})
|
||||
}
|
||||
}
|
||||
173
bin/core/src/api/write/user.rs
Normal file
173
bin/core/src/api/write/user.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use std::{collections::VecDeque, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_trait::async_trait;
|
||||
use monitor_client::{
|
||||
api::write::{
|
||||
CreateServiceUser, CreateServiceUserResponse, PushRecentlyViewed,
|
||||
PushRecentlyViewedResponse, SetLastSeenUpdate,
|
||||
SetLastSeenUpdateResponse, UpdateServiceUserDescription,
|
||||
UpdateServiceUserDescriptionResponse,
|
||||
},
|
||||
entities::{
|
||||
monitor_timestamp,
|
||||
user::{User, UserConfig},
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::update_one_by_id,
|
||||
mongodb::bson::{doc, oid::ObjectId, to_bson},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_user,
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
const RECENTLY_VIEWED_MAX: usize = 10;
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<PushRecentlyViewed, User> for State {
|
||||
#[instrument(name = "PushRecentlyViewed", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
PushRecentlyViewed { resource }: PushRecentlyViewed,
|
||||
user: User,
|
||||
) -> anyhow::Result<PushRecentlyViewedResponse> {
|
||||
let mut recently_viewed = get_user(&user.id)
|
||||
.await?
|
||||
.recently_viewed
|
||||
.into_iter()
|
||||
.filter(|r| !resource.eq(r))
|
||||
.take(RECENTLY_VIEWED_MAX - 1)
|
||||
.collect::<VecDeque<_>>();
|
||||
|
||||
recently_viewed.push_front(resource);
|
||||
|
||||
let recently_viewed = to_bson(&recently_viewed)
|
||||
.context("failed to convert recently views to bson")?;
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().await.users,
|
||||
&user.id,
|
||||
mungos::update::Update::Set(doc! {
|
||||
"recently_viewed": recently_viewed
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("context")?;
|
||||
|
||||
Ok(PushRecentlyViewedResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<SetLastSeenUpdate, User> for State {
|
||||
#[instrument(name = "SetLastSeenUpdate", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
SetLastSeenUpdate {}: SetLastSeenUpdate,
|
||||
user: User,
|
||||
) -> anyhow::Result<SetLastSeenUpdateResponse> {
|
||||
update_one_by_id(
|
||||
&db_client().await.users,
|
||||
&user.id,
|
||||
mungos::update::Update::Set(doc! {
|
||||
"last_update_view": monitor_timestamp()
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to update user last_update_view")?;
|
||||
Ok(SetLastSeenUpdateResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CreateServiceUser, User> for State {
|
||||
#[instrument(name = "CreateServiceUser", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CreateServiceUser {
|
||||
username,
|
||||
description,
|
||||
}: CreateServiceUser,
|
||||
user: User,
|
||||
) -> anyhow::Result<CreateServiceUserResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("user not admin"));
|
||||
}
|
||||
if ObjectId::from_str(&username).is_ok() {
|
||||
return Err(anyhow!("username cannot be valid ObjectId"));
|
||||
}
|
||||
let config = UserConfig::Service { description };
|
||||
let mut user = User {
|
||||
id: Default::default(),
|
||||
username,
|
||||
config,
|
||||
enabled: true,
|
||||
admin: false,
|
||||
create_server_permissions: false,
|
||||
create_build_permissions: false,
|
||||
last_update_view: 0,
|
||||
recently_viewed: Vec::new(),
|
||||
updated_at: monitor_timestamp(),
|
||||
};
|
||||
user.id = db_client()
|
||||
.await
|
||||
.users
|
||||
.insert_one(&user, None)
|
||||
.await
|
||||
.context("failed to create service user on db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted id is not object id")?
|
||||
.to_string();
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<UpdateServiceUserDescription, User> for State {
|
||||
#[instrument(
|
||||
name = "UpdateServiceUserDescription",
|
||||
skip(self, user)
|
||||
)]
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateServiceUserDescription {
|
||||
username,
|
||||
description,
|
||||
}: UpdateServiceUserDescription,
|
||||
user: User,
|
||||
) -> anyhow::Result<UpdateServiceUserDescriptionResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("user not admin"));
|
||||
}
|
||||
let db = db_client().await;
|
||||
let service_user = db
|
||||
.users
|
||||
.find_one(doc! { "username": &username }, None)
|
||||
.await
|
||||
.context("failed to query db for user")?
|
||||
.context("no user with given username")?;
|
||||
let UserConfig::Service { .. } = &service_user.config else {
|
||||
return Err(anyhow!("user is not service user"));
|
||||
};
|
||||
db.users
|
||||
.update_one(
|
||||
doc! { "username": &username },
|
||||
doc! { "$set": { "config.data.description": description } },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to update user on db")?;
|
||||
db.users
|
||||
.find_one(doc! { "username": &username }, None)
|
||||
.await
|
||||
.context("failed to query db for user")?
|
||||
.context("user with username not found")
|
||||
}
|
||||
}
|
||||
252
bin/core/src/api/write/user_group.rs
Normal file
252
bin/core/src/api/write/user_group.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::async_trait;
|
||||
use monitor_client::{
|
||||
api::write::{
|
||||
AddUserToUserGroup, CreateUserGroup, DeleteUserGroup,
|
||||
RemoveUserFromUserGroup, RenameUserGroup, SetUsersInUserGroup,
|
||||
},
|
||||
entities::{monitor_timestamp, user::User, user_group::UserGroup},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::{delete_one_by_id, find_one_by_id, update_one_by_id},
|
||||
find::find_collect,
|
||||
mongodb::bson::{doc, oid::ObjectId},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::state::{db_client, State};
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CreateUserGroup, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
CreateUserGroup { name }: CreateUserGroup,
|
||||
admin: User,
|
||||
) -> anyhow::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This call is admin-only"));
|
||||
}
|
||||
let user_group = UserGroup {
|
||||
id: Default::default(),
|
||||
users: Default::default(),
|
||||
updated_at: monitor_timestamp(),
|
||||
name,
|
||||
};
|
||||
let db = db_client().await;
|
||||
let id = db
|
||||
.user_groups
|
||||
.insert_one(user_group, None)
|
||||
.await
|
||||
.context("failed to create UserGroup on db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted id is not ObjectId")?
|
||||
.to_string();
|
||||
find_one_by_id(&db.user_groups, &id)
|
||||
.await
|
||||
.context("failed to query db for user groups")?
|
||||
.context("user group at id not found")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<RenameUserGroup, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
RenameUserGroup { id, name }: RenameUserGroup,
|
||||
admin: User,
|
||||
) -> anyhow::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This call is admin-only"));
|
||||
}
|
||||
let db = db_client().await;
|
||||
update_one_by_id(
|
||||
&db.user_groups,
|
||||
&id,
|
||||
doc! { "$set": { "name": name } },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to rename UserGroup on db")?;
|
||||
find_one_by_id(&db.user_groups, &id)
|
||||
.await
|
||||
.context("failed to query db for UserGroups")?
|
||||
.context("no user group with given id")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<DeleteUserGroup, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeleteUserGroup { id }: DeleteUserGroup,
|
||||
admin: User,
|
||||
) -> anyhow::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This call is admin-only"));
|
||||
}
|
||||
|
||||
let db = db_client().await;
|
||||
|
||||
let ug = find_one_by_id(&db.user_groups, &id)
|
||||
.await
|
||||
.context("failed to query db for UserGroups")?
|
||||
.context("no UserGroup found with given id")?;
|
||||
|
||||
delete_one_by_id(&db.user_groups, &id, None)
|
||||
.await
|
||||
.context("failed to delete UserGroup from db")?;
|
||||
|
||||
db.permissions
|
||||
.delete_many(doc! {
|
||||
"user_target.type": "UserGroup",
|
||||
"user_target.id": id,
|
||||
}, None)
|
||||
.await
|
||||
.context("failed to clean up UserGroups permissions. User Group has been deleted")?;
|
||||
|
||||
Ok(ug)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<AddUserToUserGroup, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
AddUserToUserGroup { user_group, user }: AddUserToUserGroup,
|
||||
admin: User,
|
||||
) -> anyhow::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This call is admin-only"));
|
||||
}
|
||||
|
||||
let db = db_client().await;
|
||||
|
||||
let filter = match ObjectId::from_str(&user) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "username": &user },
|
||||
};
|
||||
let user = db
|
||||
.users
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query mongo for users")?
|
||||
.context("no matching user found")?;
|
||||
|
||||
let filter = match ObjectId::from_str(&user_group) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": &user_group },
|
||||
};
|
||||
db.user_groups
|
||||
.update_one(
|
||||
filter.clone(),
|
||||
doc! { "$addToSet": { "users": &user.id } },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to add user to group on db")?;
|
||||
db.user_groups
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query db for UserGroups")?
|
||||
.context("no user group with given id")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<RemoveUserFromUserGroup, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
RemoveUserFromUserGroup {
|
||||
user_group,
|
||||
user,
|
||||
}: RemoveUserFromUserGroup,
|
||||
admin: User,
|
||||
) -> anyhow::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This call is admin-only"));
|
||||
}
|
||||
|
||||
let db = db_client().await;
|
||||
|
||||
let filter = match ObjectId::from_str(&user) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "username": &user },
|
||||
};
|
||||
let user = db
|
||||
.users
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query mongo for users")?
|
||||
.context("no matching user found")?;
|
||||
|
||||
let filter = match ObjectId::from_str(&user_group) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": &user_group },
|
||||
};
|
||||
db.user_groups
|
||||
.update_one(
|
||||
filter.clone(),
|
||||
doc! { "$pull": { "users": &user.id } },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to add user to group on db")?;
|
||||
db.user_groups
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query db for UserGroups")?
|
||||
.context("no user group with given id")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<SetUsersInUserGroup, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
SetUsersInUserGroup { user_group, users }: SetUsersInUserGroup,
|
||||
admin: User,
|
||||
) -> anyhow::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This call is admin-only"));
|
||||
}
|
||||
|
||||
let db = db_client().await;
|
||||
|
||||
let all_users = find_collect(&db.users, None, None)
|
||||
.await
|
||||
.context("failed to query db for users")?
|
||||
.into_iter()
|
||||
.map(|u| (u.username, u.id))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
// Make sure all users are user ids
|
||||
let users = users
|
||||
.into_iter()
|
||||
.filter_map(|user| match ObjectId::from_str(&user) {
|
||||
Ok(_) => Some(user),
|
||||
Err(_) => all_users.get(&user).cloned(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let filter = match ObjectId::from_str(&user_group) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": &user_group },
|
||||
};
|
||||
db.user_groups
|
||||
.update_one(
|
||||
filter.clone(),
|
||||
doc! { "$set": { "users": users } },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to add user to group on db")?;
|
||||
db.user_groups
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query db for UserGroups")?
|
||||
.context("no user group with given id")
|
||||
}
|
||||
}
|
||||
229
bin/core/src/auth/github/client.rs
Normal file
229
bin/core/src/auth/github/client.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use monitor_client::entities::config::core::{
|
||||
CoreConfig, OauthCredentials,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
auth::{random_string, STATE_PREFIX_LENGTH},
|
||||
config::core_config,
|
||||
};
|
||||
|
||||
pub fn github_oauth_client() -> &'static Option<GithubOauthClient> {
|
||||
static GITHUB_OAUTH_CLIENT: OnceLock<Option<GithubOauthClient>> =
|
||||
OnceLock::new();
|
||||
GITHUB_OAUTH_CLIENT
|
||||
.get_or_init(|| GithubOauthClient::new(core_config()))
|
||||
}
|
||||
|
||||
pub struct GithubOauthClient {
|
||||
http: reqwest::Client,
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
redirect_uri: String,
|
||||
scopes: String,
|
||||
states: Mutex<Vec<String>>,
|
||||
user_agent: String,
|
||||
}
|
||||
|
||||
impl GithubOauthClient {
|
||||
pub fn new(
|
||||
CoreConfig {
|
||||
github_oauth:
|
||||
OauthCredentials {
|
||||
enabled,
|
||||
id,
|
||||
secret,
|
||||
},
|
||||
host,
|
||||
..
|
||||
}: &CoreConfig,
|
||||
) -> Option<GithubOauthClient> {
|
||||
if !enabled {
|
||||
return None;
|
||||
}
|
||||
if host.is_empty() {
|
||||
warn!("github oauth is enabled, but 'config.host' is not configured");
|
||||
return None;
|
||||
}
|
||||
if id.is_empty() {
|
||||
warn!("github oauth is enabled, but 'config.github_oauth.id' is not configured");
|
||||
return None;
|
||||
}
|
||||
if secret.is_empty() {
|
||||
warn!("github oauth is enabled, but 'config.github_oauth.secret' is not configured");
|
||||
return None;
|
||||
}
|
||||
GithubOauthClient {
|
||||
http: reqwest::Client::new(),
|
||||
client_id: id.clone(),
|
||||
client_secret: secret.clone(),
|
||||
redirect_uri: format!("{host}/auth/github/callback"),
|
||||
user_agent: Default::default(),
|
||||
scopes: Default::default(),
|
||||
states: Default::default(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn get_login_redirect_url(
|
||||
&self,
|
||||
redirect: Option<String>,
|
||||
) -> String {
|
||||
let state_prefix = random_string(STATE_PREFIX_LENGTH);
|
||||
let state = match redirect {
|
||||
Some(redirect) => format!("{state_prefix}{redirect}"),
|
||||
None => state_prefix,
|
||||
};
|
||||
let redirect_url = format!(
|
||||
"https://github.com/login/oauth/authorize?state={state}&client_id={}&redirect_uri={}&scope={}",
|
||||
self.client_id, self.redirect_uri, self.scopes
|
||||
);
|
||||
let mut states = self.states.lock().await;
|
||||
states.push(state);
|
||||
redirect_url
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn check_state(&self, state: &str) -> bool {
|
||||
let mut contained = false;
|
||||
self.states.lock().await.retain(|s| {
|
||||
if s.as_str() == state {
|
||||
contained = true;
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
contained
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn get_access_token(
|
||||
&self,
|
||||
code: &str,
|
||||
) -> anyhow::Result<AccessTokenResponse> {
|
||||
self
|
||||
.post::<(), _>(
|
||||
"https://github.com/login/oauth/access_token",
|
||||
&[
|
||||
("client_id", self.client_id.as_str()),
|
||||
("client_secret", self.client_secret.as_str()),
|
||||
("redirect_uri", self.redirect_uri.as_str()),
|
||||
("code", code),
|
||||
],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to get github access token using code")
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn get_github_user(
|
||||
&self,
|
||||
token: &str,
|
||||
) -> anyhow::Result<GithubUserResponse> {
|
||||
self
|
||||
.get("https://api.github.com/user", &[], Some(token))
|
||||
.await
|
||||
.context("failed to get github user using access token")
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get<R: DeserializeOwned>(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
query: &[(&str, &str)],
|
||||
bearer_token: Option<&str>,
|
||||
) -> anyhow::Result<R> {
|
||||
let mut req = self
|
||||
.http
|
||||
.get(endpoint)
|
||||
.query(query)
|
||||
.header("User-Agent", &self.user_agent);
|
||||
|
||||
if let Some(bearer_token) = bearer_token {
|
||||
req =
|
||||
req.header("Authorization", format!("Bearer {bearer_token}"));
|
||||
}
|
||||
|
||||
let res = req.send().await.context("failed to reach github")?;
|
||||
|
||||
let status = res.status();
|
||||
|
||||
if status == StatusCode::OK {
|
||||
let body = res
|
||||
.json()
|
||||
.await
|
||||
.context("failed to parse body into expected type")?;
|
||||
Ok(body)
|
||||
} else {
|
||||
let text = res.text().await.context(format!(
|
||||
"status: {status} | failed to get response text"
|
||||
))?;
|
||||
Err(anyhow!("status: {status} | text: {text}"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn post<B: Serialize, R: DeserializeOwned>(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
query: &[(&str, &str)],
|
||||
body: Option<&B>,
|
||||
bearer_token: Option<&str>,
|
||||
) -> anyhow::Result<R> {
|
||||
let mut req = self
|
||||
.http
|
||||
.post(endpoint)
|
||||
.query(query)
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", &self.user_agent);
|
||||
|
||||
if let Some(body) = body {
|
||||
req = req.json(body);
|
||||
}
|
||||
|
||||
if let Some(bearer_token) = bearer_token {
|
||||
req =
|
||||
req.header("Authorization", format!("Bearer {bearer_token}"));
|
||||
}
|
||||
|
||||
let res = req.send().await.context("failed to reach github")?;
|
||||
|
||||
let status = res.status();
|
||||
|
||||
if status == StatusCode::OK {
|
||||
let body = res
|
||||
.json()
|
||||
.await
|
||||
.context("failed to parse POST body into expected type")?;
|
||||
Ok(body)
|
||||
} else {
|
||||
let text = res.text().await.with_context(|| format!(
|
||||
"method: POST | status: {status} | failed to get response text"
|
||||
))?;
|
||||
Err(anyhow!("method: POST | status: {status} | text: {text}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AccessTokenResponse {
|
||||
pub access_token: String,
|
||||
pub scope: String,
|
||||
pub token_type: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct GithubUserResponse {
|
||||
pub login: String,
|
||||
pub id: u128,
|
||||
pub avatar_url: String,
|
||||
pub email: Option<String>,
|
||||
}
|
||||
118
bin/core/src/auth/github/mod.rs
Normal file
118
bin/core/src/auth/github/mod.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{
|
||||
extract::Query, response::Redirect, routing::get, Router,
|
||||
};
|
||||
use monitor_client::entities::{
|
||||
monitor_timestamp,
|
||||
user::{User, UserConfig},
|
||||
};
|
||||
use mungos::mongodb::bson::doc;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use serror::AddStatusCode;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
state::{db_client, jwt_client},
|
||||
};
|
||||
|
||||
use self::client::github_oauth_client;
|
||||
|
||||
use super::{RedirectQuery, STATE_PREFIX_LENGTH};
|
||||
|
||||
pub mod client;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/login",
|
||||
get(|Query(query): Query<RedirectQuery>| async {
|
||||
Redirect::to(
|
||||
&github_oauth_client()
|
||||
.as_ref()
|
||||
// OK: the router is only mounted in case that the client is populated
|
||||
.unwrap()
|
||||
.get_login_redirect_url(query.redirect)
|
||||
.await,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/callback",
|
||||
get(|query| async {
|
||||
callback(query).await.status_code(StatusCode::UNAUTHORIZED)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CallbackQuery {
|
||||
state: String,
|
||||
code: String,
|
||||
}
|
||||
|
||||
#[instrument(name = "GithubCallback", level = "debug")]
|
||||
async fn callback(
|
||||
Query(query): Query<CallbackQuery>,
|
||||
) -> anyhow::Result<Redirect> {
|
||||
let client = github_oauth_client().as_ref().unwrap();
|
||||
if !client.check_state(&query.state).await {
|
||||
return Err(anyhow!("state mismatch"));
|
||||
}
|
||||
let token = client.get_access_token(&query.code).await?;
|
||||
let github_user =
|
||||
client.get_github_user(&token.access_token).await?;
|
||||
let github_id = github_user.id.to_string();
|
||||
let db_client = db_client().await;
|
||||
let user = db_client
|
||||
.users
|
||||
.find_one(doc! { "config.data.github_id": &github_id }, None)
|
||||
.await
|
||||
.context("failed at find user query from mongo")?;
|
||||
let jwt = match user {
|
||||
Some(user) => jwt_client()
|
||||
.generate(user.id)
|
||||
.context("failed to generate jwt")?,
|
||||
None => {
|
||||
let ts = monitor_timestamp();
|
||||
let no_users_exist =
|
||||
db_client.users.find_one(None, None).await?.is_none();
|
||||
let user = User {
|
||||
id: Default::default(),
|
||||
username: github_user.login,
|
||||
enabled: no_users_exist,
|
||||
admin: no_users_exist,
|
||||
create_server_permissions: no_users_exist,
|
||||
create_build_permissions: no_users_exist,
|
||||
updated_at: ts,
|
||||
last_update_view: 0,
|
||||
recently_viewed: Vec::new(),
|
||||
config: UserConfig::Github {
|
||||
github_id,
|
||||
avatar: github_user.avatar_url,
|
||||
},
|
||||
};
|
||||
let user_id = db_client
|
||||
.users
|
||||
.insert_one(user, None)
|
||||
.await
|
||||
.context("failed to create user on mongo")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
jwt_client()
|
||||
.generate(user_id)
|
||||
.context("failed to generate jwt")?
|
||||
}
|
||||
};
|
||||
let exchange_token = jwt_client().create_exchange_token(jwt).await;
|
||||
let redirect = &query.state[STATE_PREFIX_LENGTH..];
|
||||
let redirect_url = if redirect.is_empty() {
|
||||
format!("{}?token={exchange_token}", core_config().host)
|
||||
} else {
|
||||
let splitter = if redirect.contains('?') { '&' } else { '?' };
|
||||
format!("{}{splitter}token={exchange_token}", redirect)
|
||||
};
|
||||
Ok(Redirect::to(&redirect_url))
|
||||
}
|
||||
198
bin/core/src/auth/google/client.rs
Normal file
198
bin/core/src/auth/google/client.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use jwt::Token;
|
||||
use monitor_client::entities::config::core::{CoreConfig, OauthCredentials};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
auth::{random_string, STATE_PREFIX_LENGTH},
|
||||
config::core_config,
|
||||
};
|
||||
|
||||
pub fn google_oauth_client() -> &'static Option<GoogleOauthClient> {
|
||||
static GOOGLE_OAUTH_CLIENT: OnceLock<Option<GoogleOauthClient>> =
|
||||
OnceLock::new();
|
||||
GOOGLE_OAUTH_CLIENT
|
||||
.get_or_init(|| GoogleOauthClient::new(core_config()))
|
||||
}
|
||||
|
||||
pub struct GoogleOauthClient {
|
||||
http: reqwest::Client,
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
redirect_uri: String,
|
||||
scopes: String,
|
||||
states: Mutex<Vec<String>>,
|
||||
user_agent: String,
|
||||
}
|
||||
|
||||
impl GoogleOauthClient {
|
||||
pub fn new(
|
||||
CoreConfig {
|
||||
google_oauth:
|
||||
OauthCredentials {
|
||||
enabled,
|
||||
id,
|
||||
secret,
|
||||
},
|
||||
host,
|
||||
..
|
||||
}: &CoreConfig,
|
||||
) -> Option<GoogleOauthClient> {
|
||||
if !enabled {
|
||||
return None;
|
||||
}
|
||||
if host.is_empty() {
|
||||
warn!("google oauth is enabled, but 'config.host' is not configured");
|
||||
return None;
|
||||
}
|
||||
if id.is_empty() {
|
||||
warn!("google oauth is enabled, but 'config.google_oauth.id' is not configured");
|
||||
return None;
|
||||
}
|
||||
if secret.is_empty() {
|
||||
warn!("google oauth is enabled, but 'config.google_oauth.secret' is not configured");
|
||||
return None;
|
||||
}
|
||||
let scopes = urlencoding::encode(
|
||||
&[
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
]
|
||||
.join(" "),
|
||||
)
|
||||
.to_string();
|
||||
GoogleOauthClient {
|
||||
http: Default::default(),
|
||||
client_id: id.clone(),
|
||||
client_secret: secret.clone(),
|
||||
redirect_uri: format!("{host}/auth/google/callback"),
|
||||
user_agent: String::from("monitor"),
|
||||
states: Default::default(),
|
||||
scopes,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn get_login_redirect_url(
|
||||
&self,
|
||||
redirect: Option<String>,
|
||||
) -> String {
|
||||
let state_prefix = random_string(STATE_PREFIX_LENGTH);
|
||||
let state = match redirect {
|
||||
Some(redirect) => format!("{state_prefix}{redirect}"),
|
||||
None => state_prefix,
|
||||
};
|
||||
let redirect_url = format!(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth?response_type=code&state={state}&client_id={}&redirect_uri={}&scope={}",
|
||||
self.client_id, self.redirect_uri, self.scopes
|
||||
);
|
||||
let mut states = self.states.lock().await;
|
||||
states.push(state);
|
||||
redirect_url
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn check_state(&self, state: &str) -> bool {
|
||||
let mut contained = false;
|
||||
self.states.lock().await.retain(|s| {
|
||||
if s.as_str() == state {
|
||||
contained = true;
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
contained
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn get_access_token(
|
||||
&self,
|
||||
code: &str,
|
||||
) -> anyhow::Result<AccessTokenResponse> {
|
||||
self
|
||||
.post::<_>(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
&[
|
||||
("client_id", self.client_id.as_str()),
|
||||
("client_secret", self.client_secret.as_str()),
|
||||
("redirect_uri", self.redirect_uri.as_str()),
|
||||
("code", code),
|
||||
("grant_type", "authorization_code"),
|
||||
],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to get google access token using code")
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub fn get_google_user(
|
||||
&self,
|
||||
id_token: &str,
|
||||
) -> anyhow::Result<GoogleUser> {
|
||||
let t: Token<Value, GoogleUser, jwt::Unverified> =
|
||||
Token::parse_unverified(id_token)
|
||||
.context("failed to parse id_token")?;
|
||||
Ok(t.claims().to_owned())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn post<R: DeserializeOwned>(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
body: &[(&str, &str)],
|
||||
bearer_token: Option<&str>,
|
||||
) -> anyhow::Result<R> {
|
||||
let mut req = self
|
||||
.http
|
||||
.post(endpoint)
|
||||
.form(body)
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", &self.user_agent);
|
||||
|
||||
if let Some(bearer_token) = bearer_token {
|
||||
req =
|
||||
req.header("Authorization", format!("Bearer {bearer_token}"));
|
||||
}
|
||||
|
||||
let res = req.send().await.context("failed to reach google")?;
|
||||
|
||||
let status = res.status();
|
||||
|
||||
if status == StatusCode::OK {
|
||||
let body = res
|
||||
.json()
|
||||
.await
|
||||
.context("failed to parse POST body into expected type")?;
|
||||
Ok(body)
|
||||
} else {
|
||||
let text = res.text().await.context(format!(
|
||||
"method: POST | status: {status} | failed to get response text"
|
||||
))?;
|
||||
Err(anyhow!("method: POST | status: {status} | text: {text}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AccessTokenResponse {
|
||||
pub access_token: String,
|
||||
pub id_token: String,
|
||||
pub scope: String,
|
||||
pub token_type: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct GoogleUser {
|
||||
#[serde(rename = "sub")]
|
||||
pub id: String,
|
||||
pub email: String,
|
||||
pub picture: String,
|
||||
}
|
||||
133
bin/core/src/auth/google/mod.rs
Normal file
133
bin/core/src/auth/google/mod.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use axum::{
|
||||
extract::Query, response::Redirect, routing::get, Router,
|
||||
};
|
||||
use monitor_client::entities::user::{User, UserConfig};
|
||||
use mungos::mongodb::bson::doc;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use serror::AddStatusCode;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
state::{db_client, jwt_client},
|
||||
};
|
||||
|
||||
use self::client::google_oauth_client;
|
||||
|
||||
use super::{RedirectQuery, STATE_PREFIX_LENGTH};
|
||||
|
||||
pub mod client;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/login",
|
||||
get(|Query(query): Query<RedirectQuery>| async move {
|
||||
Redirect::to(
|
||||
&google_oauth_client()
|
||||
.as_ref()
|
||||
// OK: its not mounted unless the client is populated
|
||||
.unwrap()
|
||||
.get_login_redirect_url(query.redirect)
|
||||
.await,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/callback",
|
||||
get(|query| async {
|
||||
callback(query).await.status_code(StatusCode::UNAUTHORIZED)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CallbackQuery {
|
||||
state: Option<String>,
|
||||
code: Option<String>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[instrument(name = "GoogleCallback", level = "debug")]
|
||||
async fn callback(
|
||||
Query(query): Query<CallbackQuery>,
|
||||
) -> anyhow::Result<Redirect> {
|
||||
// Safe: the method is only called after the client is_some
|
||||
let client = google_oauth_client().as_ref().unwrap();
|
||||
if let Some(error) = query.error {
|
||||
return Err(anyhow!("auth error from google: {error}"));
|
||||
}
|
||||
let state = query
|
||||
.state
|
||||
.context("callback query does not contain state")?;
|
||||
if !client.check_state(&state).await {
|
||||
return Err(anyhow!("state mismatch"));
|
||||
}
|
||||
let token = client
|
||||
.get_access_token(
|
||||
&query.code.context("callback query does not contain code")?,
|
||||
)
|
||||
.await?;
|
||||
let google_user = client.get_google_user(&token.id_token)?;
|
||||
let google_id = google_user.id.to_string();
|
||||
let db_client = db_client().await;
|
||||
let user = db_client
|
||||
.users
|
||||
.find_one(doc! { "config.data.google_id": &google_id }, None)
|
||||
.await
|
||||
.context("failed at find user query from mongo")?;
|
||||
let jwt = match user {
|
||||
Some(user) => jwt_client()
|
||||
.generate(user.id)
|
||||
.context("failed to generate jwt")?,
|
||||
None => {
|
||||
let ts = unix_timestamp_ms() as i64;
|
||||
let no_users_exist =
|
||||
db_client.users.find_one(None, None).await?.is_none();
|
||||
let user = User {
|
||||
id: Default::default(),
|
||||
username: google_user
|
||||
.email
|
||||
.split('@')
|
||||
.collect::<Vec<&str>>()
|
||||
.first()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
enabled: no_users_exist,
|
||||
admin: no_users_exist,
|
||||
create_server_permissions: no_users_exist,
|
||||
create_build_permissions: no_users_exist,
|
||||
updated_at: ts,
|
||||
last_update_view: 0,
|
||||
recently_viewed: Vec::new(),
|
||||
config: UserConfig::Google {
|
||||
google_id,
|
||||
avatar: google_user.picture,
|
||||
},
|
||||
};
|
||||
let user_id = db_client
|
||||
.users
|
||||
.insert_one(user, None)
|
||||
.await
|
||||
.context("failed to create user on mongo")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
jwt_client()
|
||||
.generate(user_id)
|
||||
.context("failed to generate jwt")?
|
||||
}
|
||||
};
|
||||
let exchange_token = jwt_client().create_exchange_token(jwt).await;
|
||||
let redirect = &state[STATE_PREFIX_LENGTH..];
|
||||
let redirect_url = if redirect.is_empty() {
|
||||
format!("{}?token={exchange_token}", core_config().host)
|
||||
} else {
|
||||
let splitter = if redirect.contains('?') { '&' } else { '?' };
|
||||
format!("{}{splitter}token={exchange_token}", redirect)
|
||||
};
|
||||
Ok(Redirect::to(&redirect_url))
|
||||
}
|
||||
89
bin/core/src/auth/jwt.rs
Normal file
89
bin/core/src/auth/jwt.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_timing_util::{
|
||||
get_timelength_in_ms, unix_timestamp_ms, Timelength,
|
||||
};
|
||||
use hmac::{Hmac, Mac};
|
||||
use jwt::SignWithKey;
|
||||
use monitor_client::entities::config::core::CoreConfig;
|
||||
use mungos::mongodb::bson::doc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::random_string;
|
||||
|
||||
type ExchangeTokenMap = Mutex<HashMap<String, (String, u128)>>;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct JwtClaims {
|
||||
pub id: String,
|
||||
pub iat: u128,
|
||||
pub exp: u128,
|
||||
}
|
||||
|
||||
pub struct JwtClient {
|
||||
pub key: Hmac<Sha256>,
|
||||
valid_for_ms: u128,
|
||||
exchange_tokens: ExchangeTokenMap,
|
||||
}
|
||||
|
||||
impl JwtClient {
|
||||
pub fn new(config: &CoreConfig) -> JwtClient {
|
||||
let key = Hmac::new_from_slice(random_string(40).as_bytes())
|
||||
.expect("failed at taking HmacSha256 of jwt secret");
|
||||
JwtClient {
|
||||
key,
|
||||
valid_for_ms: get_timelength_in_ms(
|
||||
config.jwt_valid_for.to_string().parse().unwrap(),
|
||||
),
|
||||
exchange_tokens: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate(&self, user_id: String) -> anyhow::Result<String> {
|
||||
let iat = unix_timestamp_ms();
|
||||
let exp = iat + self.valid_for_ms;
|
||||
let claims = JwtClaims {
|
||||
id: user_id,
|
||||
iat,
|
||||
exp,
|
||||
};
|
||||
let jwt = claims
|
||||
.sign_with_key(&self.key)
|
||||
.context("failed at signing claim")?;
|
||||
Ok(jwt)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub async fn create_exchange_token(&self, jwt: String) -> String {
|
||||
let exchange_token = random_string(40);
|
||||
self.exchange_tokens.lock().await.insert(
|
||||
exchange_token.clone(),
|
||||
(
|
||||
jwt,
|
||||
unix_timestamp_ms()
|
||||
+ get_timelength_in_ms(Timelength::OneMinute),
|
||||
),
|
||||
);
|
||||
exchange_token
|
||||
}
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn redeem_exchange_token(
|
||||
&self,
|
||||
exchange_token: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let (jwt, valid_until) = self
|
||||
.exchange_tokens
|
||||
.lock()
|
||||
.await
|
||||
.remove(exchange_token)
|
||||
.context("invalid exchange token: unrecognized")?;
|
||||
if unix_timestamp_ms() < valid_until {
|
||||
Ok(jwt)
|
||||
} else {
|
||||
Err(anyhow!("invalid exchange token: expired"))
|
||||
}
|
||||
}
|
||||
}
|
||||
133
bin/core/src/auth/local.rs
Normal file
133
bin/core/src/auth/local.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use async_trait::async_trait;
|
||||
use axum::http::HeaderMap;
|
||||
use monitor_client::{
|
||||
api::auth::{
|
||||
CreateLocalUser, CreateLocalUserResponse, LoginLocalUser,
|
||||
LoginLocalUserResponse,
|
||||
},
|
||||
entities::user::{User, UserConfig},
|
||||
};
|
||||
use mungos::mongodb::bson::{doc, oid::ObjectId};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
state::State,
|
||||
state::{db_client, jwt_client},
|
||||
};
|
||||
|
||||
const BCRYPT_COST: u32 = 10;
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<CreateLocalUser, HeaderMap> for State {
|
||||
#[instrument(name = "CreateLocalUser", skip(self))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CreateLocalUser { username, password }: CreateLocalUser,
|
||||
_: HeaderMap,
|
||||
) -> anyhow::Result<CreateLocalUserResponse> {
|
||||
if !core_config().local_auth {
|
||||
return Err(anyhow!("local auth is not enabled"));
|
||||
}
|
||||
|
||||
if username.is_empty() {
|
||||
return Err(anyhow!("username cannot be empty string"));
|
||||
}
|
||||
|
||||
if ObjectId::from_str(&username).is_ok() {
|
||||
return Err(anyhow!("username cannot be valid ObjectId"));
|
||||
}
|
||||
|
||||
let password = bcrypt::hash(password, BCRYPT_COST)
|
||||
.context("failed to hash password")?;
|
||||
|
||||
let no_users_exist = db_client()
|
||||
.await
|
||||
.users
|
||||
.find_one(None, None)
|
||||
.await?
|
||||
.is_none();
|
||||
|
||||
let ts = unix_timestamp_ms() as i64;
|
||||
|
||||
let user = User {
|
||||
id: Default::default(),
|
||||
username,
|
||||
enabled: no_users_exist,
|
||||
admin: no_users_exist,
|
||||
create_server_permissions: no_users_exist,
|
||||
create_build_permissions: no_users_exist,
|
||||
updated_at: ts,
|
||||
last_update_view: 0,
|
||||
recently_viewed: Vec::new(),
|
||||
config: UserConfig::Local { password },
|
||||
};
|
||||
|
||||
let user_id = db_client()
|
||||
.await
|
||||
.users
|
||||
.insert_one(user, None)
|
||||
.await
|
||||
.context("failed to create user")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
|
||||
let jwt = jwt_client()
|
||||
.generate(user_id)
|
||||
.context("failed to generate jwt for user")?;
|
||||
|
||||
Ok(CreateLocalUserResponse { jwt })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolve<LoginLocalUser, HeaderMap> for State {
|
||||
#[instrument(name = "LoginLocalUser", level = "debug", skip(self))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
LoginLocalUser { username, password }: LoginLocalUser,
|
||||
_: HeaderMap,
|
||||
) -> anyhow::Result<LoginLocalUserResponse> {
|
||||
if !core_config().local_auth {
|
||||
return Err(anyhow!("local auth is not enabled"));
|
||||
}
|
||||
|
||||
let user = db_client()
|
||||
.await
|
||||
.users
|
||||
.find_one(doc! { "username": &username }, None)
|
||||
.await
|
||||
.context("failed at db query for users")?
|
||||
.with_context(|| {
|
||||
format!("did not find user with username {username}")
|
||||
})?;
|
||||
|
||||
let UserConfig::Local {
|
||||
password: user_pw_hash,
|
||||
} = user.config
|
||||
else {
|
||||
return Err(anyhow!(
|
||||
"non-local auth users can not log in with a password"
|
||||
));
|
||||
};
|
||||
|
||||
let verified = bcrypt::verify(password, &user_pw_hash)
|
||||
.context("failed at verify password")?;
|
||||
|
||||
if !verified {
|
||||
return Err(anyhow!("invalid credentials"));
|
||||
}
|
||||
|
||||
let jwt = jwt_client()
|
||||
.generate(user.id)
|
||||
.context("failed at generating jwt for user")?;
|
||||
|
||||
Ok(LoginLocalUserResponse { jwt })
|
||||
}
|
||||
}
|
||||
161
bin/core/src/auth/mod.rs
Normal file
161
bin/core/src/auth/mod.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use ::jwt::VerifyWithKey;
|
||||
use anyhow::{anyhow, Context};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use axum::{
|
||||
extract::Request, http::HeaderMap, middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use monitor_client::entities::{monitor_timestamp, user::User};
|
||||
use mungos::mongodb::bson::doc;
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_user,
|
||||
state::{db_client, jwt_client},
|
||||
};
|
||||
|
||||
use self::jwt::JwtClaims;
|
||||
|
||||
pub mod github;
|
||||
pub mod google;
|
||||
pub mod jwt;
|
||||
|
||||
mod local;
|
||||
|
||||
const STATE_PREFIX_LENGTH: usize = 20;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RedirectQuery {
|
||||
pub redirect: Option<String>,
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn auth_request(
|
||||
headers: HeaderMap,
|
||||
mut req: Request,
|
||||
next: Next,
|
||||
) -> serror::Result<Response> {
|
||||
let user = authenticate_check_enabled(&headers).await?;
|
||||
req.extensions_mut().insert(user);
|
||||
Ok(next.run(req).await)
|
||||
}
|
||||
|
||||
pub fn random_string(length: usize) -> String {
|
||||
thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(length)
|
||||
.map(char::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_user_id_from_headers(
|
||||
headers: &HeaderMap,
|
||||
) -> anyhow::Result<String> {
|
||||
match (
|
||||
headers.get("authorization"),
|
||||
headers.get("x-api-key"),
|
||||
headers.get("x-api-secret"),
|
||||
) {
|
||||
(Some(jwt), _, _) => {
|
||||
// USE JWT
|
||||
let jwt = jwt.to_str().context("jwt is not str")?;
|
||||
auth_jwt_get_user_id(jwt)
|
||||
.await
|
||||
.context("failed to authenticate jwt")
|
||||
}
|
||||
(None, Some(key), Some(secret)) => {
|
||||
// USE API KEY / SECRET
|
||||
let key = key.to_str().context("key is not str")?;
|
||||
let secret = secret.to_str().context("secret is not str")?;
|
||||
auth_api_key_get_user_id(key, secret)
|
||||
.await
|
||||
.context("failed to authenticate api key")
|
||||
}
|
||||
_ => {
|
||||
// AUTH FAIL
|
||||
Err(anyhow!("must attach either AUTHORIZATION header with jwt OR pass X-API-KEY and X-API-SECRET"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn authenticate_check_enabled(
|
||||
headers: &HeaderMap,
|
||||
) -> anyhow::Result<User> {
|
||||
let user_id = get_user_id_from_headers(headers).await?;
|
||||
let user = get_user(&user_id).await?;
|
||||
if user.enabled {
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(anyhow!("user not enabled"))
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn auth_jwt_get_user_id(
|
||||
jwt: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let claims: JwtClaims = jwt
|
||||
.verify_with_key(&jwt_client().key)
|
||||
.context("failed to verify claims")?;
|
||||
if claims.exp > unix_timestamp_ms() {
|
||||
Ok(claims.id)
|
||||
} else {
|
||||
Err(anyhow!("token has expired"))
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn auth_jwt_check_enabled(
|
||||
jwt: &str,
|
||||
) -> anyhow::Result<User> {
|
||||
let user_id = auth_jwt_get_user_id(jwt).await?;
|
||||
check_enabled(user_id).await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn auth_api_key_get_user_id(
|
||||
key: &str,
|
||||
secret: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let key = db_client()
|
||||
.await
|
||||
.api_keys
|
||||
.find_one(doc! { "key": key }, None)
|
||||
.await
|
||||
.context("failed to query db")?
|
||||
.context("no api key matching key")?;
|
||||
if key.expires != 0 && key.expires < monitor_timestamp() {
|
||||
return Err(anyhow!("api key expired"));
|
||||
}
|
||||
if bcrypt::verify(secret, &key.secret)
|
||||
.context("failed to verify secret hash")?
|
||||
{
|
||||
// secret matches
|
||||
Ok(key.user_id)
|
||||
} else {
|
||||
// secret mismatch
|
||||
Err(anyhow!("invalid api secret"))
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn auth_api_key_check_enabled(
|
||||
key: &str,
|
||||
secret: &str,
|
||||
) -> anyhow::Result<User> {
|
||||
let user_id = auth_api_key_get_user_id(key, secret).await?;
|
||||
check_enabled(user_id).await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn check_enabled(user_id: String) -> anyhow::Result<User> {
|
||||
let user = get_user(&user_id).await?;
|
||||
if user.enabled {
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(anyhow!("user not enabled"))
|
||||
}
|
||||
}
|
||||
1056
bin/core/src/cloud/aws.rs
Normal file
1056
bin/core/src/cloud/aws.rs
Normal file
File diff suppressed because it is too large
Load Diff
7
bin/core/src/cloud/mod.rs
Normal file
7
bin/core/src/cloud/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod aws;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BuildCleanupData {
|
||||
Server { repo_name: String },
|
||||
Aws { instance_id: String, region: String },
|
||||
}
|
||||
124
bin/core/src/config.rs
Normal file
124
bin/core/src/config.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::Context;
|
||||
use merge_config_files::parse_config_file;
|
||||
use monitor_client::entities::config::core::{CoreConfig, Env};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub fn frontend_path() -> &'static String {
|
||||
#[derive(Deserialize)]
|
||||
struct FrontendEnv {
|
||||
#[serde(default = "default_frontend_path")]
|
||||
monitor_frontend_path: String,
|
||||
}
|
||||
|
||||
fn default_frontend_path() -> String {
|
||||
"/frontend".to_string()
|
||||
}
|
||||
|
||||
static FRONTEND_PATH: OnceLock<String> = OnceLock::new();
|
||||
FRONTEND_PATH.get_or_init(|| {
|
||||
let FrontendEnv {
|
||||
monitor_frontend_path,
|
||||
} = envy::from_env()
|
||||
.context("failed to parse FrontendEnv")
|
||||
.unwrap();
|
||||
monitor_frontend_path
|
||||
})
|
||||
}
|
||||
|
||||
pub fn core_config() -> &'static CoreConfig {
|
||||
static CORE_CONFIG: OnceLock<CoreConfig> = OnceLock::new();
|
||||
CORE_CONFIG.get_or_init(|| {
|
||||
let env: Env = envy::from_env()
|
||||
.context("failed to parse core Env")
|
||||
.unwrap();
|
||||
let config_path = &env.monitor_config_path;
|
||||
let mut config =
|
||||
parse_config_file::<CoreConfig>(config_path.as_str())
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("failed at parsing config at {config_path} | {e:#}")
|
||||
});
|
||||
|
||||
// Overrides
|
||||
config.title = env.monitor_title.unwrap_or(config.title);
|
||||
config.host = env.monitor_host.unwrap_or(config.host);
|
||||
config.port = env.monitor_port.unwrap_or(config.port);
|
||||
config.passkey = env.monitor_passkey.unwrap_or(config.passkey);
|
||||
config.jwt_valid_for =
|
||||
env.monitor_jwt_valid_for.unwrap_or(config.jwt_valid_for);
|
||||
config.monitoring_interval = env
|
||||
.monitor_monitoring_interval
|
||||
.unwrap_or(config.monitoring_interval);
|
||||
config.keep_stats_for_days = env
|
||||
.monitor_keep_stats_for_days
|
||||
.unwrap_or(config.keep_stats_for_days);
|
||||
config.keep_alerts_for_days = env
|
||||
.monitor_keep_alerts_for_days
|
||||
.unwrap_or(config.keep_alerts_for_days);
|
||||
config.github_webhook_secret = env
|
||||
.monitor_github_webhook_secret
|
||||
.unwrap_or(config.github_webhook_secret);
|
||||
config.github_webhook_base_url = env
|
||||
.monitor_github_webhook_base_url
|
||||
.or(config.github_webhook_base_url);
|
||||
config.docker_organizations = env
|
||||
.monitor_docker_organizations
|
||||
.unwrap_or(config.docker_organizations);
|
||||
|
||||
config.logging.level =
|
||||
env.monitor_logging_level.unwrap_or(config.logging.level);
|
||||
config.logging.stdio =
|
||||
env.monitor_logging_stdio.unwrap_or(config.logging.stdio);
|
||||
config.logging.otlp_endpoint = env
|
||||
.monitor_logging_otlp_endpoint
|
||||
.or(config.logging.otlp_endpoint);
|
||||
config.logging.opentelemetry_service_name = env
|
||||
.monitor_logging_opentelemetry_service_name
|
||||
.unwrap_or(config.logging.opentelemetry_service_name);
|
||||
|
||||
config.local_auth =
|
||||
env.monitor_local_auth.unwrap_or(config.local_auth);
|
||||
|
||||
config.github_oauth.enabled = env
|
||||
.monitor_github_oauth_enabled
|
||||
.unwrap_or(config.github_oauth.enabled);
|
||||
config.github_oauth.id = env
|
||||
.monitor_github_oauth_id
|
||||
.unwrap_or(config.github_oauth.id);
|
||||
config.github_oauth.secret = env
|
||||
.monitor_github_oauth_secret
|
||||
.unwrap_or(config.github_oauth.secret);
|
||||
|
||||
config.google_oauth.enabled = env
|
||||
.monitor_google_oauth_enabled
|
||||
.unwrap_or(config.google_oauth.enabled);
|
||||
config.google_oauth.id = env
|
||||
.monitor_google_oauth_id
|
||||
.unwrap_or(config.google_oauth.id);
|
||||
config.google_oauth.secret = env
|
||||
.monitor_google_oauth_secret
|
||||
.unwrap_or(config.google_oauth.secret);
|
||||
|
||||
config.mongo.uri = env.monitor_mongo_uri.or(config.mongo.uri);
|
||||
config.mongo.address =
|
||||
env.monitor_mongo_address.or(config.mongo.address);
|
||||
config.mongo.username =
|
||||
env.monitor_mongo_username.or(config.mongo.username);
|
||||
config.mongo.password =
|
||||
env.monitor_mongo_password.or(config.mongo.password);
|
||||
config.mongo.app_name =
|
||||
env.monitor_mongo_app_name.unwrap_or(config.mongo.app_name);
|
||||
config.mongo.db_name =
|
||||
env.monitor_mongo_db_name.unwrap_or(config.mongo.db_name);
|
||||
|
||||
config.aws.access_key_id = env
|
||||
.monitor_aws_access_key_id
|
||||
.unwrap_or(config.aws.access_key_id);
|
||||
config.aws.secret_access_key = env
|
||||
.monitor_aws_secret_access_key
|
||||
.unwrap_or(config.aws.secret_access_key);
|
||||
|
||||
config
|
||||
})
|
||||
}
|
||||
117
bin/core/src/db.rs
Normal file
117
bin/core/src/db.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use mongo_indexed::{create_index, create_unique_index, Indexed};
|
||||
use monitor_client::entities::{
|
||||
alert::Alert,
|
||||
alerter::Alerter,
|
||||
api_key::ApiKey,
|
||||
build::Build,
|
||||
builder::Builder,
|
||||
config::core::MongoConfig,
|
||||
deployment::Deployment,
|
||||
permission::Permission,
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::{stats::SystemStatsRecord, Server},
|
||||
server_template::ServerTemplate,
|
||||
tag::Tag,
|
||||
update::Update,
|
||||
user::User,
|
||||
user_group::UserGroup,
|
||||
};
|
||||
use mungos::{
|
||||
init::MongoBuilder,
|
||||
mongodb::{Collection, Database},
|
||||
};
|
||||
|
||||
pub struct DbClient {
|
||||
pub users: Collection<User>,
|
||||
pub user_groups: Collection<UserGroup>,
|
||||
pub permissions: Collection<Permission>,
|
||||
pub api_keys: Collection<ApiKey>,
|
||||
pub tags: Collection<Tag>,
|
||||
pub updates: Collection<Update>,
|
||||
pub alerts: Collection<Alert>,
|
||||
pub stats: Collection<SystemStatsRecord>,
|
||||
// RESOURCES
|
||||
pub servers: Collection<Server>,
|
||||
pub deployments: Collection<Deployment>,
|
||||
pub builds: Collection<Build>,
|
||||
pub builders: Collection<Builder>,
|
||||
pub repos: Collection<Repo>,
|
||||
pub procedures: Collection<Procedure>,
|
||||
pub alerters: Collection<Alerter>,
|
||||
pub server_templates: Collection<ServerTemplate>,
|
||||
//
|
||||
pub db: Database,
|
||||
}
|
||||
|
||||
impl DbClient {
|
||||
pub async fn new(
|
||||
MongoConfig {
|
||||
uri,
|
||||
address,
|
||||
username,
|
||||
password,
|
||||
app_name,
|
||||
db_name,
|
||||
}: &MongoConfig,
|
||||
) -> anyhow::Result<DbClient> {
|
||||
let mut client = MongoBuilder::default().app_name(app_name);
|
||||
|
||||
match (uri, address, username, password) {
|
||||
(Some(uri), _, _, _) => {
|
||||
client = client.uri(uri);
|
||||
}
|
||||
(_, Some(address), Some(username), Some(password)) => {
|
||||
client = client
|
||||
.address(address)
|
||||
.username(username)
|
||||
.password(password);
|
||||
}
|
||||
(_, Some(address), _, _) => {
|
||||
client = client.address(address);
|
||||
}
|
||||
_ => {
|
||||
error!("config.mongo not configured correctly. must pass either config.mongo.uri, or config.mongo.address + config.mongo.username? + config.mongo.password?");
|
||||
std::process::exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
let client = client.build().await?;
|
||||
let db = client.database(db_name);
|
||||
|
||||
let client = DbClient {
|
||||
users: User::collection(&db, true).await?,
|
||||
user_groups: UserGroup::collection(&db, true).await?,
|
||||
permissions: Permission::collection(&db, true).await?,
|
||||
api_keys: ApiKey::collection(&db, true).await?,
|
||||
tags: Tag::collection(&db, true).await?,
|
||||
updates: Update::collection(&db, true).await?,
|
||||
alerts: Alert::collection(&db, true).await?,
|
||||
stats: SystemStatsRecord::collection(&db, true).await?,
|
||||
servers: resource_collection(&db, "Server").await?,
|
||||
deployments: resource_collection(&db, "Deployment").await?,
|
||||
builds: resource_collection(&db, "Build").await?,
|
||||
builders: resource_collection(&db, "Builder").await?,
|
||||
repos: resource_collection(&db, "Repo").await?,
|
||||
alerters: resource_collection(&db, "Alerter").await?,
|
||||
procedures: resource_collection(&db, "Procedure").await?,
|
||||
server_templates: resource_collection(&db, "ServerTemplate")
|
||||
.await?,
|
||||
db,
|
||||
};
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
||||
async fn resource_collection<T>(
|
||||
db: &Database,
|
||||
collection_name: &str,
|
||||
) -> anyhow::Result<Collection<T>> {
|
||||
let coll = db.collection::<T>(collection_name);
|
||||
|
||||
create_unique_index(&coll, "name").await?;
|
||||
|
||||
create_index(&coll, "tags").await?;
|
||||
|
||||
Ok(coll)
|
||||
}
|
||||
94
bin/core/src/helpers/action_state.rs
Normal file
94
bin/core/src/helpers/action_state.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use monitor_client::{
|
||||
busy::Busy,
|
||||
entities::{
|
||||
build::BuildActionState, deployment::DeploymentActionState,
|
||||
procedure::ProcedureActionState, repo::RepoActionState,
|
||||
server::ServerActionState,
|
||||
},
|
||||
};
|
||||
|
||||
use super::cache::Cache;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ActionStates {
|
||||
pub build: Cache<String, Arc<ActionState<BuildActionState>>>,
|
||||
pub deployment:
|
||||
Cache<String, Arc<ActionState<DeploymentActionState>>>,
|
||||
pub server: Cache<String, Arc<ActionState<ServerActionState>>>,
|
||||
pub repo: Cache<String, Arc<ActionState<RepoActionState>>>,
|
||||
pub procedure:
|
||||
Cache<String, Arc<ActionState<ProcedureActionState>>>,
|
||||
}
|
||||
|
||||
/// Need to be able to check "busy" with write lock acquired.
|
||||
#[derive(Default)]
|
||||
pub struct ActionState<States: Default + Send + 'static>(
|
||||
Mutex<States>,
|
||||
);
|
||||
|
||||
impl<States: Default + Busy + Copy + Send + 'static>
|
||||
ActionState<States>
|
||||
{
|
||||
pub fn get(&self) -> anyhow::Result<States> {
|
||||
Ok(
|
||||
*self
|
||||
.0
|
||||
.lock()
|
||||
.map_err(|e| anyhow!("action state lock poisoned | {e:?}"))?,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn busy(&self) -> anyhow::Result<bool> {
|
||||
Ok(
|
||||
self
|
||||
.0
|
||||
.lock()
|
||||
.map_err(|e| anyhow!("action state lock poisoned | {e:?}"))?
|
||||
.busy(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Will acquire lock, check busy, and if not will
|
||||
/// run the provided update function on the states.
|
||||
/// Returns a guard that returns the states to default (not busy) when dropped.
|
||||
pub fn update(
|
||||
&self,
|
||||
handler: impl Fn(&mut States),
|
||||
) -> anyhow::Result<UpdateGuard<States>> {
|
||||
let mut lock = self
|
||||
.0
|
||||
.lock()
|
||||
.map_err(|e| anyhow!("action state lock poisoned | {e:?}"))?;
|
||||
if lock.busy() {
|
||||
return Err(anyhow!("resource is busy"));
|
||||
}
|
||||
handler(&mut *lock);
|
||||
Ok(UpdateGuard(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// When dropped will return the inner state to default.
|
||||
/// The inner mutex guard must already be dropped BEFORE this is dropped,
|
||||
/// which is guaranteed as the inner guard is dropped by all public methods before
|
||||
/// user could drop UpdateGuard.
|
||||
pub struct UpdateGuard<'a, States: Default + Send + 'static>(
|
||||
&'a Mutex<States>,
|
||||
);
|
||||
|
||||
impl<'a, States: Default + Send + 'static> Drop
|
||||
for UpdateGuard<'a, States>
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
let mut lock = match self.0.lock() {
|
||||
Ok(lock) => lock,
|
||||
Err(e) => {
|
||||
error!("CRITICAL: an action state lock is poisoned | {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
*lock = States::default();
|
||||
}
|
||||
}
|
||||
257
bin/core/src/helpers/alert.rs
Normal file
257
bin/core/src/helpers/alert.rs
Normal file
@@ -0,0 +1,257 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use futures::future::join_all;
|
||||
use monitor_client::entities::{
|
||||
alert::{Alert, AlertData},
|
||||
alerter::*,
|
||||
deployment::DockerContainerState,
|
||||
server::stats::SeverityLevel,
|
||||
};
|
||||
use mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use reqwest::StatusCode;
|
||||
use slack::types::Block;
|
||||
|
||||
use crate::state::db_client;
|
||||
|
||||
#[instrument]
|
||||
pub async fn send_alerts(alerts: &[Alert]) {
|
||||
if alerts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let alerters = find_collect(
|
||||
&db_client().await.alerters,
|
||||
doc! { "config.params.enabled": true },
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(e) = alerters {
|
||||
error!(
|
||||
"ERROR sending alerts | failed to get alerters from db | {e:#}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let alerters = alerters.unwrap();
|
||||
|
||||
let handles =
|
||||
alerts.iter().map(|alert| send_alert(&alerters, alert));
|
||||
|
||||
join_all(handles).await;
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn send_alert(alerters: &[Alerter], alert: &Alert) {
|
||||
if alerters.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let handles = alerters.iter().map(|alerter| async {
|
||||
match &alerter.config {
|
||||
AlerterConfig::Slack(SlackAlerterConfig { url, enabled }) => {
|
||||
if !enabled {
|
||||
return Ok(());
|
||||
}
|
||||
send_slack_alert(url, alert)
|
||||
.await
|
||||
.context("failed to send slack alert")
|
||||
}
|
||||
AlerterConfig::Custom(CustomAlerterConfig { url, enabled }) => {
|
||||
if !enabled {
|
||||
return Ok(());
|
||||
}
|
||||
send_custom_alert(url, alert).await.context(format!(
|
||||
"failed to send alert to custom alerter at {url}"
|
||||
))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
join_all(handles)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|res| res.err())
|
||||
.for_each(|e| error!("{e:#}"));
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn send_custom_alert(
|
||||
url: &str,
|
||||
alert: &Alert,
|
||||
) -> anyhow::Result<()> {
|
||||
let res = reqwest::Client::new()
|
||||
.post(url)
|
||||
.json(alert)
|
||||
.send()
|
||||
.await
|
||||
.context("failed at post request to alerter")?;
|
||||
let status = res.status();
|
||||
if status != StatusCode::OK {
|
||||
let text = res
|
||||
.text()
|
||||
.await
|
||||
.context("failed to get response text on alerter response")?;
|
||||
return Err(anyhow!(
|
||||
"post to alerter failed | {status} | {text}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn send_slack_alert(
|
||||
url: &str,
|
||||
alert: &Alert,
|
||||
) -> anyhow::Result<()> {
|
||||
let level = fmt_level(alert.level);
|
||||
let (text, blocks): (_, Option<_>) = match &alert.data {
|
||||
AlertData::ServerUnreachable { name, region, .. } => {
|
||||
let region = fmt_region(region);
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
let text =
|
||||
format!("{level} | *{name}*{region} is now *reachable*");
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} is now *reachable*"
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
SeverityLevel::Critical => {
|
||||
let text =
|
||||
format!("{level} | *{name}*{region} is *unreachable* ❌");
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} is *unreachable* ❌"
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
AlertData::ServerCpu {
|
||||
name,
|
||||
region,
|
||||
percentage,
|
||||
..
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let text = format!("{level} | *{name}*{region} cpu usage at *{percentage:.1}%* 📈 🚨");
|
||||
let blocks = vec![
|
||||
Block::header(format!("{level} 🚨")),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} cpu usage at *{percentage:.1}%* 📈 🚨"
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::ServerMem {
|
||||
name,
|
||||
region,
|
||||
used_gb,
|
||||
total_gb,
|
||||
..
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
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*"
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::ServerDisk {
|
||||
name,
|
||||
region,
|
||||
path,
|
||||
used_gb,
|
||||
total_gb,
|
||||
..
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
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*"
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::ContainerStateChange {
|
||||
name,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
..
|
||||
} => {
|
||||
let to = fmt_docker_container_state(to);
|
||||
let text = format!("📦 container *{name}* is now {to}");
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"server: {server_name}\nprevious: {from}"
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::AwsBuilderTerminationFailed { instance_id } => {
|
||||
let text = format!(
|
||||
"{level} | Failed to terminated AWS builder instance"
|
||||
);
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!("instance id: {instance_id}")),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::None {} => Default::default(),
|
||||
};
|
||||
if !text.is_empty() {
|
||||
let slack = slack::Client::new(url);
|
||||
slack.send_message(text, blocks).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fmt_region(region: &Option<String>) -> String {
|
||||
match region {
|
||||
Some(region) => format!(" ({region})"),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_docker_container_state(
|
||||
state: &DockerContainerState,
|
||||
) -> String {
|
||||
match state {
|
||||
DockerContainerState::Running => String::from("Running ▶️"),
|
||||
DockerContainerState::Exited => String::from("Exited 🛑"),
|
||||
DockerContainerState::Restarting => String::from("Restarting 🔄"),
|
||||
DockerContainerState::NotDeployed => String::from("Not Deployed"),
|
||||
_ => state.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_level(level: SeverityLevel) -> &'static str {
|
||||
match level {
|
||||
SeverityLevel::Critical => "CRITICAL 🚨",
|
||||
SeverityLevel::Warning => "WARNING 🚨",
|
||||
SeverityLevel::Ok => "OK ✅",
|
||||
}
|
||||
}
|
||||
84
bin/core/src/helpers/cache.rs
Normal file
84
bin/core/src/helpers/cache.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use std::{collections::HashMap, hash::Hash};
|
||||
|
||||
use monitor_client::busy::Busy;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Cache<K: PartialEq + Eq + Hash, T: Clone + Default> {
|
||||
cache: RwLock<HashMap<K, T>>,
|
||||
}
|
||||
|
||||
impl<
|
||||
K: PartialEq + Eq + Hash + std::fmt::Debug + Clone,
|
||||
T: Clone + Default,
|
||||
> Cache<K, T>
|
||||
{
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn get(&self, key: &K) -> Option<T> {
|
||||
self.cache.read().await.get(key).cloned()
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn get_or_insert_default(&self, key: &K) -> T {
|
||||
let mut lock = self.cache.write().await;
|
||||
match lock.get(key).cloned() {
|
||||
Some(item) => item,
|
||||
None => {
|
||||
let item: T = Default::default();
|
||||
lock.insert(key.clone(), item.clone());
|
||||
item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn get_list(&self) -> Vec<T> {
|
||||
let cache = self.cache.read().await;
|
||||
cache.iter().map(|(_, e)| e.clone()).collect()
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn insert<Key>(&self, key: Key, val: T)
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
Key: Into<K> + std::fmt::Debug,
|
||||
{
|
||||
self.cache.write().await.insert(key.into(), val);
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self, handler))]
|
||||
pub async fn update_entry<Key>(
|
||||
&self,
|
||||
key: Key,
|
||||
handler: impl Fn(&mut T),
|
||||
) where
|
||||
Key: Into<K> + std::fmt::Debug,
|
||||
{
|
||||
let mut cache = self.cache.write().await;
|
||||
handler(cache.entry(key.into()).or_default());
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn clear(&self) {
|
||||
self.cache.write().await.clear();
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn remove(&self, key: &K) {
|
||||
self.cache.write().await.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
K: PartialEq + Eq + Hash + std::fmt::Debug + Clone,
|
||||
T: Clone + Default + Busy,
|
||||
> Cache<K, T>
|
||||
{
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn busy(&self, id: &K) -> bool {
|
||||
match self.get(id).await {
|
||||
Some(state) => state.busy(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
34
bin/core/src/helpers/channel.rs
Normal file
34
bin/core/src/helpers/channel.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use monitor_client::entities::update::{Update, UpdateListItem};
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
|
||||
/// A channel sending (build_id, update_id)
|
||||
pub fn build_cancel_channel(
|
||||
) -> &'static BroadcastChannel<(String, Update)> {
|
||||
static BUILD_CANCEL_CHANNEL: OnceLock<
|
||||
BroadcastChannel<(String, Update)>,
|
||||
> = OnceLock::new();
|
||||
BUILD_CANCEL_CHANNEL.get_or_init(|| BroadcastChannel::new(100))
|
||||
}
|
||||
|
||||
pub fn update_channel() -> &'static BroadcastChannel<UpdateListItem> {
|
||||
static UPDATE_CHANNEL: OnceLock<BroadcastChannel<UpdateListItem>> =
|
||||
OnceLock::new();
|
||||
UPDATE_CHANNEL.get_or_init(|| BroadcastChannel::new(100))
|
||||
}
|
||||
|
||||
pub struct BroadcastChannel<T> {
|
||||
pub sender: Mutex<broadcast::Sender<T>>,
|
||||
pub receiver: broadcast::Receiver<T>,
|
||||
}
|
||||
|
||||
impl<T: Clone> BroadcastChannel<T> {
|
||||
pub fn new(capacity: usize) -> BroadcastChannel<T> {
|
||||
let (sender, receiver) = broadcast::channel(capacity);
|
||||
BroadcastChannel {
|
||||
sender: sender.into(),
|
||||
receiver,
|
||||
}
|
||||
}
|
||||
}
|
||||
119
bin/core/src/helpers/mod.rs
Normal file
119
bin/core/src/helpers/mod.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use monitor_client::entities::{
|
||||
permission::{Permission, PermissionLevel, UserTarget},
|
||||
server::Server,
|
||||
update::ResourceTarget,
|
||||
user::User,
|
||||
};
|
||||
use mungos::mongodb::bson::doc;
|
||||
use periphery_client::PeripheryClient;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::{config::core_config, state::db_client};
|
||||
|
||||
pub mod action_state;
|
||||
pub mod alert;
|
||||
pub mod cache;
|
||||
pub mod channel;
|
||||
pub mod procedure;
|
||||
pub mod prune;
|
||||
pub mod query;
|
||||
pub mod resource;
|
||||
pub mod update;
|
||||
|
||||
pub fn empty_or_only_spaces(word: &str) -> bool {
|
||||
if word.is_empty() {
|
||||
return true;
|
||||
}
|
||||
for char in word.chars() {
|
||||
if char != ' ' {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn random_duration(min_ms: u64, max_ms: u64) -> Duration {
|
||||
Duration::from_millis(thread_rng().gen_range(min_ms..max_ms))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn remove_from_recently_viewed<T>(
|
||||
resource: T,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
T: Into<ResourceTarget> + std::fmt::Debug,
|
||||
{
|
||||
let resource: ResourceTarget = resource.into();
|
||||
let (ty, id) = resource.extract_variant_id();
|
||||
db_client()
|
||||
.await
|
||||
.users
|
||||
.update_many(
|
||||
doc! {},
|
||||
doc! {
|
||||
"$pull": {
|
||||
"recently_viewed": {
|
||||
"type": ty.to_string(),
|
||||
"id": id,
|
||||
}
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"failed to remove resource from users recently viewed",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
pub fn periphery_client(
|
||||
server: &Server,
|
||||
) -> anyhow::Result<PeripheryClient> {
|
||||
if !server.config.enabled {
|
||||
return Err(anyhow!("server not enabled"));
|
||||
}
|
||||
|
||||
let client = PeripheryClient::new(
|
||||
&server.config.address,
|
||||
&core_config().passkey,
|
||||
);
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn create_permission<T>(
|
||||
user: &User,
|
||||
target: T,
|
||||
level: PermissionLevel,
|
||||
) where
|
||||
T: Into<ResourceTarget> + std::fmt::Debug,
|
||||
{
|
||||
// No need to actually create permissions for admins
|
||||
if user.admin {
|
||||
return;
|
||||
}
|
||||
let target: ResourceTarget = target.into();
|
||||
if let Err(e) = db_client()
|
||||
.await
|
||||
.permissions
|
||||
.insert_one(
|
||||
Permission {
|
||||
id: Default::default(),
|
||||
user_target: UserTarget::User(user.id.clone()),
|
||||
resource_target: target.clone(),
|
||||
level,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("failed to create permission for {target:?} | {e:#}");
|
||||
};
|
||||
}
|
||||
273
bin/core/src/helpers/procedure.rs
Normal file
273
bin/core/src/helpers/procedure.rs
Normal file
@@ -0,0 +1,273 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::{anyhow, Context, Ok};
|
||||
use futures::future::join_all;
|
||||
use monitor_client::{
|
||||
api::execute::Execution,
|
||||
entities::{
|
||||
monitor_timestamp,
|
||||
procedure::{EnabledExecution, Procedure, ProcedureType},
|
||||
update::Update,
|
||||
user::procedure_user,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::state::State;
|
||||
|
||||
use super::update::update_update;
|
||||
|
||||
#[instrument]
|
||||
pub async fn execute_procedure(
|
||||
procedure: &Procedure,
|
||||
update: &Mutex<Update>,
|
||||
) -> anyhow::Result<()> {
|
||||
let start_ts = monitor_timestamp();
|
||||
|
||||
use ProcedureType::*;
|
||||
match procedure.config.procedure_type {
|
||||
Sequence => {
|
||||
add_line_to_update(
|
||||
update,
|
||||
&format!(
|
||||
"executing sequence: {} ({})",
|
||||
procedure.name, procedure.id
|
||||
),
|
||||
)
|
||||
.await;
|
||||
execute_sequence(
|
||||
filter_list_by_enabled(&procedure.config.executions),
|
||||
&procedure.id,
|
||||
&procedure.name,
|
||||
update,
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
let time = Duration::from_millis(
|
||||
(monitor_timestamp() - start_ts) as u64,
|
||||
);
|
||||
format!(
|
||||
"failed sequence execution after {time:?}. {} ({})",
|
||||
procedure.name, procedure.id
|
||||
)
|
||||
})?;
|
||||
let time = Duration::from_millis(
|
||||
(monitor_timestamp() - start_ts) as u64,
|
||||
);
|
||||
add_line_to_update(
|
||||
update,
|
||||
&format!(
|
||||
"finished sequence execution in {time:?}: {} ({}) ✅",
|
||||
procedure.name, procedure.id
|
||||
),
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
Parallel => {
|
||||
add_line_to_update(
|
||||
update,
|
||||
&format!(
|
||||
"executing parallel: {} ({})",
|
||||
procedure.name, procedure.id
|
||||
),
|
||||
)
|
||||
.await;
|
||||
execute_parallel(
|
||||
filter_list_by_enabled(&procedure.config.executions),
|
||||
&procedure.id,
|
||||
&procedure.name,
|
||||
update,
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
let time = Duration::from_millis(
|
||||
(monitor_timestamp() - start_ts) as u64,
|
||||
);
|
||||
format!(
|
||||
"failed parallel execution after {time:?}. {} ({})",
|
||||
procedure.name, procedure.id
|
||||
)
|
||||
})?;
|
||||
let time = Duration::from_millis(
|
||||
(monitor_timestamp() - start_ts) as u64,
|
||||
);
|
||||
add_line_to_update(
|
||||
update,
|
||||
&format!(
|
||||
"finished parallel execution in {time:?}: {} ({}) ✅",
|
||||
procedure.name, procedure.id
|
||||
),
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn execute_execution(
|
||||
execution: Execution,
|
||||
|
||||
// used to prevent recursive procedure
|
||||
parent_id: &str,
|
||||
parent_name: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let user = procedure_user().to_owned();
|
||||
let update = match execution {
|
||||
Execution::None(_) => return Ok(()),
|
||||
Execution::RunProcedure(req) => {
|
||||
if req.procedure == parent_id || req.procedure == parent_name {
|
||||
return Err(anyhow!("Self referential procedure detected"));
|
||||
}
|
||||
State
|
||||
.resolve(req, user)
|
||||
.await
|
||||
.context("failed at RunProcedure")?
|
||||
}
|
||||
Execution::RunBuild(req) => State
|
||||
.resolve(req, user)
|
||||
.await
|
||||
.context("failed at RunBuild")?,
|
||||
Execution::Deploy(req) => {
|
||||
State.resolve(req, user).await.context("failed at Deploy")?
|
||||
}
|
||||
Execution::StartContainer(req) => State
|
||||
.resolve(req, user)
|
||||
.await
|
||||
.context("failed at StartContainer")?,
|
||||
Execution::StopContainer(req) => {
|
||||
State
|
||||
.resolve(req, user)
|
||||
.await
|
||||
.context("failed at StopContainer")?
|
||||
}
|
||||
Execution::StopAllContainers(req) => State
|
||||
.resolve(req, user)
|
||||
.await
|
||||
.context("failed at StopAllContainers")?,
|
||||
Execution::RemoveContainer(req) => State
|
||||
.resolve(req, user)
|
||||
.await
|
||||
.context("failed at RemoveContainer")?,
|
||||
Execution::CloneRepo(req) => State
|
||||
.resolve(req, user)
|
||||
.await
|
||||
.context("failed at CloneRepo")?,
|
||||
Execution::PullRepo(req) => State
|
||||
.resolve(req, user)
|
||||
.await
|
||||
.context("failed at PullRepo")?,
|
||||
Execution::PruneDockerNetworks(req) => State
|
||||
.resolve(req, user)
|
||||
.await
|
||||
.context("failed at PruneDockerNetworks")?,
|
||||
Execution::PruneDockerImages(req) => State
|
||||
.resolve(req, user)
|
||||
.await
|
||||
.context("failed at PruneDockerImages")?,
|
||||
Execution::PruneDockerContainers(req) => State
|
||||
.resolve(req, user)
|
||||
.await
|
||||
.context("failed at PruneDockerContainers")?,
|
||||
};
|
||||
if update.success {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"execution not successful. see update {}",
|
||||
update.id
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn execute_sequence(
|
||||
executions: Vec<Execution>,
|
||||
parent_id: &str,
|
||||
parent_name: &str,
|
||||
update: &Mutex<Update>,
|
||||
) -> anyhow::Result<()> {
|
||||
for execution in executions {
|
||||
let now = Instant::now();
|
||||
add_line_to_update(
|
||||
update,
|
||||
&format!("executing stage: {execution:?}"),
|
||||
)
|
||||
.await;
|
||||
let fail_log = format!("failed on {execution:?}");
|
||||
execute_execution(execution.clone(), parent_id, parent_name)
|
||||
.await
|
||||
.context(fail_log)?;
|
||||
add_line_to_update(
|
||||
update,
|
||||
&format!(
|
||||
"finished stage in {:?}: {execution:?}",
|
||||
now.elapsed()
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn execute_parallel(
|
||||
executions: Vec<Execution>,
|
||||
parent_id: &str,
|
||||
parent_name: &str,
|
||||
update: &Mutex<Update>,
|
||||
) -> anyhow::Result<()> {
|
||||
let futures = executions.into_iter().map(|execution| async move {
|
||||
let now = Instant::now();
|
||||
add_line_to_update(
|
||||
update,
|
||||
&format!("executing stage: {execution:?}"),
|
||||
)
|
||||
.await;
|
||||
let fail_log = format!("failed on {execution:?}");
|
||||
let res =
|
||||
execute_execution(execution.clone(), parent_id, parent_name)
|
||||
.await
|
||||
.context(fail_log);
|
||||
add_line_to_update(
|
||||
update,
|
||||
&format!(
|
||||
"finished stage in {:?}: {execution:?}",
|
||||
now.elapsed()
|
||||
),
|
||||
)
|
||||
.await;
|
||||
res
|
||||
});
|
||||
join_all(futures)
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<anyhow::Result<_>>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn filter_list_by_enabled(
|
||||
list: &[EnabledExecution],
|
||||
) -> Vec<Execution> {
|
||||
list
|
||||
.iter()
|
||||
.filter(|item| item.enabled)
|
||||
.map(|item| item.execution.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// ASSUMES FIRST LOG IS ALREADY CREATED
|
||||
#[instrument(level = "debug")]
|
||||
async fn add_line_to_update(update: &Mutex<Update>, line: &str) {
|
||||
let mut lock = update.lock().await;
|
||||
let log = &mut lock.logs[0];
|
||||
log.stdout.push('\n');
|
||||
log.stdout.push_str(line);
|
||||
let update = lock.clone();
|
||||
drop(lock);
|
||||
if let Err(e) = update_update(update).await {
|
||||
error!("failed to update an update during procedure | {e:#}");
|
||||
};
|
||||
}
|
||||
64
bin/core/src/helpers/prune.rs
Normal file
64
bin/core/src/helpers/prune.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use async_timing_util::{
|
||||
unix_timestamp_ms, wait_until_timelength, Timelength, ONE_DAY_MS,
|
||||
};
|
||||
use mungos::mongodb::bson::doc;
|
||||
|
||||
use crate::{config::core_config, state::db_client};
|
||||
|
||||
pub fn spawn_prune_loop() {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
wait_until_timelength(Timelength::OneDay, 5000).await;
|
||||
let (stats_res, alerts_res) =
|
||||
tokio::join!(prune_stats(), prune_alerts());
|
||||
if let Err(e) = stats_res {
|
||||
error!("error in pruning stats | {e:#}");
|
||||
}
|
||||
if let Err(e) = alerts_res {
|
||||
error!("error in pruning alerts | {e:#}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn prune_stats() -> anyhow::Result<()> {
|
||||
if core_config().keep_stats_for_days == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let delete_before_ts = (unix_timestamp_ms()
|
||||
- core_config().keep_stats_for_days as u128 * ONE_DAY_MS)
|
||||
as i64;
|
||||
let res = db_client()
|
||||
.await
|
||||
.stats
|
||||
.delete_many(
|
||||
doc! {
|
||||
"ts": { "$lt": delete_before_ts }
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
info!("deleted {} stats from db", res.deleted_count);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn prune_alerts() -> anyhow::Result<()> {
|
||||
if core_config().keep_alerts_for_days == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let delete_before_ts = (unix_timestamp_ms()
|
||||
- core_config().keep_alerts_for_days as u128 * ONE_DAY_MS)
|
||||
as i64;
|
||||
let res = db_client()
|
||||
.await
|
||||
.alerts
|
||||
.delete_many(
|
||||
doc! {
|
||||
"ts": { "$lt": delete_before_ts }
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
info!("deleted {} alerts from db", res.deleted_count);
|
||||
Ok(())
|
||||
}
|
||||
140
bin/core/src/helpers/query.rs
Normal file
140
bin/core/src/helpers/query.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use monitor_client::entities::{
|
||||
deployment::{Deployment, DockerContainerState},
|
||||
server::{Server, ServerStatus},
|
||||
tag::Tag,
|
||||
user::{admin_service_user, User},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::find_one_by_id,
|
||||
find::find_collect,
|
||||
mongodb::bson::{doc, oid::ObjectId, Document},
|
||||
};
|
||||
|
||||
use crate::state::db_client;
|
||||
|
||||
use super::resource::StateResource;
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_user(user_id: &str) -> anyhow::Result<User> {
|
||||
if let Some(user) = admin_service_user(user_id) {
|
||||
return Ok(user);
|
||||
}
|
||||
find_one_by_id(&db_client().await.users, user_id)
|
||||
.await
|
||||
.context("failed to query mongo for user")?
|
||||
.with_context(|| format!("no user found with id {user_id}"))
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_server_with_status(
|
||||
server_id_or_name: &str,
|
||||
) -> anyhow::Result<(Server, ServerStatus)> {
|
||||
let server = Server::get_resource(server_id_or_name).await?;
|
||||
if !server.config.enabled {
|
||||
return Ok((server, ServerStatus::Disabled));
|
||||
}
|
||||
let status = match super::periphery_client(&server)?
|
||||
.request(periphery_client::api::GetHealth {})
|
||||
.await
|
||||
{
|
||||
Ok(_) => ServerStatus::Ok,
|
||||
Err(_) => ServerStatus::NotOk,
|
||||
};
|
||||
Ok((server, status))
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_deployment_state(
|
||||
deployment: &Deployment,
|
||||
) -> anyhow::Result<DockerContainerState> {
|
||||
if deployment.config.server_id.is_empty() {
|
||||
return Ok(DockerContainerState::NotDeployed);
|
||||
}
|
||||
let (server, status) =
|
||||
get_server_with_status(&deployment.config.server_id).await?;
|
||||
if status != ServerStatus::Ok {
|
||||
return Ok(DockerContainerState::Unknown);
|
||||
}
|
||||
let container = super::periphery_client(&server)?
|
||||
.request(periphery_client::api::container::GetContainerList {})
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|container| container.name == deployment.name);
|
||||
|
||||
let state = match container {
|
||||
Some(container) => container.state,
|
||||
None => DockerContainerState::NotDeployed,
|
||||
};
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
// TAG
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_tag(id_or_name: &str) -> anyhow::Result<Tag> {
|
||||
let query = match ObjectId::from_str(id_or_name) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": id_or_name },
|
||||
};
|
||||
db_client()
|
||||
.await
|
||||
.tags
|
||||
.find_one(query, None)
|
||||
.await
|
||||
.context("failed to query mongo for tag")?
|
||||
.with_context(|| format!("no tag found matching {id_or_name}"))
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_tag_check_owner(
|
||||
id_or_name: &str,
|
||||
user: &User,
|
||||
) -> anyhow::Result<Tag> {
|
||||
let tag = get_tag(id_or_name).await?;
|
||||
if user.admin || tag.owner == user.id {
|
||||
return Ok(tag);
|
||||
}
|
||||
Err(anyhow!("user must be tag owner or admin"))
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_user_user_group_ids(
|
||||
user_id: &str,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
let res = find_collect(
|
||||
&db_client().await.user_groups,
|
||||
doc! {
|
||||
"users": user_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to query db for user groups")?
|
||||
.into_iter()
|
||||
.map(|ug| ug.id)
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn user_target_query(
|
||||
user_id: &str,
|
||||
) -> anyhow::Result<Vec<Document>> {
|
||||
let mut user_target_query = vec![
|
||||
doc! { "user_target.type": "User", "user_target.id": user_id },
|
||||
];
|
||||
let user_groups = get_user_user_group_ids(user_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|ug_id| {
|
||||
doc! {
|
||||
"user_target.type": "UserGroup", "user_target.id": ug_id,
|
||||
}
|
||||
});
|
||||
user_target_query.extend(user_groups);
|
||||
Ok(user_target_query)
|
||||
}
|
||||
737
bin/core/src/helpers/resource.rs
Normal file
737
bin/core/src/helpers/resource.rs
Normal file
@@ -0,0 +1,737 @@
|
||||
use std::{collections::HashSet, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use futures::future::join_all;
|
||||
use monitor_client::{
|
||||
api::write::CreateTag,
|
||||
entities::{
|
||||
alerter::{
|
||||
Alerter, AlerterConfig, AlerterConfigVariant, AlerterInfo,
|
||||
AlerterListItem, AlerterListItemInfo, AlerterQuerySpecifics,
|
||||
},
|
||||
build::{
|
||||
Build, BuildConfig, BuildInfo, BuildListItem,
|
||||
BuildListItemInfo, BuildQuerySpecifics,
|
||||
},
|
||||
builder::{
|
||||
Builder, BuilderConfig, BuilderConfigVariant, BuilderListItem,
|
||||
BuilderListItemInfo, BuilderQuerySpecifics,
|
||||
},
|
||||
deployment::{
|
||||
Deployment, DeploymentConfig, DeploymentImage,
|
||||
DeploymentListItem, DeploymentListItemInfo,
|
||||
DeploymentQuerySpecifics,
|
||||
},
|
||||
permission::PermissionLevel,
|
||||
procedure::{
|
||||
Procedure, ProcedureConfig, ProcedureListItem,
|
||||
ProcedureListItemInfo, ProcedureQuerySpecifics,
|
||||
},
|
||||
repo::{
|
||||
Repo, RepoConfig, RepoInfo, RepoListItem, RepoListItemInfo,
|
||||
RepoQuerySpecifics,
|
||||
},
|
||||
resource::{AddFilters, Resource, ResourceQuery},
|
||||
server::{
|
||||
Server, ServerConfig, ServerListItem, ServerListItemInfo,
|
||||
ServerQuerySpecifics,
|
||||
},
|
||||
server_template::{
|
||||
ServerTemplate, ServerTemplateConfig,
|
||||
ServerTemplateConfigVariant, ServerTemplateListItem,
|
||||
ServerTemplateListItemInfo, ServerTemplateQuerySpecifics,
|
||||
},
|
||||
update::{ResourceTarget, ResourceTargetVariant},
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::{
|
||||
bson::{doc, oid::ObjectId, Document},
|
||||
Collection,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
use crate::{
|
||||
helpers::query::user_target_query,
|
||||
state::State,
|
||||
state::{db_client, deployment_status_cache, server_status_cache},
|
||||
};
|
||||
|
||||
use super::query::get_tag;
|
||||
|
||||
pub trait StateResource {
|
||||
type ListItem: Serialize + Send;
|
||||
type Config: Send
|
||||
+ Sync
|
||||
+ Unpin
|
||||
+ Serialize
|
||||
+ DeserializeOwned
|
||||
+ 'static;
|
||||
type Info: Send
|
||||
+ Sync
|
||||
+ Unpin
|
||||
+ Default
|
||||
+ Serialize
|
||||
+ DeserializeOwned
|
||||
+ 'static;
|
||||
type QuerySpecifics: AddFilters + Default + std::fmt::Debug;
|
||||
|
||||
fn name() -> &'static str;
|
||||
|
||||
fn resource_target_variant() -> ResourceTargetVariant;
|
||||
|
||||
async fn coll(
|
||||
) -> &'static Collection<Resource<Self::Config, Self::Info>>;
|
||||
|
||||
async fn to_list_item(
|
||||
resource: Resource<Self::Config, Self::Info>,
|
||||
) -> anyhow::Result<Self::ListItem>;
|
||||
|
||||
async fn get_resource(
|
||||
id_or_name: &str,
|
||||
) -> anyhow::Result<Resource<Self::Config, Self::Info>> {
|
||||
let filter = match ObjectId::from_str(id_or_name) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": id_or_name },
|
||||
};
|
||||
Self::coll()
|
||||
.await
|
||||
.find_one(filter, None)
|
||||
.await
|
||||
.context("failed to query db for resource")?
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"did not find any {} matching {id_or_name}",
|
||||
Self::name()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_resource_check_permissions(
|
||||
id_or_name: &str,
|
||||
user: &User,
|
||||
permission_level: PermissionLevel,
|
||||
) -> anyhow::Result<Resource<Self::Config, Self::Info>> {
|
||||
let resource = Self::get_resource(id_or_name).await?;
|
||||
if user.admin {
|
||||
return Ok(resource);
|
||||
}
|
||||
let permissions =
|
||||
Self::get_user_permission_on_resource(&user.id, &resource.id)
|
||||
.await?;
|
||||
if permissions >= permission_level {
|
||||
Ok(resource)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"user does not have required permissions on this {}",
|
||||
Self::name()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_user_permission_on_resource(
|
||||
user_id: &str,
|
||||
resource_id: &str,
|
||||
) -> anyhow::Result<PermissionLevel> {
|
||||
get_user_permission_on_resource(
|
||||
user_id,
|
||||
Self::resource_target_variant(),
|
||||
resource_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_resource_ids_for_non_admin(
|
||||
user_id: &str,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
get_resource_ids_for_non_admin(
|
||||
user_id,
|
||||
Self::resource_target_variant(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_resource_list_items_for_user(
|
||||
mut query: ResourceQuery<Self::QuerySpecifics>,
|
||||
user: &User,
|
||||
) -> anyhow::Result<Vec<Self::ListItem>> {
|
||||
validate_resource_query_tags(&mut query).await;
|
||||
let mut filters = Document::new();
|
||||
query.add_filters(&mut filters);
|
||||
Self::query_resource_list_items_for_user(filters, user).await
|
||||
}
|
||||
|
||||
async fn query_resource_list_items_for_user(
|
||||
filters: Document,
|
||||
user: &User,
|
||||
) -> anyhow::Result<Vec<Self::ListItem>> {
|
||||
let list = Self::query_resources_for_user(filters, user)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| Self::to_list_item(resource));
|
||||
|
||||
let list = join_all(list)
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<anyhow::Result<Vec<_>>>()
|
||||
.context(format!(
|
||||
"failed to convert {} list item",
|
||||
Self::name()
|
||||
))?;
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
async fn list_resources_for_user(
|
||||
mut query: ResourceQuery<Self::QuerySpecifics>,
|
||||
user: &User,
|
||||
) -> anyhow::Result<Vec<Resource<Self::Config, Self::Info>>> {
|
||||
validate_resource_query_tags(&mut query).await;
|
||||
let mut filters = Document::new();
|
||||
query.add_filters(&mut filters);
|
||||
Self::query_resources_for_user(filters, user).await
|
||||
}
|
||||
|
||||
async fn query_resources_for_user(
|
||||
mut filters: Document,
|
||||
user: &User,
|
||||
) -> anyhow::Result<Vec<Resource<Self::Config, Self::Info>>> {
|
||||
if !user.admin {
|
||||
let ids = Self::get_resource_ids_for_non_admin(&user.id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.flat_map(|id| ObjectId::from_str(&id))
|
||||
.collect::<Vec<_>>();
|
||||
filters.insert("_id", doc! { "$in": ids });
|
||||
}
|
||||
find_collect(Self::coll().await, filters, None)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to pull {}s from mongo", Self::name())
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_description(
|
||||
id_or_name: &str,
|
||||
description: &str,
|
||||
user: &User,
|
||||
) -> anyhow::Result<()> {
|
||||
Self::get_resource_check_permissions(
|
||||
id_or_name,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
let filter = match ObjectId::from_str(id_or_name) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": id_or_name },
|
||||
};
|
||||
Self::coll()
|
||||
.await
|
||||
.update_one(
|
||||
filter,
|
||||
doc! { "$set": { "description": description } },
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_tags_on_resource(
|
||||
id_or_name: &str,
|
||||
tags: Vec<String>,
|
||||
user: User,
|
||||
) -> anyhow::Result<()> {
|
||||
let futures = tags.iter().map(|tag| async {
|
||||
match get_tag(tag).await {
|
||||
Ok(tag) => Ok(tag.id),
|
||||
Err(_) => State
|
||||
.resolve(
|
||||
CreateTag {
|
||||
name: tag.to_string(),
|
||||
},
|
||||
user.clone(),
|
||||
)
|
||||
.await
|
||||
.map(|tag| tag.id),
|
||||
}
|
||||
});
|
||||
let tags = join_all(futures)
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
Self::coll()
|
||||
.await
|
||||
.update_one(
|
||||
id_or_name_filter(id_or_name),
|
||||
doc! { "$set": { "tags": tags } },
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_tag_from_resources(
|
||||
tag_id: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
Self::coll()
|
||||
.await
|
||||
.update_many(
|
||||
doc! {},
|
||||
doc! { "$pull": { "tags": tag_id } },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to remove tag from resources")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn id_or_name_filter(id_or_name: &str) -> Document {
|
||||
match ObjectId::from_str(id_or_name) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": id_or_name },
|
||||
}
|
||||
}
|
||||
|
||||
impl StateResource for Server {
|
||||
type ListItem = ServerListItem;
|
||||
type Config = ServerConfig;
|
||||
type Info = ();
|
||||
type QuerySpecifics = ServerQuerySpecifics;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"server"
|
||||
}
|
||||
|
||||
fn resource_target_variant() -> ResourceTargetVariant {
|
||||
ResourceTargetVariant::Server
|
||||
}
|
||||
|
||||
async fn coll() -> &'static Collection<Server> {
|
||||
&db_client().await.servers
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
server: Server,
|
||||
) -> anyhow::Result<ServerListItem> {
|
||||
let status = server_status_cache().get(&server.id).await;
|
||||
Ok(ServerListItem {
|
||||
name: server.name,
|
||||
created_at: ObjectId::from_str(&server.id)?
|
||||
.timestamp()
|
||||
.timestamp_millis(),
|
||||
id: server.id,
|
||||
tags: server.tags,
|
||||
resource_type: ResourceTargetVariant::Server,
|
||||
info: ServerListItemInfo {
|
||||
status: status.map(|s| s.status).unwrap_or_default(),
|
||||
region: server.config.region,
|
||||
send_unreachable_alerts: server
|
||||
.config
|
||||
.send_unreachable_alerts,
|
||||
send_cpu_alerts: server.config.send_cpu_alerts,
|
||||
send_mem_alerts: server.config.send_mem_alerts,
|
||||
send_disk_alerts: server.config.send_disk_alerts,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StateResource for Deployment {
|
||||
type ListItem = DeploymentListItem;
|
||||
type Config = DeploymentConfig;
|
||||
type Info = ();
|
||||
type QuerySpecifics = DeploymentQuerySpecifics;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"deployment"
|
||||
}
|
||||
|
||||
fn resource_target_variant() -> ResourceTargetVariant {
|
||||
ResourceTargetVariant::Deployment
|
||||
}
|
||||
|
||||
async fn coll() -> &'static Collection<Deployment> {
|
||||
&db_client().await.deployments
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
deployment: Deployment,
|
||||
) -> anyhow::Result<DeploymentListItem> {
|
||||
let status = deployment_status_cache().get(&deployment.id).await;
|
||||
let (image, build_id) = match deployment.config.image {
|
||||
DeploymentImage::Build { build_id, version } => {
|
||||
let build = Build::get_resource(&build_id).await?;
|
||||
let version = if version.is_none() {
|
||||
build.config.version.to_string()
|
||||
} else {
|
||||
version.to_string()
|
||||
};
|
||||
(format!("{}:{version}", build.name), Some(build_id))
|
||||
}
|
||||
DeploymentImage::Image { image } => (image, None),
|
||||
};
|
||||
Ok(DeploymentListItem {
|
||||
name: deployment.name,
|
||||
created_at: ObjectId::from_str(&deployment.id)?
|
||||
.timestamp()
|
||||
.timestamp_millis(),
|
||||
id: deployment.id,
|
||||
tags: deployment.tags,
|
||||
resource_type: ResourceTargetVariant::Deployment,
|
||||
info: DeploymentListItemInfo {
|
||||
state: status
|
||||
.as_ref()
|
||||
.map(|s| s.curr.state)
|
||||
.unwrap_or_default(),
|
||||
status: status.as_ref().and_then(|s| {
|
||||
s.curr.container.as_ref().and_then(|c| c.status.to_owned())
|
||||
}),
|
||||
image,
|
||||
server_id: deployment.config.server_id,
|
||||
build_id,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StateResource for Build {
|
||||
type ListItem = BuildListItem;
|
||||
type Config = BuildConfig;
|
||||
type Info = BuildInfo;
|
||||
type QuerySpecifics = BuildQuerySpecifics;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"build"
|
||||
}
|
||||
|
||||
fn resource_target_variant() -> ResourceTargetVariant {
|
||||
ResourceTargetVariant::Build
|
||||
}
|
||||
|
||||
async fn coll() -> &'static Collection<Build> {
|
||||
&db_client().await.builds
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
build: Build,
|
||||
) -> anyhow::Result<BuildListItem> {
|
||||
Ok(BuildListItem {
|
||||
name: build.name,
|
||||
created_at: ObjectId::from_str(&build.id)?
|
||||
.timestamp()
|
||||
.timestamp_millis(),
|
||||
id: build.id,
|
||||
tags: build.tags,
|
||||
resource_type: ResourceTargetVariant::Build,
|
||||
info: BuildListItemInfo {
|
||||
last_built_at: build.info.last_built_at,
|
||||
version: build.config.version,
|
||||
repo: build.config.repo,
|
||||
branch: build.config.branch,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StateResource for Repo {
|
||||
type ListItem = RepoListItem;
|
||||
type Config = RepoConfig;
|
||||
type Info = RepoInfo;
|
||||
type QuerySpecifics = RepoQuerySpecifics;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"repo"
|
||||
}
|
||||
|
||||
fn resource_target_variant() -> ResourceTargetVariant {
|
||||
ResourceTargetVariant::Repo
|
||||
}
|
||||
|
||||
async fn coll() -> &'static Collection<Repo> {
|
||||
&db_client().await.repos
|
||||
}
|
||||
|
||||
async fn to_list_item(repo: Repo) -> anyhow::Result<RepoListItem> {
|
||||
Ok(RepoListItem {
|
||||
name: repo.name,
|
||||
created_at: ObjectId::from_str(&repo.id)?
|
||||
.timestamp()
|
||||
.timestamp_millis(),
|
||||
id: repo.id,
|
||||
tags: repo.tags,
|
||||
resource_type: ResourceTargetVariant::Repo,
|
||||
info: RepoListItemInfo {
|
||||
last_pulled_at: repo.info.last_pulled_at,
|
||||
repo: repo.config.repo,
|
||||
branch: repo.config.branch,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StateResource for Builder {
|
||||
type ListItem = BuilderListItem;
|
||||
type Config = BuilderConfig;
|
||||
type Info = ();
|
||||
type QuerySpecifics = BuilderQuerySpecifics;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"builder"
|
||||
}
|
||||
|
||||
fn resource_target_variant() -> ResourceTargetVariant {
|
||||
ResourceTargetVariant::Builder
|
||||
}
|
||||
|
||||
async fn coll() -> &'static Collection<Builder> {
|
||||
&db_client().await.builders
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
builder: Builder,
|
||||
) -> anyhow::Result<BuilderListItem> {
|
||||
let (builder_type, instance_type) = match builder.config {
|
||||
BuilderConfig::Server(config) => (
|
||||
BuilderConfigVariant::Server.to_string(),
|
||||
Some(config.server_id),
|
||||
),
|
||||
BuilderConfig::Aws(config) => (
|
||||
BuilderConfigVariant::Aws.to_string(),
|
||||
Some(config.instance_type),
|
||||
),
|
||||
};
|
||||
|
||||
Ok(BuilderListItem {
|
||||
name: builder.name,
|
||||
created_at: ObjectId::from_str(&builder.id)?
|
||||
.timestamp()
|
||||
.timestamp_millis(),
|
||||
id: builder.id,
|
||||
tags: builder.tags,
|
||||
resource_type: ResourceTargetVariant::Builder,
|
||||
info: BuilderListItemInfo {
|
||||
builder_type,
|
||||
instance_type,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StateResource for Alerter {
|
||||
type ListItem = AlerterListItem;
|
||||
type Config = AlerterConfig;
|
||||
type Info = AlerterInfo;
|
||||
type QuerySpecifics = AlerterQuerySpecifics;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"alerter"
|
||||
}
|
||||
|
||||
fn resource_target_variant() -> ResourceTargetVariant {
|
||||
ResourceTargetVariant::Alerter
|
||||
}
|
||||
|
||||
async fn coll() -> &'static Collection<Alerter> {
|
||||
&db_client().await.alerters
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
alerter: Alerter,
|
||||
) -> anyhow::Result<AlerterListItem> {
|
||||
let (alerter_type, enabled) = match alerter.config {
|
||||
AlerterConfig::Custom(config) => {
|
||||
(AlerterConfigVariant::Custom.to_string(), config.enabled)
|
||||
}
|
||||
AlerterConfig::Slack(config) => {
|
||||
(AlerterConfigVariant::Slack.to_string(), config.enabled)
|
||||
}
|
||||
};
|
||||
Ok(AlerterListItem {
|
||||
name: alerter.name,
|
||||
created_at: ObjectId::from_str(&alerter.id)?
|
||||
.timestamp()
|
||||
.timestamp_millis(),
|
||||
id: alerter.id,
|
||||
tags: alerter.tags,
|
||||
resource_type: ResourceTargetVariant::Alerter,
|
||||
info: AlerterListItemInfo {
|
||||
alerter_type: alerter_type.to_string(),
|
||||
is_default: alerter.info.is_default,
|
||||
enabled,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StateResource for Procedure {
|
||||
type ListItem = ProcedureListItem;
|
||||
type Config = ProcedureConfig;
|
||||
type Info = ();
|
||||
type QuerySpecifics = ProcedureQuerySpecifics;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"procedure"
|
||||
}
|
||||
|
||||
fn resource_target_variant() -> ResourceTargetVariant {
|
||||
ResourceTargetVariant::Procedure
|
||||
}
|
||||
|
||||
async fn coll() -> &'static Collection<Procedure> {
|
||||
&db_client().await.procedures
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
procedure: Procedure,
|
||||
) -> anyhow::Result<ProcedureListItem> {
|
||||
Ok(ProcedureListItem {
|
||||
name: procedure.name,
|
||||
created_at: ObjectId::from_str(&procedure.id)?
|
||||
.timestamp()
|
||||
.timestamp_millis(),
|
||||
id: procedure.id,
|
||||
tags: procedure.tags,
|
||||
resource_type: ResourceTargetVariant::Procedure,
|
||||
info: ProcedureListItemInfo {
|
||||
procedure_type: procedure.config.procedure_type,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StateResource for ServerTemplate {
|
||||
type ListItem = ServerTemplateListItem;
|
||||
type Config = ServerTemplateConfig;
|
||||
type Info = ();
|
||||
type QuerySpecifics = ServerTemplateQuerySpecifics;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"server_template"
|
||||
}
|
||||
|
||||
fn resource_target_variant() -> ResourceTargetVariant {
|
||||
ResourceTargetVariant::Alerter
|
||||
}
|
||||
|
||||
async fn coll() -> &'static Collection<ServerTemplate> {
|
||||
&db_client().await.server_templates
|
||||
}
|
||||
|
||||
async fn to_list_item(
|
||||
server_template: ServerTemplate,
|
||||
) -> anyhow::Result<ServerTemplateListItem> {
|
||||
let (template_type, instance_type) = match server_template.config
|
||||
{
|
||||
ServerTemplateConfig::Aws(config) => (
|
||||
ServerTemplateConfigVariant::Aws.to_string(),
|
||||
Some(config.instance_type),
|
||||
),
|
||||
};
|
||||
Ok(ServerTemplateListItem {
|
||||
name: server_template.name,
|
||||
created_at: ObjectId::from_str(&server_template.id)?
|
||||
.timestamp()
|
||||
.timestamp_millis(),
|
||||
id: server_template.id,
|
||||
tags: server_template.tags,
|
||||
resource_type: ResourceTargetVariant::ServerTemplate,
|
||||
info: ServerTemplateListItemInfo {
|
||||
provider: template_type.to_string(),
|
||||
instance_type,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_user_permission_on_resource(
|
||||
user_id: &str,
|
||||
resource_variant: ResourceTargetVariant,
|
||||
resource_id: &str,
|
||||
) -> anyhow::Result<PermissionLevel> {
|
||||
let permission = find_collect(
|
||||
&db_client().await.permissions,
|
||||
doc! {
|
||||
"$or": user_target_query(user_id).await?,
|
||||
"resource_target.type": resource_variant.as_ref(),
|
||||
"resource_target.id": resource_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to query db for permissions")?
|
||||
.into_iter()
|
||||
// get the max permission user has between personal / any user groups
|
||||
.fold(PermissionLevel::None, |level, permission| {
|
||||
if permission.level > level {
|
||||
permission.level
|
||||
} else {
|
||||
level
|
||||
}
|
||||
});
|
||||
Ok(permission)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn delete_all_permissions_on_resource<T>(target: T)
|
||||
where
|
||||
T: Into<ResourceTarget> + std::fmt::Debug,
|
||||
{
|
||||
let target: ResourceTarget = target.into();
|
||||
let (variant, id) = target.extract_variant_id();
|
||||
if let Err(e) = db_client()
|
||||
.await
|
||||
.permissions
|
||||
.delete_many(
|
||||
doc! {
|
||||
"resource_target.type": variant.as_ref(),
|
||||
"resource_target.id": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("failed to delete_many permissions matching target {target:?} | {e:#}");
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_resource_ids_for_non_admin(
|
||||
user_id: &str,
|
||||
resource_type: ResourceTargetVariant,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
let permissions = find_collect(
|
||||
&db_client().await.permissions,
|
||||
doc! {
|
||||
"$or": user_target_query(user_id).await?,
|
||||
"resource_target.type": resource_type.as_ref(),
|
||||
"level": { "$in": ["Read", "Execute", "Write"] }
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to query permissions on db")?
|
||||
.into_iter()
|
||||
.map(|p| p.resource_target.extract_variant_id().1.to_string())
|
||||
// collect into hashset first to remove any duplicates
|
||||
.collect::<HashSet<_>>();
|
||||
Ok(permissions.into_iter().collect())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn validate_resource_query_tags<
|
||||
T: Default + std::fmt::Debug,
|
||||
>(
|
||||
query: &mut ResourceQuery<T>,
|
||||
) {
|
||||
let futures = query.tags.iter().map(|tag| get_tag(tag));
|
||||
let res = join_all(futures).await;
|
||||
query.tags = res.into_iter().flatten().map(|tag| tag.id).collect();
|
||||
}
|
||||
95
bin/core/src/helpers/update.rs
Normal file
95
bin/core/src/helpers/update.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use anyhow::Context;
|
||||
use monitor_client::entities::{
|
||||
monitor_timestamp,
|
||||
update::{ResourceTarget, Update, UpdateListItem},
|
||||
user::User,
|
||||
Operation,
|
||||
};
|
||||
use mungos::{
|
||||
by_id::{find_one_by_id, update_one_by_id},
|
||||
mongodb::bson::to_document,
|
||||
};
|
||||
|
||||
use crate::state::db_client;
|
||||
|
||||
use super::channel::update_channel;
|
||||
|
||||
pub fn make_update(
|
||||
target: impl Into<ResourceTarget>,
|
||||
operation: Operation,
|
||||
user: &User,
|
||||
) -> Update {
|
||||
Update {
|
||||
start_ts: monitor_timestamp(),
|
||||
target: target.into(),
|
||||
operation,
|
||||
operator: user.id.clone(),
|
||||
success: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn update_list_item(
|
||||
update: Update,
|
||||
) -> anyhow::Result<UpdateListItem> {
|
||||
let username = if User::is_service_user(&update.operator) {
|
||||
update.operator.clone()
|
||||
} else {
|
||||
find_one_by_id(&db_client().await.users, &update.operator)
|
||||
.await
|
||||
.context("failed to query mongo for user")?
|
||||
.with_context(|| {
|
||||
format!("no user found with id {}", update.operator)
|
||||
})?
|
||||
.username
|
||||
};
|
||||
let update = UpdateListItem {
|
||||
id: update.id,
|
||||
operation: update.operation,
|
||||
start_ts: update.start_ts,
|
||||
success: update.success,
|
||||
operator: update.operator,
|
||||
target: update.target,
|
||||
status: update.status,
|
||||
version: update.version,
|
||||
username,
|
||||
};
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn send_update(update: UpdateListItem) -> anyhow::Result<()> {
|
||||
update_channel().sender.lock().await.send(update)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn add_update(
|
||||
mut update: Update,
|
||||
) -> anyhow::Result<String> {
|
||||
update.id = db_client()
|
||||
.await
|
||||
.updates
|
||||
.insert_one(&update, None)
|
||||
.await
|
||||
.context("failed to insert update into db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not object id")?
|
||||
.to_string();
|
||||
let id = update.id.clone();
|
||||
let update = update_list_item(update).await?;
|
||||
let _ = send_update(update).await;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn update_update(update: Update) -> anyhow::Result<()> {
|
||||
update_one_by_id(&db_client().await.updates, &update.id, mungos::update::Update::Set(to_document(&update)?), None)
|
||||
.await
|
||||
.context("failed to update the update on db. the update build process was deleted")?;
|
||||
let update = update_list_item(update).await?;
|
||||
let _ = send_update(update).await;
|
||||
Ok(())
|
||||
}
|
||||
238
bin/core/src/listener/github.rs
Normal file
238
bin/core/src/listener/github.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{extract::Path, http::HeaderMap, routing::post, Router};
|
||||
use hex::ToHex;
|
||||
use hmac::{Hmac, Mac};
|
||||
use monitor_client::{
|
||||
api::execute,
|
||||
entities::{build::Build, repo::Repo, user::github_user},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use serde::Deserialize;
|
||||
use sha2::Sha256;
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{random_duration, resource::StateResource},
|
||||
state::State,
|
||||
};
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Id {
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct IdBranch {
|
||||
id: String,
|
||||
branch: String,
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/build/:id",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("build_webhook", id);
|
||||
async {
|
||||
let res = handle_build_webhook(id.clone(), headers, body).await;
|
||||
if let Err(e) = res {
|
||||
warn!("failed to run build webook for build {id} | {e:#}");
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/repo/:id/clone",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("repo_clone_webhook", id);
|
||||
async {
|
||||
let res = handle_repo_clone_webhook(id.clone(), headers, body).await;
|
||||
if let Err(e) = res {
|
||||
warn!("failed to run repo clone webook for repo {id} | {e:#}");
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
},
|
||||
)
|
||||
)
|
||||
.route(
|
||||
"/repo/:id/pull",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("repo_pull_webhook", id);
|
||||
async {
|
||||
let res = handle_repo_pull_webhook(id.clone(), headers, body).await;
|
||||
if let Err(e) = res {
|
||||
warn!("failed to run repo pull webook for repo {id} | {e:#}");
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
},
|
||||
)
|
||||
)
|
||||
.route(
|
||||
"/procedure/:id/:branch",
|
||||
post(
|
||||
|Path(IdBranch { id, branch }), headers: HeaderMap, body: String| async move {
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("procedure_webhook", id, branch);
|
||||
async {
|
||||
let res = handle_procedure_webhook(
|
||||
id.clone(),
|
||||
branch,
|
||||
headers,
|
||||
body
|
||||
).await;
|
||||
if let Err(e) = res {
|
||||
warn!("failed to run procedure webook for procedure {id} | {e:#}");
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle_build_webhook(
|
||||
build_id: String,
|
||||
headers: HeaderMap,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
verify_gh_signature(headers, &body).await?;
|
||||
let request_branch = extract_branch(&body)?;
|
||||
let build = Build::get_resource(&build_id).await?;
|
||||
if request_branch != build.config.branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
State
|
||||
.resolve(
|
||||
execute::RunBuild { build: build_id },
|
||||
github_user().to_owned(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_repo_clone_webhook(
|
||||
repo_id: String,
|
||||
headers: HeaderMap,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
verify_gh_signature(headers, &body).await?;
|
||||
let request_branch = extract_branch(&body)?;
|
||||
let repo = Repo::get_resource(&repo_id).await?;
|
||||
if request_branch != repo.config.branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
State
|
||||
.resolve(
|
||||
execute::CloneRepo { repo: repo_id },
|
||||
github_user().to_owned(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_repo_pull_webhook(
|
||||
repo_id: String,
|
||||
headers: HeaderMap,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
verify_gh_signature(headers, &body).await?;
|
||||
let request_branch = extract_branch(&body)?;
|
||||
let repo = Repo::get_resource(&repo_id).await?;
|
||||
if request_branch != repo.config.branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
State
|
||||
.resolve(
|
||||
execute::PullRepo { repo: repo_id },
|
||||
github_user().to_owned(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_procedure_webhook(
|
||||
procedure_id: String,
|
||||
target_branch: String,
|
||||
headers: HeaderMap,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
verify_gh_signature(headers, &body).await?;
|
||||
let request_branch = extract_branch(&body)?;
|
||||
if request_branch != target_branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
State
|
||||
.resolve(
|
||||
execute::RunProcedure {
|
||||
procedure: procedure_id,
|
||||
},
|
||||
github_user().to_owned(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn verify_gh_signature(
|
||||
headers: HeaderMap,
|
||||
body: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
// wait random amount of time
|
||||
tokio::time::sleep(random_duration(0, 500)).await;
|
||||
|
||||
let signature = headers.get("x-hub-signature-256");
|
||||
if signature.is_none() {
|
||||
return Err(anyhow!("no signature in headers"));
|
||||
}
|
||||
let signature = signature.unwrap().to_str();
|
||||
if signature.is_err() {
|
||||
return Err(anyhow!("failed to unwrap signature"));
|
||||
}
|
||||
let signature = signature.unwrap().replace("sha256=", "");
|
||||
let mut mac = HmacSha256::new_from_slice(
|
||||
core_config().github_webhook_secret.as_bytes(),
|
||||
)
|
||||
.expect("github webhook | failed to create hmac sha256");
|
||||
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,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
7
bin/core/src/listener/mod.rs
Normal file
7
bin/core/src/listener/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use axum::Router;
|
||||
|
||||
mod github;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new().nest("/github", github::router())
|
||||
}
|
||||
91
bin/core/src/main.rs
Normal file
91
bin/core/src/main.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
use std::{net::SocketAddr, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::Router;
|
||||
use termination_signal::tokio::immediate_term_handle;
|
||||
use tower_http::{
|
||||
cors::{Any, CorsLayer},
|
||||
services::{ServeDir, ServeFile},
|
||||
};
|
||||
|
||||
use crate::config::{core_config, frontend_path};
|
||||
|
||||
mod api;
|
||||
mod auth;
|
||||
mod cloud;
|
||||
mod config;
|
||||
mod db;
|
||||
mod helpers;
|
||||
mod listener;
|
||||
mod monitor;
|
||||
mod state;
|
||||
mod ws;
|
||||
|
||||
async fn app() -> anyhow::Result<()> {
|
||||
dotenv::dotenv().ok();
|
||||
let config = core_config();
|
||||
logger::init(&config.logging)?;
|
||||
info!("monitor core version: v{}", env!("CARGO_PKG_VERSION"));
|
||||
info!("config: {:?}", config.sanitized());
|
||||
|
||||
// Spawn monitoring loops
|
||||
monitor::spawn_monitor_loop();
|
||||
helpers::prune::spawn_prune_loop();
|
||||
|
||||
// Setup static frontend services
|
||||
let frontend_path = frontend_path();
|
||||
let frontend_index =
|
||||
ServeFile::new(format!("{frontend_path}/index.html"));
|
||||
let serve_dir = ServeDir::new(frontend_path)
|
||||
.not_found_service(frontend_index.clone());
|
||||
|
||||
let app = Router::new()
|
||||
.nest("/auth", api::auth::router())
|
||||
.nest("/read", api::read::router())
|
||||
.nest("/write", api::write::router())
|
||||
.nest("/execute", api::execute::router())
|
||||
.nest("/listener", listener::router())
|
||||
.nest("/ws", ws::router())
|
||||
.nest_service("/", serve_dir)
|
||||
.fallback_service(frontend_index)
|
||||
.layer(cors()?);
|
||||
|
||||
let socket_addr =
|
||||
SocketAddr::from_str(&format!("0.0.0.0:{}", core_config().port))
|
||||
.context("failed to parse socket addr")?;
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&socket_addr)
|
||||
.await
|
||||
.context("failed to bind to tcp listener")?;
|
||||
|
||||
info!("monitor core listening on {socket_addr}");
|
||||
|
||||
axum::serve(listener, app).await.context("server crashed")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let term_signal = immediate_term_handle()?;
|
||||
|
||||
let app = tokio::spawn(app());
|
||||
|
||||
tokio::select! {
|
||||
res = app => return res?,
|
||||
_ = term_signal => {},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cors() -> anyhow::Result<CorsLayer> {
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any);
|
||||
Ok(cors)
|
||||
}
|
||||
70
bin/core/src/monitor/alert/deployment.rs
Normal file
70
bin/core/src/monitor/alert/deployment.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use monitor_client::entities::{
|
||||
alert::{Alert, AlertData, AlertDataVariant},
|
||||
deployment::Deployment,
|
||||
server::stats::SeverityLevel,
|
||||
update::ResourceTarget,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
helpers::{alert::send_alerts, resource::StateResource},
|
||||
monitor::deployment_status_cache,
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn alert_deployments(
|
||||
ts: i64,
|
||||
server_names: HashMap<String, String>,
|
||||
) {
|
||||
let mut alerts = Vec::<Alert>::new();
|
||||
for v in deployment_status_cache().get_list().await {
|
||||
if v.prev.is_none() {
|
||||
continue;
|
||||
}
|
||||
let prev = v.prev.as_ref().unwrap().to_owned();
|
||||
if v.curr.state != prev {
|
||||
// send alert
|
||||
let d = Deployment::get_resource(&v.curr.id).await;
|
||||
if let Err(e) = d {
|
||||
error!("failed to get deployment from db | {e:#?}");
|
||||
continue;
|
||||
}
|
||||
let d = d.unwrap();
|
||||
let target: ResourceTarget = (&d).into();
|
||||
let data = AlertData::ContainerStateChange {
|
||||
id: v.curr.id.clone(),
|
||||
name: d.name,
|
||||
server_name: server_names
|
||||
.get(&d.config.server_id)
|
||||
.cloned()
|
||||
.unwrap_or(String::from("unknown")),
|
||||
server_id: d.config.server_id,
|
||||
from: prev,
|
||||
to: v.curr.state,
|
||||
};
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
level: SeverityLevel::Warning,
|
||||
variant: AlertDataVariant::ContainerStateChange,
|
||||
resolved: true,
|
||||
resolved_ts: ts.into(),
|
||||
target,
|
||||
data,
|
||||
ts,
|
||||
};
|
||||
if d.config.send_alerts {
|
||||
alerts.push(alert);
|
||||
}
|
||||
}
|
||||
}
|
||||
if alerts.is_empty() {
|
||||
return;
|
||||
}
|
||||
send_alerts(&alerts).await;
|
||||
let res = db_client().await.alerts.insert_many(alerts, None).await;
|
||||
if let Err(e) = res {
|
||||
error!("failed to record deployment status alerts to db | {e:#}");
|
||||
}
|
||||
}
|
||||
59
bin/core/src/monitor/alert/mod.rs
Normal file
59
bin/core/src/monitor/alert/mod.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
mod deployment;
|
||||
mod server;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Context;
|
||||
use monitor_client::entities::{
|
||||
resource::ResourceQuery,
|
||||
server::{Server, ServerListItem},
|
||||
user::User,
|
||||
};
|
||||
|
||||
use crate::helpers::resource::StateResource;
|
||||
|
||||
// called after cache update
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn check_alerts(ts: i64) {
|
||||
let servers = get_all_servers_map().await;
|
||||
|
||||
if let Err(e) = servers {
|
||||
error!("{e:#?}");
|
||||
return;
|
||||
}
|
||||
|
||||
let (servers, server_names) = servers.unwrap();
|
||||
|
||||
tokio::join!(
|
||||
server::alert_servers(ts, servers),
|
||||
deployment::alert_deployments(ts, server_names)
|
||||
);
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn get_all_servers_map() -> anyhow::Result<(
|
||||
HashMap<String, ServerListItem>,
|
||||
HashMap<String, String>,
|
||||
)> {
|
||||
let servers = Server::list_resource_list_items_for_user(
|
||||
ResourceQuery::default(),
|
||||
&User {
|
||||
admin: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("failed to get servers from db (in alert_servers)")?;
|
||||
|
||||
let servers = servers
|
||||
.into_iter()
|
||||
.map(|server| (server.id.clone(), server))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let server_names = servers
|
||||
.iter()
|
||||
.map(|(id, server)| (id.clone(), server.name.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
Ok((servers, server_names))
|
||||
}
|
||||
501
bin/core/src/monitor/alert/server.rs
Normal file
501
bin/core/src/monitor/alert/server.rs
Normal file
@@ -0,0 +1,501 @@
|
||||
use std::{collections::HashMap, path::PathBuf, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use mongo_indexed::Indexed;
|
||||
use monitor_client::entities::{
|
||||
alert::{Alert, AlertData, AlertDataVariant},
|
||||
monitor_timestamp, optional_string,
|
||||
server::{stats::SeverityLevel, ServerListItem, ServerStatus},
|
||||
update::ResourceTarget,
|
||||
};
|
||||
use mungos::{
|
||||
bulk_update::{self, BulkUpdate},
|
||||
find::find_collect,
|
||||
mongodb::bson::{doc, oid::ObjectId, to_bson},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
helpers::alert::send_alerts,
|
||||
state::{db_client, server_status_cache},
|
||||
};
|
||||
|
||||
type SendAlerts = bool;
|
||||
type OpenAlertMap<T = AlertDataVariant> =
|
||||
HashMap<ResourceTarget, HashMap<T, Alert>>;
|
||||
type OpenDiskAlertMap = OpenAlertMap<PathBuf>;
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn alert_servers(
|
||||
ts: i64,
|
||||
mut servers: HashMap<String, ServerListItem>,
|
||||
) {
|
||||
let server_statuses = server_status_cache().get_list().await;
|
||||
|
||||
let (alerts, disk_alerts) = match get_open_alerts().await {
|
||||
Ok(alerts) => alerts,
|
||||
Err(e) => {
|
||||
error!("{e:#}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut alerts_to_open = Vec::<(Alert, SendAlerts)>::new();
|
||||
let mut alerts_to_update = Vec::<(Alert, SendAlerts)>::new();
|
||||
let mut alert_ids_to_close = Vec::<(String, SendAlerts)>::new();
|
||||
|
||||
for server_status in server_statuses {
|
||||
let Some(server) = servers.remove(&server_status.id) else {
|
||||
continue;
|
||||
};
|
||||
let server_alerts =
|
||||
alerts.get(&ResourceTarget::Server(server_status.id.clone()));
|
||||
|
||||
// ===================
|
||||
// SERVER HEALTH
|
||||
// ===================
|
||||
let health_alert = server_alerts.as_ref().and_then(|alerts| {
|
||||
alerts.get(&AlertDataVariant::ServerUnreachable)
|
||||
});
|
||||
match (server_status.status, health_alert) {
|
||||
(ServerStatus::NotOk, None) => {
|
||||
// open unreachable alert
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: false,
|
||||
resolved_ts: None,
|
||||
level: SeverityLevel::Critical,
|
||||
target: ResourceTarget::Server(server_status.id.clone()),
|
||||
variant: AlertDataVariant::ServerUnreachable,
|
||||
data: AlertData::ServerUnreachable {
|
||||
id: server_status.id.clone(),
|
||||
name: server.name.clone(),
|
||||
region: optional_string(&server.info.region),
|
||||
err: server_status.err.clone(),
|
||||
},
|
||||
};
|
||||
alerts_to_open
|
||||
.push((alert, server.info.send_unreachable_alerts))
|
||||
}
|
||||
(ServerStatus::NotOk, Some(alert)) => {
|
||||
// update alert err
|
||||
let mut alert = alert.clone();
|
||||
let (id, name, region) = match alert.data {
|
||||
AlertData::ServerUnreachable {
|
||||
id, name, region, ..
|
||||
} => (id, name, region),
|
||||
data => {
|
||||
error!("got incorrect alert data in ServerStatus handler. got {data:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
alert.data = AlertData::ServerUnreachable {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
err: server_status.err.clone(),
|
||||
};
|
||||
|
||||
// Never send this alert, severity is always 'Critical'
|
||||
alerts_to_update.push((alert, false));
|
||||
}
|
||||
|
||||
// Close an open alert
|
||||
(
|
||||
ServerStatus::Ok | ServerStatus::Disabled,
|
||||
Some(health_alert),
|
||||
) => alert_ids_to_close.push((
|
||||
health_alert.id.clone(),
|
||||
server.info.send_unreachable_alerts,
|
||||
)),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let Some(health) = &server_status.health else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// ===================
|
||||
// SERVER CPU
|
||||
// ===================
|
||||
let cpu_alert = server_alerts
|
||||
.as_ref()
|
||||
.and_then(|alerts| alerts.get(&AlertDataVariant::ServerCpu))
|
||||
.cloned();
|
||||
match (health.cpu, cpu_alert) {
|
||||
(SeverityLevel::Warning | SeverityLevel::Critical, None) => {
|
||||
// open alert
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: false,
|
||||
resolved_ts: None,
|
||||
level: health.cpu,
|
||||
target: ResourceTarget::Server(server_status.id.clone()),
|
||||
variant: AlertDataVariant::ServerCpu,
|
||||
data: AlertData::ServerCpu {
|
||||
id: server_status.id.clone(),
|
||||
name: server.name.clone(),
|
||||
region: optional_string(&server.info.region),
|
||||
percentage: server_status
|
||||
.stats
|
||||
.as_ref()
|
||||
.map(|s| s.cpu_perc as f64)
|
||||
.unwrap_or(0.0),
|
||||
},
|
||||
};
|
||||
alerts_to_open.push((alert, server.info.send_cpu_alerts));
|
||||
}
|
||||
(
|
||||
SeverityLevel::Warning | SeverityLevel::Critical,
|
||||
Some(mut alert),
|
||||
) => {
|
||||
// modify alert level
|
||||
if alert.level != health.cpu {
|
||||
alert.level = health.cpu;
|
||||
alert.data = AlertData::ServerCpu {
|
||||
id: server_status.id.clone(),
|
||||
name: server.name.clone(),
|
||||
region: optional_string(&server.info.region),
|
||||
percentage: server_status
|
||||
.stats
|
||||
.as_ref()
|
||||
.map(|s| s.cpu_perc as f64)
|
||||
.unwrap_or(0.0),
|
||||
};
|
||||
alerts_to_update.push((alert, server.info.send_cpu_alerts));
|
||||
}
|
||||
}
|
||||
(SeverityLevel::Ok, Some(alert)) => alert_ids_to_close
|
||||
.push((alert.id.clone(), server.info.send_cpu_alerts)),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// ===================
|
||||
// SERVER MEM
|
||||
// ===================
|
||||
let mem_alert = server_alerts
|
||||
.as_ref()
|
||||
.and_then(|alerts| alerts.get(&AlertDataVariant::ServerMem))
|
||||
.cloned();
|
||||
match (health.mem, mem_alert) {
|
||||
(SeverityLevel::Warning | SeverityLevel::Critical, None) => {
|
||||
// open alert
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: false,
|
||||
resolved_ts: None,
|
||||
level: health.cpu,
|
||||
target: ResourceTarget::Server(server_status.id.clone()),
|
||||
variant: AlertDataVariant::ServerMem,
|
||||
data: AlertData::ServerMem {
|
||||
id: server_status.id.clone(),
|
||||
name: server.name.clone(),
|
||||
region: optional_string(&server.info.region),
|
||||
total_gb: server_status
|
||||
.stats
|
||||
.as_ref()
|
||||
.map(|s| s.mem_total_gb)
|
||||
.unwrap_or(0.0),
|
||||
used_gb: server_status
|
||||
.stats
|
||||
.as_ref()
|
||||
.map(|s| s.mem_used_gb)
|
||||
.unwrap_or(0.0),
|
||||
},
|
||||
};
|
||||
alerts_to_open.push((alert, server.info.send_mem_alerts));
|
||||
}
|
||||
(
|
||||
SeverityLevel::Warning | SeverityLevel::Critical,
|
||||
Some(mut alert),
|
||||
) => {
|
||||
if alert.level != health.mem {
|
||||
alert.level = health.mem;
|
||||
alert.data = AlertData::ServerMem {
|
||||
id: server_status.id.clone(),
|
||||
name: server.name.clone(),
|
||||
region: optional_string(&server.info.region),
|
||||
total_gb: server_status
|
||||
.stats
|
||||
.as_ref()
|
||||
.map(|s| s.mem_total_gb)
|
||||
.unwrap_or(0.0),
|
||||
used_gb: server_status
|
||||
.stats
|
||||
.as_ref()
|
||||
.map(|s| s.mem_used_gb)
|
||||
.unwrap_or(0.0),
|
||||
};
|
||||
alerts_to_update.push((alert, server.info.send_mem_alerts));
|
||||
}
|
||||
}
|
||||
(SeverityLevel::Ok, Some(alert)) => alert_ids_to_close
|
||||
.push((alert.id.clone(), server.info.send_mem_alerts)),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// ===================
|
||||
// SERVER DISK
|
||||
// ===================
|
||||
|
||||
let server_disk_alerts = disk_alerts
|
||||
.get(&ResourceTarget::Server(server_status.id.clone()));
|
||||
|
||||
for (path, health) in &health.disks {
|
||||
let disk_alert = server_disk_alerts
|
||||
.as_ref()
|
||||
.and_then(|alerts| alerts.get(path))
|
||||
.cloned();
|
||||
match (*health, disk_alert) {
|
||||
(SeverityLevel::Warning | SeverityLevel::Critical, None) => {
|
||||
let disk = server_status.stats.as_ref().and_then(|stats| {
|
||||
stats.disks.iter().find(|disk| disk.mount == *path)
|
||||
});
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: false,
|
||||
resolved_ts: None,
|
||||
level: *health,
|
||||
target: ResourceTarget::Server(server_status.id.clone()),
|
||||
variant: AlertDataVariant::ServerDisk,
|
||||
data: AlertData::ServerDisk {
|
||||
id: server_status.id.clone(),
|
||||
name: server.name.clone(),
|
||||
region: optional_string(&server.info.region),
|
||||
path: path.to_owned(),
|
||||
total_gb: disk.map(|d| d.total_gb).unwrap_or_default(),
|
||||
used_gb: disk.map(|d| d.used_gb).unwrap_or_default(),
|
||||
},
|
||||
};
|
||||
alerts_to_open.push((alert, server.info.send_disk_alerts));
|
||||
}
|
||||
(
|
||||
SeverityLevel::Warning | SeverityLevel::Critical,
|
||||
Some(mut alert),
|
||||
) => {
|
||||
if *health != alert.level {
|
||||
let disk =
|
||||
server_status.stats.as_ref().and_then(|stats| {
|
||||
stats.disks.iter().find(|disk| disk.mount == *path)
|
||||
});
|
||||
alert.level = *health;
|
||||
alert.data = AlertData::ServerDisk {
|
||||
id: server_status.id.clone(),
|
||||
name: server.name.clone(),
|
||||
region: optional_string(&server.info.region),
|
||||
path: path.to_owned(),
|
||||
total_gb: disk.map(|d| d.total_gb).unwrap_or_default(),
|
||||
used_gb: disk.map(|d| d.used_gb).unwrap_or_default(),
|
||||
};
|
||||
alerts_to_update
|
||||
.push((alert, server.info.send_disk_alerts));
|
||||
}
|
||||
}
|
||||
(SeverityLevel::Ok, Some(alert)) => alert_ids_to_close
|
||||
.push((alert.id.clone(), server.info.send_disk_alerts)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokio::join!(
|
||||
open_alerts(&alerts_to_open),
|
||||
update_alerts(&alerts_to_update),
|
||||
resolve_alerts(&alert_ids_to_close),
|
||||
);
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn open_alerts(alerts: &[(Alert, SendAlerts)]) {
|
||||
if alerts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let db = db_client().await;
|
||||
|
||||
let open = || async {
|
||||
let ids = db
|
||||
.alerts
|
||||
.insert_many(alerts.iter().map(|(alert, _)| alert), None)
|
||||
.await?
|
||||
.inserted_ids
|
||||
.into_iter()
|
||||
.filter_map(|(index, id)| {
|
||||
alerts.get(index)?.1.then(|| id.as_object_id())
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
anyhow::Ok(ids)
|
||||
};
|
||||
|
||||
let ids_to_send = match open().await {
|
||||
Ok(ids) => ids,
|
||||
Err(e) => {
|
||||
error!("failed to open alerts on db | {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let alerts = match find_collect(
|
||||
&db.alerts,
|
||||
doc! { "_id": { "$in": ids_to_send } },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(alerts) => alerts,
|
||||
Err(e) => {
|
||||
error!("failed to pull created alerts from mongo | {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
send_alerts(&alerts).await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn update_alerts(alerts: &[(Alert, SendAlerts)]) {
|
||||
if alerts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let open = || async {
|
||||
let updates = alerts.iter().map(|(alert, _)| {
|
||||
let update = BulkUpdate {
|
||||
query: doc! { "_id": ObjectId::from_str(&alert.id).context("failed to convert alert id to ObjectId")? },
|
||||
update: doc! { "$set": to_bson(alert).context("failed to convert alert to bson")? }
|
||||
};
|
||||
anyhow::Ok(update)
|
||||
})
|
||||
.filter_map(|update| match update {
|
||||
Ok(update) => Some(update),
|
||||
Err(e) => {
|
||||
warn!("failed to generate bulk update for alert | {e:#}");
|
||||
None
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
bulk_update::bulk_update(
|
||||
&db_client().await.db,
|
||||
Alert::default_collection_name(),
|
||||
&updates,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.context("failed to bulk update alerts")?;
|
||||
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let alerts = alerts
|
||||
.iter()
|
||||
.filter(|(_, send)| *send)
|
||||
.map(|(alert, _)| alert)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (res, _) = tokio::join!(open(), send_alerts(&alerts));
|
||||
|
||||
if let Err(e) = res {
|
||||
error!("failed to create alerts on db | {e:#}");
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn resolve_alerts(alert_ids: &[(String, SendAlerts)]) {
|
||||
if alert_ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let send_alerts_map =
|
||||
alert_ids.iter().cloned().collect::<HashMap<_, _>>();
|
||||
|
||||
let close = || async {
|
||||
let alert_ids = alert_ids
|
||||
.iter()
|
||||
.map(|(id, _)| {
|
||||
ObjectId::from_str(id)
|
||||
.context("failed to convert alert id to ObjectId")
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
db_client()
|
||||
.await
|
||||
.alerts
|
||||
.update_many(
|
||||
doc! { "_id": { "$in": &alert_ids } },
|
||||
doc! {
|
||||
"$set": {
|
||||
"resolved": true,
|
||||
"resolved_ts": monitor_timestamp()
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to resolve alerts on db")?;
|
||||
let mut closed = find_collect(
|
||||
&db_client().await.alerts,
|
||||
doc! { "_id": { "$in": &alert_ids } },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to get closed alerts from db")?;
|
||||
|
||||
for closed in &mut closed {
|
||||
closed.level = SeverityLevel::Ok;
|
||||
}
|
||||
|
||||
let closed = closed
|
||||
.into_iter()
|
||||
.filter(|closed| {
|
||||
if let ResourceTarget::Server(id) = &closed.target {
|
||||
send_alerts_map.get(id).cloned().unwrap_or(true)
|
||||
} else {
|
||||
error!("got resource target other than server in resolve_server_alerts");
|
||||
true
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
send_alerts(&closed).await;
|
||||
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
if let Err(e) = close().await {
|
||||
error!("failed to resolve alerts | {e:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn get_open_alerts(
|
||||
) -> anyhow::Result<(OpenAlertMap, OpenDiskAlertMap)> {
|
||||
let alerts = find_collect(
|
||||
&db_client().await.alerts,
|
||||
doc! { "resolved": false },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to get open alerts from db")?;
|
||||
|
||||
let mut map = OpenAlertMap::new();
|
||||
let mut disk_map = OpenDiskAlertMap::new();
|
||||
|
||||
for alert in alerts {
|
||||
match &alert.data {
|
||||
AlertData::ServerDisk { path, .. } => {
|
||||
let inner = disk_map.entry(alert.target.clone()).or_default();
|
||||
inner.insert(path.to_owned(), alert);
|
||||
}
|
||||
_ => {
|
||||
let inner = map.entry(alert.target.clone()).or_default();
|
||||
inner.insert(alert.variant, alert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((map, disk_map))
|
||||
}
|
||||
118
bin/core/src/monitor/helpers.rs
Normal file
118
bin/core/src/monitor/helpers.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use monitor_client::entities::{
|
||||
deployment::{Deployment, DockerContainerState},
|
||||
server::{
|
||||
stats::{
|
||||
ServerHealth, SeverityLevel, SingleDiskUsage, SystemStats,
|
||||
},
|
||||
Server, ServerConfig, ServerStatus,
|
||||
},
|
||||
};
|
||||
use serror::Serror;
|
||||
|
||||
use crate::state::{deployment_status_cache, server_status_cache};
|
||||
|
||||
use super::{CachedDeploymentStatus, CachedServerStatus, History};
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub async fn insert_deployments_status_unknown(
|
||||
deployments: Vec<Deployment>,
|
||||
) {
|
||||
let status_cache = deployment_status_cache();
|
||||
for deployment in deployments {
|
||||
let prev =
|
||||
status_cache.get(&deployment.id).await.map(|s| s.curr.state);
|
||||
status_cache
|
||||
.insert(
|
||||
deployment.id.clone(),
|
||||
History {
|
||||
curr: CachedDeploymentStatus {
|
||||
id: deployment.id,
|
||||
state: DockerContainerState::Unknown,
|
||||
container: None,
|
||||
},
|
||||
prev,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub async fn insert_server_status(
|
||||
server: &Server,
|
||||
status: ServerStatus,
|
||||
version: String,
|
||||
stats: Option<SystemStats>,
|
||||
err: impl Into<Option<Serror>>,
|
||||
) {
|
||||
let health = stats.as_ref().map(|s| get_server_health(server, s));
|
||||
server_status_cache()
|
||||
.insert(
|
||||
server.id.clone(),
|
||||
CachedServerStatus {
|
||||
id: server.id.clone(),
|
||||
status,
|
||||
version,
|
||||
stats,
|
||||
health,
|
||||
err: err.into(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn get_server_health(
|
||||
server: &Server,
|
||||
SystemStats {
|
||||
cpu_perc,
|
||||
mem_used_gb,
|
||||
mem_total_gb,
|
||||
disks,
|
||||
..
|
||||
}: &SystemStats,
|
||||
) -> ServerHealth {
|
||||
let ServerConfig {
|
||||
cpu_warning,
|
||||
cpu_critical,
|
||||
mem_warning,
|
||||
mem_critical,
|
||||
disk_warning,
|
||||
disk_critical,
|
||||
..
|
||||
} = &server.config;
|
||||
let mut health = ServerHealth::default();
|
||||
|
||||
if cpu_perc >= cpu_critical {
|
||||
health.cpu = SeverityLevel::Critical
|
||||
} else if cpu_perc >= cpu_warning {
|
||||
health.cpu = SeverityLevel::Warning
|
||||
}
|
||||
|
||||
let mem_perc = 100.0 * mem_used_gb / mem_total_gb;
|
||||
if mem_perc >= *mem_critical {
|
||||
health.mem = SeverityLevel::Critical
|
||||
} else if mem_perc >= *mem_warning {
|
||||
health.mem = SeverityLevel::Warning
|
||||
}
|
||||
|
||||
for SingleDiskUsage {
|
||||
mount,
|
||||
used_gb,
|
||||
total_gb,
|
||||
} in disks
|
||||
{
|
||||
let perc = 100.0 * used_gb / total_gb;
|
||||
let stats_state = if perc >= *disk_critical {
|
||||
SeverityLevel::Critical
|
||||
} else if perc >= *disk_warning {
|
||||
SeverityLevel::Warning
|
||||
} else {
|
||||
SeverityLevel::Ok
|
||||
};
|
||||
health.disks.insert(mount.clone(), stats_state);
|
||||
}
|
||||
|
||||
health
|
||||
}
|
||||
189
bin/core/src/monitor/mod.rs
Normal file
189
bin/core/src/monitor/mod.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
use async_timing_util::{wait_until_timelength, Timelength};
|
||||
use futures::future::join_all;
|
||||
use monitor_client::entities::{
|
||||
deployment::{ContainerSummary, DockerContainerState},
|
||||
server::{
|
||||
stats::{ServerHealth, SystemStats},
|
||||
Server, ServerStatus,
|
||||
},
|
||||
};
|
||||
use mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use periphery_client::api;
|
||||
use serror::Serror;
|
||||
|
||||
use crate::{
|
||||
helpers::periphery_client,
|
||||
monitor::{alert::check_alerts, record::record_server_stats},
|
||||
state::{db_client, deployment_status_cache},
|
||||
};
|
||||
|
||||
use self::helpers::{
|
||||
insert_deployments_status_unknown, insert_server_status,
|
||||
};
|
||||
|
||||
mod alert;
|
||||
mod helpers;
|
||||
mod record;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct History<Curr: Default, Prev> {
|
||||
pub curr: Curr,
|
||||
pub prev: Option<Prev>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct CachedServerStatus {
|
||||
pub id: String,
|
||||
pub status: ServerStatus,
|
||||
pub version: String,
|
||||
pub stats: Option<SystemStats>,
|
||||
pub health: Option<ServerHealth>,
|
||||
pub err: Option<serror::Serror>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct CachedDeploymentStatus {
|
||||
pub id: String,
|
||||
pub state: DockerContainerState,
|
||||
pub container: Option<ContainerSummary>,
|
||||
}
|
||||
|
||||
pub fn spawn_monitor_loop() {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let ts = (wait_until_timelength(Timelength::FiveSeconds, 500)
|
||||
.await
|
||||
- 500) as i64;
|
||||
let servers =
|
||||
match find_collect(&db_client().await.servers, None, None)
|
||||
.await
|
||||
{
|
||||
Ok(servers) => servers,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"failed to get server list (manage status cache) | {e:#}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let futures = servers.into_iter().map(|server| async move {
|
||||
update_cache_for_server(&server).await;
|
||||
});
|
||||
join_all(futures).await;
|
||||
tokio::join!(check_alerts(ts), record_server_stats(ts));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn update_cache_for_server(server: &Server) {
|
||||
let deployments = match find_collect(
|
||||
&db_client().await.deployments,
|
||||
doc! { "config.server_id": &server.id },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(deployments) => deployments,
|
||||
Err(e) => {
|
||||
error!("failed to get deployments list from mongo (update status cache) | server id: {} | {e:#}", server.id);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !server.config.enabled {
|
||||
insert_deployments_status_unknown(deployments).await;
|
||||
insert_server_status(
|
||||
server,
|
||||
ServerStatus::Disabled,
|
||||
String::from("unknown"),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
// already handle server disabled case above, so using unwrap here
|
||||
let periphery = periphery_client(server).unwrap();
|
||||
|
||||
let version = match periphery.request(api::GetVersion {}).await {
|
||||
Ok(version) => version.version,
|
||||
Err(e) => {
|
||||
insert_deployments_status_unknown(deployments).await;
|
||||
insert_server_status(
|
||||
server,
|
||||
ServerStatus::NotOk,
|
||||
String::from("unknown"),
|
||||
None,
|
||||
Serror::from(&e),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let stats = if server.config.stats_monitoring {
|
||||
match periphery.request(api::stats::GetSystemStats {}).await {
|
||||
Ok(stats) => Some(stats),
|
||||
Err(e) => {
|
||||
insert_deployments_status_unknown(deployments).await;
|
||||
insert_server_status(
|
||||
server,
|
||||
ServerStatus::NotOk,
|
||||
String::from("unknown"),
|
||||
None,
|
||||
Serror::from(&e),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
insert_server_status(
|
||||
server,
|
||||
ServerStatus::Ok,
|
||||
version,
|
||||
stats,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let containers =
|
||||
periphery.request(api::container::GetContainerList {}).await;
|
||||
if containers.is_err() {
|
||||
insert_deployments_status_unknown(deployments).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let containers = containers.unwrap();
|
||||
let status_cache = deployment_status_cache();
|
||||
for deployment in deployments {
|
||||
let container = containers
|
||||
.iter()
|
||||
.find(|c| c.name == deployment.name)
|
||||
.cloned();
|
||||
let prev =
|
||||
status_cache.get(&deployment.id).await.map(|s| s.curr.state);
|
||||
let state = container
|
||||
.as_ref()
|
||||
.map(|c| c.state)
|
||||
.unwrap_or(DockerContainerState::NotDeployed);
|
||||
status_cache
|
||||
.insert(
|
||||
deployment.id.clone(),
|
||||
History {
|
||||
curr: CachedDeploymentStatus {
|
||||
id: deployment.id,
|
||||
state,
|
||||
container,
|
||||
},
|
||||
prev,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
39
bin/core/src/monitor/record.rs
Normal file
39
bin/core/src/monitor/record.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use monitor_client::entities::server::stats::{
|
||||
sum_disk_usage, SystemStatsRecord, TotalDiskUsage,
|
||||
};
|
||||
|
||||
use crate::state::{db_client, server_status_cache};
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn record_server_stats(ts: i64) {
|
||||
let status = server_status_cache().get_list().await;
|
||||
let records = status
|
||||
.into_iter()
|
||||
.filter_map(|status| {
|
||||
let stats = status.stats.as_ref()?;
|
||||
|
||||
let TotalDiskUsage {
|
||||
used_gb: disk_used_gb,
|
||||
total_gb: disk_total_gb,
|
||||
} = sum_disk_usage(&stats.disks);
|
||||
|
||||
Some(SystemStatsRecord {
|
||||
ts,
|
||||
sid: status.id.clone(),
|
||||
cpu_perc: stats.cpu_perc,
|
||||
mem_total_gb: stats.mem_total_gb,
|
||||
mem_used_gb: stats.mem_used_gb,
|
||||
disk_total_gb,
|
||||
disk_used_gb,
|
||||
disks: stats.disks.clone(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !records.is_empty() {
|
||||
let res =
|
||||
db_client().await.stats.insert_many(records, None).await;
|
||||
if let Err(e) = res {
|
||||
error!("failed to record server stats | {e:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
54
bin/core/src/state.rs
Normal file
54
bin/core/src/state.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use monitor_client::entities::deployment::DockerContainerState;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use crate::{
|
||||
auth::jwt::JwtClient,
|
||||
config::core_config,
|
||||
db::DbClient,
|
||||
helpers::{action_state::ActionStates, cache::Cache},
|
||||
monitor::{CachedDeploymentStatus, CachedServerStatus, History},
|
||||
};
|
||||
|
||||
pub struct State;
|
||||
|
||||
pub async fn db_client() -> &'static DbClient {
|
||||
static DB_CLIENT: OnceCell<DbClient> = OnceCell::const_new();
|
||||
DB_CLIENT
|
||||
.get_or_init(|| async {
|
||||
DbClient::new(&core_config().mongo)
|
||||
.await
|
||||
.expect("failed to initialize mongo client")
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn jwt_client() -> &'static JwtClient {
|
||||
static JWT_CLIENT: OnceLock<JwtClient> = OnceLock::new();
|
||||
JWT_CLIENT.get_or_init(|| JwtClient::new(core_config()))
|
||||
}
|
||||
|
||||
pub fn action_states() -> &'static ActionStates {
|
||||
static ACTION_STATES: OnceLock<ActionStates> = OnceLock::new();
|
||||
ACTION_STATES.get_or_init(ActionStates::default)
|
||||
}
|
||||
|
||||
pub type DeploymentStatusCache = Cache<
|
||||
String,
|
||||
Arc<History<CachedDeploymentStatus, DockerContainerState>>,
|
||||
>;
|
||||
|
||||
pub fn deployment_status_cache() -> &'static DeploymentStatusCache {
|
||||
static DEPLOYMENT_STATUS_CACHE: OnceLock<DeploymentStatusCache> =
|
||||
OnceLock::new();
|
||||
DEPLOYMENT_STATUS_CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub type ServerStatusCache = Cache<String, Arc<CachedServerStatus>>;
|
||||
|
||||
pub fn server_status_cache() -> &'static ServerStatusCache {
|
||||
static SERVER_STATUS_CACHE: OnceLock<ServerStatusCache> =
|
||||
OnceLock::new();
|
||||
SERVER_STATUS_CACHE.get_or_init(Default::default)
|
||||
}
|
||||
218
bin/core/src/ws.rs
Normal file
218
bin/core/src/ws.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{
|
||||
extract::{
|
||||
ws::{Message, WebSocket},
|
||||
WebSocketUpgrade,
|
||||
},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use monitor_client::{
|
||||
entities::{
|
||||
permission::PermissionLevel, update::ResourceTarget, user::User,
|
||||
},
|
||||
ws::WsLoginMessage,
|
||||
};
|
||||
use mungos::by_id::find_one_by_id;
|
||||
use serde_json::json;
|
||||
use serror::serialize_error;
|
||||
use tokio::select;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
auth::{auth_api_key_check_enabled, auth_jwt_check_enabled},
|
||||
db::DbClient,
|
||||
helpers::{
|
||||
channel::update_channel,
|
||||
resource::get_user_permission_on_resource,
|
||||
},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new().route("/update", get(ws_handler))
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
|
||||
// get a reveiver for internal update messages.
|
||||
let mut receiver = update_channel().receiver.resubscribe();
|
||||
|
||||
// handle http -> ws updgrade
|
||||
ws.on_upgrade(|socket| async move {
|
||||
let Some((socket, user)) = ws_login(socket).await else {
|
||||
return
|
||||
};
|
||||
|
||||
let (mut ws_sender, mut ws_reciever) = socket.split();
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
let cancel_clone = cancel.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let db_client = db_client().await;
|
||||
loop {
|
||||
// poll for updates off the receiver / await cancel.
|
||||
let update = select! {
|
||||
_ = cancel_clone.cancelled() => break,
|
||||
update = receiver.recv() => {update.expect("failed to recv update msg")}
|
||||
};
|
||||
|
||||
// before sending every update, verify user is still valid.
|
||||
// kill the connection is user if found to be invalid.
|
||||
let user = check_user_valid(db_client, &user.id).await;
|
||||
let user = match user {
|
||||
Err(e) => {
|
||||
let _ = ws_sender
|
||||
.send(Message::Text(json!({ "type": "INVALID_USER", "msg": serialize_error(&e) }).to_string()))
|
||||
.await;
|
||||
let _ = ws_sender.close().await;
|
||||
return;
|
||||
},
|
||||
Ok(user) => user,
|
||||
};
|
||||
|
||||
// Only send if user has permission on the target resource.
|
||||
if user_can_see_update(&user, &update.target).await.is_ok() {
|
||||
let _ = ws_sender
|
||||
.send(Message::Text(serde_json::to_string(&update).unwrap()))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle messages from the client.
|
||||
// After login, only handles close message.
|
||||
while let Some(msg) = ws_reciever.next().await {
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
if let Message::Close(_) = msg {
|
||||
cancel.cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
cancel.cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn ws_login(
|
||||
mut socket: WebSocket,
|
||||
) -> Option<(WebSocket, User)> {
|
||||
let login_msg = match socket.recv().await {
|
||||
Some(Ok(Message::Text(login_msg))) => LoginMessage::Ok(login_msg),
|
||||
Some(Ok(msg)) => {
|
||||
LoginMessage::Err(format!("invalid login message: {msg:?}"))
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
LoginMessage::Err(format!("failed to get login message: {e:?}"))
|
||||
}
|
||||
None => {
|
||||
LoginMessage::Err("failed to get login message".to_string())
|
||||
}
|
||||
};
|
||||
let login_msg = match login_msg {
|
||||
LoginMessage::Ok(login_msg) => login_msg,
|
||||
LoginMessage::Err(msg) => {
|
||||
let _ = socket.send(Message::Text(msg)).await;
|
||||
let _ = socket.close().await;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match WsLoginMessage::from_json_str(&login_msg) {
|
||||
// Login using a jwt
|
||||
Ok(WsLoginMessage::Jwt { jwt }) => {
|
||||
match auth_jwt_check_enabled(&jwt).await {
|
||||
Ok(user) => {
|
||||
let _ =
|
||||
socket.send(Message::Text("LOGGED_IN".to_string())).await;
|
||||
Some((socket, user))
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = socket
|
||||
.send(Message::Text(format!(
|
||||
"failed to authenticate user using jwt | {e:#}"
|
||||
)))
|
||||
.await;
|
||||
let _ = socket.close().await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
// login using api keys
|
||||
Ok(WsLoginMessage::ApiKeys { key, secret }) => {
|
||||
match auth_api_key_check_enabled(&key, &secret).await {
|
||||
Ok(user) => {
|
||||
let _ =
|
||||
socket.send(Message::Text("LOGGED_IN".to_string())).await;
|
||||
Some((socket, user))
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = socket
|
||||
.send(Message::Text(format!(
|
||||
"failed to authenticate user using api keys | {e:#}"
|
||||
)))
|
||||
.await;
|
||||
let _ = socket.close().await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = socket
|
||||
.send(Message::Text(format!(
|
||||
"failed to parse login message: {e:#}"
|
||||
)))
|
||||
.await;
|
||||
let _ = socket.close().await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum LoginMessage {
|
||||
/// The text message
|
||||
Ok(String),
|
||||
/// The err message
|
||||
Err(String),
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(db_client))]
|
||||
async fn check_user_valid(
|
||||
db_client: &DbClient,
|
||||
user_id: &str,
|
||||
) -> anyhow::Result<User> {
|
||||
let user = find_one_by_id(&db_client.users, user_id)
|
||||
.await
|
||||
.context("failed to query mongo for users")?
|
||||
.context("user not found")?;
|
||||
if !user.enabled {
|
||||
return Err(anyhow!("user not enabled"));
|
||||
}
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn user_can_see_update(
|
||||
user: &User,
|
||||
update_target: &ResourceTarget,
|
||||
) -> anyhow::Result<()> {
|
||||
if user.admin {
|
||||
return Ok(());
|
||||
}
|
||||
let (variant, id) = update_target.extract_variant_id();
|
||||
let permissions =
|
||||
get_user_permission_on_resource(&user.id, variant, id).await?;
|
||||
if permissions > PermissionLevel::None {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("user does not have permissions on {variant} {id}"))
|
||||
}
|
||||
}
|
||||
25
bin/migrator/Cargo.toml
Normal file
25
bin/migrator/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "migrator"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
monitor_client.workspace = true
|
||||
# db_client.workspace = true
|
||||
logger.workspace = true
|
||||
#
|
||||
termination_signal.workspace = true
|
||||
mungos.workspace = true
|
||||
mongo_indexed.workspace = true
|
||||
#
|
||||
tokio.workspace = true
|
||||
anyhow.workspace = true
|
||||
dotenv.workspace = true
|
||||
envy.workspace = true
|
||||
serde.workspace = true
|
||||
tracing.workspace = true
|
||||
chrono = "0.4"
|
||||
1
bin/migrator/src/legacy/mod.rs
Normal file
1
bin/migrator/src/legacy/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod v0;
|
||||
53
bin/migrator/src/legacy/v0/action.rs
Normal file
53
bin/migrator/src/legacy/v0/action.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Command, PermissionsMap};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct Action {
|
||||
#[serde(
|
||||
default,
|
||||
rename = "_id",
|
||||
skip_serializing_if = "String::is_empty",
|
||||
with = "hex_string_as_object_id"
|
||||
)]
|
||||
pub id: String,
|
||||
|
||||
pub name: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
|
||||
pub path: String,
|
||||
|
||||
pub command: String,
|
||||
|
||||
// run action on all servers in this array
|
||||
#[serde(default)]
|
||||
pub server_ids: Vec<String>,
|
||||
|
||||
// run action on all servers in these groups
|
||||
#[serde(default)]
|
||||
pub group_ids: Vec<String>,
|
||||
|
||||
// run action on all servers
|
||||
#[serde(default)]
|
||||
pub run_on_all: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub permissions: PermissionsMap,
|
||||
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub created_at: String,
|
||||
#[serde(default)]
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<Action> for Command {
|
||||
fn from(value: Action) -> Command {
|
||||
Command {
|
||||
path: value.path,
|
||||
command: value.command,
|
||||
}
|
||||
}
|
||||
}
|
||||
13
bin/migrator/src/legacy/v0/alert.rs
Normal file
13
bin/migrator/src/legacy/v0/alert.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct Alert {
|
||||
#[serde(
|
||||
default,
|
||||
rename = "_id",
|
||||
skip_serializing_if = "String::is_empty",
|
||||
with = "hex_string_as_object_id"
|
||||
)]
|
||||
pub id: String,
|
||||
}
|
||||
234
bin/migrator/src/legacy/v0/build.rs
Normal file
234
bin/migrator/src/legacy/v0/build.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use monitor_client::entities::build::{BuildConfig, BuildInfo};
|
||||
use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
unix_from_monitor_ts, Command, EnvironmentVar, PermissionsMap,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct Build {
|
||||
#[serde(
|
||||
default,
|
||||
rename = "_id",
|
||||
skip_serializing_if = "String::is_empty",
|
||||
with = "hex_string_as_object_id"
|
||||
)]
|
||||
pub id: String,
|
||||
|
||||
pub name: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub permissions: PermissionsMap,
|
||||
|
||||
#[serde(default)]
|
||||
pub skip_secret_interp: bool,
|
||||
|
||||
pub server_id: Option<String>, // server which this image should be built on
|
||||
|
||||
pub aws_config: Option<AwsBuilderBuildConfig>,
|
||||
|
||||
pub version: Version,
|
||||
|
||||
// git related
|
||||
pub repo: Option<String>,
|
||||
|
||||
pub branch: Option<String>,
|
||||
|
||||
pub github_account: Option<String>,
|
||||
|
||||
// build related
|
||||
pub pre_build: Option<Command>,
|
||||
|
||||
pub docker_build_args: Option<DockerBuildArgs>,
|
||||
|
||||
pub docker_account: Option<String>,
|
||||
|
||||
pub docker_organization: Option<String>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub last_built_at: String,
|
||||
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub created_at: String,
|
||||
#[serde(default)]
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct BuildActionState {
|
||||
pub building: bool,
|
||||
pub updating: bool,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Default, PartialEq,
|
||||
)]
|
||||
pub struct Version {
|
||||
pub major: i32,
|
||||
pub minor: i32,
|
||||
pub patch: i32,
|
||||
}
|
||||
|
||||
impl ToString for Version {
|
||||
fn to_string(&self) -> String {
|
||||
format!("{}.{}.{}", self.major, self.minor, self.patch)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Version {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let vals = value
|
||||
.split('.')
|
||||
.map(|v| {
|
||||
anyhow::Ok(
|
||||
v.parse().context("failed at parsing value into i32")?,
|
||||
)
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<i32>>>()?;
|
||||
let version = Version {
|
||||
major: *vals
|
||||
.first()
|
||||
.ok_or(anyhow!("must include at least major version"))?,
|
||||
minor: *vals.get(1).unwrap_or(&0),
|
||||
patch: *vals.get(2).unwrap_or(&0),
|
||||
};
|
||||
Ok(version)
|
||||
}
|
||||
}
|
||||
|
||||
impl Version {
|
||||
pub fn increment(&mut self) {
|
||||
self.patch += 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Version> for monitor_client::entities::Version {
|
||||
fn from(value: Version) -> Self {
|
||||
Self {
|
||||
major: value.major,
|
||||
minor: value.minor,
|
||||
patch: value.patch,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, PartialEq, Default,
|
||||
)]
|
||||
pub struct DockerBuildArgs {
|
||||
pub build_path: String,
|
||||
pub dockerfile_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub build_args: Vec<EnvironmentVar>,
|
||||
#[serde(default)]
|
||||
pub extra_args: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub use_buildx: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct BuildVersionsReponse {
|
||||
pub version: Version,
|
||||
pub ts: String,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, PartialEq, Default,
|
||||
)]
|
||||
pub struct AwsBuilderBuildConfig {
|
||||
pub region: Option<String>,
|
||||
|
||||
pub instance_type: Option<String>,
|
||||
|
||||
pub ami_name: Option<String>,
|
||||
|
||||
pub volume_gb: Option<i32>,
|
||||
|
||||
pub subnet_id: Option<String>,
|
||||
|
||||
pub security_group_ids: Option<Vec<String>>,
|
||||
|
||||
pub key_pair_name: Option<String>,
|
||||
|
||||
pub assign_public_ip: Option<bool>,
|
||||
}
|
||||
|
||||
impl TryFrom<Build> for monitor_client::entities::build::Build {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: Build) -> Result<Self, Self::Error> {
|
||||
let (
|
||||
build_path,
|
||||
dockerfile_path,
|
||||
build_args,
|
||||
extra_args,
|
||||
use_buildx,
|
||||
) = value
|
||||
.docker_build_args
|
||||
.map(|args| {
|
||||
(
|
||||
args.build_path,
|
||||
args.dockerfile_path.unwrap_or_default(),
|
||||
args
|
||||
.build_args
|
||||
.into_iter()
|
||||
.map(|arg| monitor_client::entities::EnvironmentVar {
|
||||
variable: arg.variable,
|
||||
value: arg.value,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
args.extra_args,
|
||||
args.use_buildx,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let build = Self {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
// permissions: value
|
||||
// .permissions
|
||||
// .into_iter()
|
||||
// .map(|(id, p)| (id, p.into()))
|
||||
// .collect(),
|
||||
updated_at: unix_from_monitor_ts(&value.updated_at)?,
|
||||
tags: Vec::new(),
|
||||
info: BuildInfo {
|
||||
last_built_at: unix_from_monitor_ts(&value.last_built_at)?,
|
||||
},
|
||||
config: BuildConfig {
|
||||
builder_id: String::new(),
|
||||
skip_secret_interp: value.skip_secret_interp,
|
||||
version: value.version.into(),
|
||||
repo: value.repo.unwrap_or_default(),
|
||||
branch: value.branch.unwrap_or_default(),
|
||||
github_account: value.github_account.unwrap_or_default(),
|
||||
docker_account: value.docker_account.unwrap_or_default(),
|
||||
docker_organization: value
|
||||
.docker_organization
|
||||
.unwrap_or_default(),
|
||||
pre_build: value
|
||||
.pre_build
|
||||
.map(|command| monitor_client::entities::SystemCommand {
|
||||
path: command.path,
|
||||
command: command.command,
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
build_path,
|
||||
dockerfile_path,
|
||||
build_args,
|
||||
extra_args,
|
||||
use_buildx,
|
||||
labels: Default::default(),
|
||||
},
|
||||
};
|
||||
Ok(build)
|
||||
}
|
||||
}
|
||||
201
bin/migrator/src/legacy/v0/config.rs
Normal file
201
bin/migrator/src/legacy/v0/config.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
use std::{collections::HashMap, net::IpAddr, path::PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::Timelength;
|
||||
|
||||
pub type GithubUsername = String;
|
||||
pub type GithubToken = String;
|
||||
pub type GithubAccounts = HashMap<GithubUsername, GithubToken>;
|
||||
|
||||
pub type DockerUsername = String;
|
||||
pub type DockerToken = String;
|
||||
pub type DockerAccounts = HashMap<DockerUsername, DockerToken>;
|
||||
|
||||
pub type SecretsMap = HashMap<String, String>; // these are used for injection into deployments run commands
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct CoreConfig {
|
||||
#[serde(default = "default_title")]
|
||||
pub title: String,
|
||||
|
||||
// the host to use with oauth redirect url, whatever host the user hits to access monitor. eg 'https://monitor.mogh.tech'
|
||||
pub host: String,
|
||||
|
||||
// port the core web server runs on
|
||||
#[serde(default = "default_core_port")]
|
||||
pub port: u16,
|
||||
|
||||
pub jwt_secret: String,
|
||||
|
||||
#[serde(default = "default_jwt_valid_for")]
|
||||
pub jwt_valid_for: Timelength,
|
||||
|
||||
// interval at which to collect server stats and alert for out of bounds
|
||||
pub monitoring_interval: Timelength,
|
||||
|
||||
// daily utc offset in hours to run daily update. eg 8:00 eastern time is 13:00 UTC, so offset should be 13. default of 0 runs at UTC midnight.
|
||||
#[serde(default)]
|
||||
pub daily_offset_hours: u8,
|
||||
|
||||
// number of days to keep stats, or 0 to disable pruning. stats older than this number of days are deleted on a daily cycle
|
||||
#[serde(default)]
|
||||
pub keep_stats_for_days: u64,
|
||||
|
||||
// used to verify validity from github webhooks
|
||||
pub github_webhook_secret: String,
|
||||
|
||||
// used to form the frontend listener url, if None will use 'host'.
|
||||
pub github_webhook_base_url: Option<String>,
|
||||
|
||||
// sent in auth header with req to periphery
|
||||
pub passkey: String,
|
||||
|
||||
// integration with slack app
|
||||
pub slack_url: Option<String>,
|
||||
|
||||
// enable login with local auth
|
||||
pub local_auth: bool,
|
||||
|
||||
// allowed docker orgs used with monitor. first in this list will be default for build
|
||||
#[serde(default)]
|
||||
pub docker_organizations: Vec<String>,
|
||||
|
||||
pub mongo: MongoConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub github_oauth: OauthCredentials,
|
||||
|
||||
#[serde(default)]
|
||||
pub google_oauth: OauthCredentials,
|
||||
|
||||
#[serde(default)]
|
||||
pub aws: AwsBuilderConfig,
|
||||
}
|
||||
|
||||
fn default_title() -> String {
|
||||
String::from("monitor")
|
||||
}
|
||||
|
||||
fn default_core_port() -> u16 {
|
||||
9000
|
||||
}
|
||||
|
||||
fn default_jwt_valid_for() -> Timelength {
|
||||
Timelength::OneWeek
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct OauthCredentials {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct MongoConfig {
|
||||
pub uri: String,
|
||||
#[serde(default = "default_core_mongo_app_name")]
|
||||
pub app_name: String,
|
||||
#[serde(default = "default_core_mongo_db_name")]
|
||||
pub db_name: String,
|
||||
}
|
||||
|
||||
fn default_core_mongo_app_name() -> String {
|
||||
"monitor_core".to_string()
|
||||
}
|
||||
|
||||
fn default_core_mongo_db_name() -> String {
|
||||
"monitor".to_string()
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct AwsBuilderConfig {
|
||||
#[serde(skip_serializing)]
|
||||
pub access_key_id: String,
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
pub secret_access_key: String,
|
||||
|
||||
pub default_ami_name: String,
|
||||
pub default_subnet_id: String,
|
||||
pub default_key_pair_name: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub available_ami_accounts: AvailableAmiAccounts,
|
||||
|
||||
#[serde(default = "default_aws_region")]
|
||||
pub default_region: String,
|
||||
|
||||
#[serde(default = "default_volume_gb")]
|
||||
pub default_volume_gb: i32,
|
||||
|
||||
#[serde(default = "default_instance_type")]
|
||||
pub default_instance_type: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub default_security_group_ids: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub default_assign_public_ip: bool,
|
||||
}
|
||||
|
||||
fn default_aws_region() -> String {
|
||||
String::from("us-east-1")
|
||||
}
|
||||
|
||||
fn default_volume_gb() -> i32 {
|
||||
8
|
||||
}
|
||||
|
||||
fn default_instance_type() -> String {
|
||||
String::from("m5.2xlarge")
|
||||
}
|
||||
|
||||
pub type AvailableAmiAccounts = HashMap<String, AmiAccounts>; // (ami_name, AmiAccounts)
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct AmiAccounts {
|
||||
pub ami_id: String,
|
||||
#[serde(default)]
|
||||
pub github: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub docker: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub secrets: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PeripheryConfig {
|
||||
#[serde(default = "default_periphery_port")]
|
||||
pub port: u16,
|
||||
#[serde(default = "default_repo_dir")]
|
||||
pub repo_dir: PathBuf,
|
||||
#[serde(default = "default_stats_refresh_interval")]
|
||||
pub stats_polling_rate: Timelength,
|
||||
#[serde(default)]
|
||||
pub allowed_ips: Vec<IpAddr>,
|
||||
#[serde(default)]
|
||||
pub passkeys: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub secrets: SecretsMap,
|
||||
#[serde(default)]
|
||||
pub github_accounts: GithubAccounts,
|
||||
#[serde(default)]
|
||||
pub docker_accounts: DockerAccounts,
|
||||
}
|
||||
|
||||
fn default_periphery_port() -> u16 {
|
||||
8000
|
||||
}
|
||||
|
||||
fn default_repo_dir() -> PathBuf {
|
||||
"/repos".parse().unwrap()
|
||||
}
|
||||
|
||||
fn default_stats_refresh_interval() -> Timelength {
|
||||
Timelength::FiveSeconds
|
||||
}
|
||||
386
bin/migrator/src/legacy/v0/deployment.rs
Normal file
386
bin/migrator/src/legacy/v0/deployment.rs
Normal file
@@ -0,0 +1,386 @@
|
||||
use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::legacy::v0::unix_from_monitor_ts;
|
||||
|
||||
use super::{Command, EnvironmentVar, PermissionsMap, Version};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct Deployment {
|
||||
#[serde(
|
||||
default,
|
||||
rename = "_id",
|
||||
skip_serializing_if = "String::is_empty",
|
||||
with = "hex_string_as_object_id"
|
||||
)]
|
||||
pub id: String,
|
||||
|
||||
pub name: String, // must be formatted to be compat with docker
|
||||
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
|
||||
pub server_id: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub permissions: PermissionsMap,
|
||||
|
||||
#[serde(default)]
|
||||
pub skip_secret_interp: bool,
|
||||
|
||||
pub docker_run_args: DockerRunArgs,
|
||||
|
||||
#[serde(default = "default_term_signal_labels")]
|
||||
pub term_signal_labels: Vec<TerminationSignalLabel>,
|
||||
|
||||
#[serde(default)]
|
||||
pub termination_signal: TerminationSignal,
|
||||
|
||||
#[serde(default = "default_termination_timeout")]
|
||||
pub termination_timeout: i32,
|
||||
|
||||
pub build_id: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub redeploy_on_build: bool,
|
||||
|
||||
pub build_version: Option<Version>,
|
||||
|
||||
// deployment repo related
|
||||
pub repo: Option<String>,
|
||||
|
||||
pub branch: Option<String>,
|
||||
|
||||
pub github_account: Option<String>,
|
||||
|
||||
pub on_clone: Option<Command>,
|
||||
|
||||
pub on_pull: Option<Command>,
|
||||
|
||||
pub repo_mount: Option<Conversion>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub created_at: String,
|
||||
#[serde(default)]
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
fn default_termination_timeout() -> i32 {
|
||||
10
|
||||
}
|
||||
|
||||
fn default_term_signal_labels() -> Vec<TerminationSignalLabel> {
|
||||
vec![TerminationSignalLabel::default()]
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct DeploymentWithContainerState {
|
||||
pub deployment: Deployment,
|
||||
pub state: DockerContainerState,
|
||||
pub container: Option<BasicContainerInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct DeploymentActionState {
|
||||
pub deploying: bool,
|
||||
pub stopping: bool,
|
||||
pub starting: bool,
|
||||
pub removing: bool,
|
||||
pub pulling: bool,
|
||||
pub recloning: bool,
|
||||
pub updating: bool,
|
||||
pub renaming: bool,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq,
|
||||
)]
|
||||
pub struct TerminationSignalLabel {
|
||||
pub signal: TerminationSignal,
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
impl From<TerminationSignalLabel>
|
||||
for monitor_client::entities::deployment::TerminationSignalLabel
|
||||
{
|
||||
fn from(value: TerminationSignalLabel) -> Self {
|
||||
Self {
|
||||
signal: value.signal.into(),
|
||||
label: value.label,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct DockerRunArgs {
|
||||
pub image: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub ports: Vec<Conversion>,
|
||||
|
||||
#[serde(default)]
|
||||
pub volumes: Vec<Conversion>,
|
||||
|
||||
#[serde(default)]
|
||||
pub environment: Vec<EnvironmentVar>,
|
||||
|
||||
#[serde(default = "default_network")]
|
||||
pub network: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub restart: RestartMode,
|
||||
|
||||
pub post_image: Option<String>,
|
||||
|
||||
pub container_user: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub extra_args: Vec<String>,
|
||||
|
||||
pub docker_account: Option<String>, // the username of the dockerhub account
|
||||
}
|
||||
|
||||
impl Default for DockerRunArgs {
|
||||
fn default() -> DockerRunArgs {
|
||||
DockerRunArgs {
|
||||
network: "host".to_string(),
|
||||
image: Default::default(),
|
||||
ports: Default::default(),
|
||||
volumes: Default::default(),
|
||||
environment: Default::default(),
|
||||
restart: Default::default(),
|
||||
post_image: Default::default(),
|
||||
container_user: Default::default(),
|
||||
extra_args: Default::default(),
|
||||
docker_account: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_network() -> String {
|
||||
String::from("host")
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct BasicContainerInfo {
|
||||
pub name: String,
|
||||
pub id: String,
|
||||
pub image: String,
|
||||
pub state: DockerContainerState,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Default, PartialEq,
|
||||
)]
|
||||
pub struct Conversion {
|
||||
pub local: String,
|
||||
pub container: String,
|
||||
}
|
||||
|
||||
impl From<Conversion>
|
||||
for monitor_client::entities::deployment::Conversion
|
||||
{
|
||||
fn from(value: Conversion) -> Self {
|
||||
Self {
|
||||
local: value.local,
|
||||
container: value.container,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct DockerContainerStats {
|
||||
#[serde(alias = "Name")]
|
||||
pub name: String,
|
||||
#[serde(alias = "CPUPerc")]
|
||||
pub cpu_perc: String,
|
||||
#[serde(alias = "MemPerc")]
|
||||
pub mem_perc: String,
|
||||
#[serde(alias = "MemUsage")]
|
||||
pub mem_usage: String,
|
||||
#[serde(alias = "NetIO")]
|
||||
pub net_io: String,
|
||||
#[serde(alias = "BlockIO")]
|
||||
pub block_io: String,
|
||||
#[serde(alias = "PIDs")]
|
||||
pub pids: String,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Hash,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Default,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DockerContainerState {
|
||||
#[default]
|
||||
Unknown,
|
||||
NotDeployed,
|
||||
Created,
|
||||
Restarting,
|
||||
Running,
|
||||
Removing,
|
||||
Paused,
|
||||
Exited,
|
||||
Dead,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Hash,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Default,
|
||||
)]
|
||||
pub enum RestartMode {
|
||||
#[default]
|
||||
#[serde(rename = "no")]
|
||||
NoRestart,
|
||||
#[serde(rename = "on-failure")]
|
||||
OnFailure,
|
||||
#[serde(rename = "always")]
|
||||
Always,
|
||||
#[serde(rename = "unless-stopped")]
|
||||
UnlessStopped,
|
||||
}
|
||||
|
||||
impl From<RestartMode>
|
||||
for monitor_client::entities::deployment::RestartMode
|
||||
{
|
||||
fn from(value: RestartMode) -> Self {
|
||||
use monitor_client::entities::deployment::RestartMode::*;
|
||||
match value {
|
||||
RestartMode::NoRestart => NoRestart,
|
||||
RestartMode::OnFailure => OnFailure,
|
||||
RestartMode::Always => Always,
|
||||
RestartMode::UnlessStopped => UnlessStopped,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Hash,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Default,
|
||||
)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum TerminationSignal {
|
||||
#[serde(alias = "1")]
|
||||
SigHup,
|
||||
#[serde(alias = "2")]
|
||||
SigInt,
|
||||
#[serde(alias = "3")]
|
||||
SigQuit,
|
||||
#[default]
|
||||
#[serde(alias = "15")]
|
||||
SigTerm,
|
||||
}
|
||||
|
||||
impl From<TerminationSignal>
|
||||
for monitor_client::entities::deployment::TerminationSignal
|
||||
{
|
||||
fn from(value: TerminationSignal) -> Self {
|
||||
use monitor_client::entities::deployment::TerminationSignal::*;
|
||||
match value {
|
||||
TerminationSignal::SigHup => SigHup,
|
||||
TerminationSignal::SigInt => SigInt,
|
||||
TerminationSignal::SigQuit => SigQuit,
|
||||
TerminationSignal::SigTerm => SigTerm,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Deployment>
|
||||
for monitor_client::entities::deployment::Deployment
|
||||
{
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: Deployment) -> Result<Self, Self::Error> {
|
||||
let image = if let Some(build_id) = value.build_id {
|
||||
monitor_client::entities::deployment::DeploymentImage::Build {
|
||||
build_id,
|
||||
version: value.build_version.unwrap_or_default().into(),
|
||||
}
|
||||
} else {
|
||||
monitor_client::entities::deployment::DeploymentImage::Image {
|
||||
image: value.docker_run_args.image,
|
||||
}
|
||||
};
|
||||
let deployment = Self {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
// permissions: value
|
||||
// .permissions
|
||||
// .into_iter()
|
||||
// .map(|(id, p)| (id, p.into()))
|
||||
// .collect(),
|
||||
updated_at: unix_from_monitor_ts(&value.updated_at)?,
|
||||
tags: Vec::new(),
|
||||
info: (),
|
||||
config:
|
||||
monitor_client::entities::deployment::DeploymentConfig {
|
||||
server_id: value.server_id,
|
||||
send_alerts: true,
|
||||
image,
|
||||
skip_secret_interp: value.skip_secret_interp,
|
||||
redeploy_on_build: value.redeploy_on_build,
|
||||
term_signal_labels: value
|
||||
.term_signal_labels
|
||||
.into_iter()
|
||||
.map(|t| t.into())
|
||||
.collect(),
|
||||
termination_signal: value.termination_signal.into(),
|
||||
termination_timeout: value.termination_timeout,
|
||||
ports: value
|
||||
.docker_run_args
|
||||
.ports
|
||||
.into_iter()
|
||||
.map(|p| p.into())
|
||||
.collect(),
|
||||
volumes: value
|
||||
.docker_run_args
|
||||
.volumes
|
||||
.into_iter()
|
||||
.map(|v| v.into())
|
||||
.collect(),
|
||||
environment: value
|
||||
.docker_run_args
|
||||
.environment
|
||||
.into_iter()
|
||||
.map(|e| e.into())
|
||||
.collect(),
|
||||
network: value.docker_run_args.network,
|
||||
restart: value.docker_run_args.restart.into(),
|
||||
process_args: value
|
||||
.docker_run_args
|
||||
.post_image
|
||||
.unwrap_or_default(),
|
||||
extra_args: value.docker_run_args.extra_args,
|
||||
docker_account: value
|
||||
.docker_run_args
|
||||
.docker_account
|
||||
.unwrap_or_default(),
|
||||
labels: Default::default(),
|
||||
},
|
||||
};
|
||||
Ok(deployment)
|
||||
}
|
||||
}
|
||||
38
bin/migrator/src/legacy/v0/group.rs
Normal file
38
bin/migrator/src/legacy/v0/group.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::PermissionsMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct Group {
|
||||
#[serde(
|
||||
default,
|
||||
rename = "_id",
|
||||
skip_serializing_if = "String::is_empty",
|
||||
with = "hex_string_as_object_id"
|
||||
)]
|
||||
pub id: String,
|
||||
|
||||
pub name: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub permissions: PermissionsMap,
|
||||
|
||||
pub builds: Vec<String>,
|
||||
|
||||
pub deployments: Vec<String>,
|
||||
|
||||
pub servers: Vec<String>,
|
||||
|
||||
pub procedures: Vec<String>,
|
||||
|
||||
pub groups: Vec<String>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub created_at: String,
|
||||
#[serde(default)]
|
||||
pub updated_at: String,
|
||||
}
|
||||
336
bin/migrator/src/legacy/v0/mod.rs
Normal file
336
bin/migrator/src/legacy/v0/mod.rs
Normal file
@@ -0,0 +1,336 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use chrono::{DateTime, LocalResult, SecondsFormat, TimeZone, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod traits;
|
||||
|
||||
mod action;
|
||||
mod alert;
|
||||
mod build;
|
||||
mod config;
|
||||
mod deployment;
|
||||
mod group;
|
||||
mod periphery_command;
|
||||
mod procedure;
|
||||
mod server;
|
||||
mod update;
|
||||
mod user;
|
||||
|
||||
pub use action::*;
|
||||
pub use alert::*;
|
||||
pub use build::*;
|
||||
pub use config::*;
|
||||
pub use deployment::*;
|
||||
pub use group::*;
|
||||
pub use periphery_command::*;
|
||||
pub use procedure::*;
|
||||
pub use server::*;
|
||||
pub use update::*;
|
||||
pub use user::*;
|
||||
|
||||
pub type PermissionsMap = HashMap<String, PermissionLevel>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct CloneArgs {
|
||||
pub name: String,
|
||||
pub repo: Option<String>,
|
||||
pub branch: Option<String>,
|
||||
pub on_clone: Option<Command>,
|
||||
pub on_pull: Option<Command>,
|
||||
pub github_account: Option<GithubUsername>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq,
|
||||
)]
|
||||
pub struct Command {
|
||||
#[serde(default)]
|
||||
pub path: String,
|
||||
#[serde(default)]
|
||||
pub command: String,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Default, PartialEq,
|
||||
)]
|
||||
pub struct EnvironmentVar {
|
||||
pub variable: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl From<EnvironmentVar>
|
||||
for monitor_client::entities::EnvironmentVar
|
||||
{
|
||||
fn from(value: EnvironmentVar) -> Self {
|
||||
Self {
|
||||
variable: value.variable,
|
||||
value: value.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct UserCredentials {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, PartialEq, Hash, Eq, Clone, Copy,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AccountType {
|
||||
Github,
|
||||
Docker,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
Default,
|
||||
PartialEq,
|
||||
Hash,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Operation {
|
||||
// do nothing
|
||||
#[default]
|
||||
None,
|
||||
|
||||
// server
|
||||
CreateServer,
|
||||
UpdateServer,
|
||||
DeleteServer,
|
||||
PruneImagesServer,
|
||||
PruneContainersServer,
|
||||
PruneNetworksServer,
|
||||
RenameServer,
|
||||
|
||||
// build
|
||||
CreateBuild,
|
||||
UpdateBuild,
|
||||
DeleteBuild,
|
||||
BuildBuild,
|
||||
|
||||
// deployment
|
||||
CreateDeployment,
|
||||
UpdateDeployment,
|
||||
DeleteDeployment,
|
||||
DeployContainer,
|
||||
StopContainer,
|
||||
StartContainer,
|
||||
RemoveContainer,
|
||||
PullDeployment,
|
||||
RecloneDeployment,
|
||||
RenameDeployment,
|
||||
|
||||
// procedure
|
||||
CreateProcedure,
|
||||
UpdateProcedure,
|
||||
DeleteProcedure,
|
||||
|
||||
// command
|
||||
CreateCommand,
|
||||
UpdateCommand,
|
||||
DeleteCommand,
|
||||
RunCommand,
|
||||
|
||||
// group
|
||||
CreateGroup,
|
||||
UpdateGroup,
|
||||
DeleteGroup,
|
||||
|
||||
// user
|
||||
ModifyUserEnabled,
|
||||
ModifyUserCreateServerPermissions,
|
||||
ModifyUserCreateBuildPermissions,
|
||||
ModifyUserPermissions,
|
||||
|
||||
// github webhook automation
|
||||
AutoBuild,
|
||||
AutoPull,
|
||||
}
|
||||
|
||||
impl From<Operation> for monitor_client::entities::Operation {
|
||||
fn from(value: Operation) -> Self {
|
||||
use monitor_client::entities::Operation::*;
|
||||
match value {
|
||||
Operation::None => None,
|
||||
Operation::CreateServer => CreateServer,
|
||||
Operation::UpdateServer => UpdateServer,
|
||||
Operation::DeleteServer => DeleteServer,
|
||||
Operation::PruneImagesServer => PruneImagesServer,
|
||||
Operation::PruneContainersServer => PruneContainersServer,
|
||||
Operation::PruneNetworksServer => PruneNetworksServer,
|
||||
Operation::RenameServer => RenameServer,
|
||||
Operation::CreateBuild => CreateBuild,
|
||||
Operation::UpdateBuild => UpdateBuild,
|
||||
Operation::DeleteBuild => DeleteBuild,
|
||||
Operation::BuildBuild => RunBuild,
|
||||
Operation::CreateDeployment => CreateDeployment,
|
||||
Operation::UpdateDeployment => UpdateDeployment,
|
||||
Operation::DeleteDeployment => DeleteDeployment,
|
||||
Operation::DeployContainer => DeployContainer,
|
||||
Operation::StopContainer => StopContainer,
|
||||
Operation::StartContainer => StartContainer,
|
||||
Operation::RemoveContainer => RemoveContainer,
|
||||
Operation::PullDeployment => None,
|
||||
Operation::RecloneDeployment => None,
|
||||
Operation::RenameDeployment => RenameDeployment,
|
||||
Operation::CreateProcedure => None,
|
||||
Operation::UpdateProcedure => None,
|
||||
Operation::DeleteProcedure => None,
|
||||
Operation::CreateCommand => None,
|
||||
Operation::UpdateCommand => None,
|
||||
Operation::DeleteCommand => None,
|
||||
Operation::RunCommand => None,
|
||||
Operation::CreateGroup => None,
|
||||
Operation::UpdateGroup => None,
|
||||
Operation::DeleteGroup => None,
|
||||
Operation::ModifyUserEnabled => None,
|
||||
Operation::ModifyUserCreateServerPermissions => None,
|
||||
Operation::ModifyUserCreateBuildPermissions => None,
|
||||
Operation::ModifyUserPermissions => None,
|
||||
Operation::AutoBuild => RunBuild,
|
||||
Operation::AutoPull => PullRepo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
Hash,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Default,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PermissionLevel {
|
||||
#[default]
|
||||
None,
|
||||
Read,
|
||||
Execute,
|
||||
Update,
|
||||
}
|
||||
|
||||
impl Default for &PermissionLevel {
|
||||
fn default() -> Self {
|
||||
&PermissionLevel::None
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PermissionLevel>
|
||||
for monitor_client::entities::permission::PermissionLevel
|
||||
{
|
||||
fn from(value: PermissionLevel) -> Self {
|
||||
use monitor_client::entities::permission::PermissionLevel::*;
|
||||
match value {
|
||||
PermissionLevel::None => None,
|
||||
PermissionLevel::Read => Read,
|
||||
PermissionLevel::Execute => Execute,
|
||||
PermissionLevel::Update => Write,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, PartialEq, Hash, Eq, Clone, Copy,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PermissionsTarget {
|
||||
Server,
|
||||
Deployment,
|
||||
Build,
|
||||
Procedure,
|
||||
Group,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Hash,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Default,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Timelength {
|
||||
#[serde(rename = "1-sec")]
|
||||
OneSecond,
|
||||
#[serde(rename = "5-sec")]
|
||||
FiveSeconds,
|
||||
#[serde(rename = "10-sec")]
|
||||
TenSeconds,
|
||||
#[serde(rename = "15-sec")]
|
||||
FifteenSeconds,
|
||||
#[serde(rename = "30-sec")]
|
||||
ThirtySeconds,
|
||||
#[default]
|
||||
#[serde(rename = "1-min")]
|
||||
OneMinute,
|
||||
#[serde(rename = "2-min")]
|
||||
TwoMinutes,
|
||||
#[serde(rename = "5-min")]
|
||||
FiveMinutes,
|
||||
#[serde(rename = "10-min")]
|
||||
TenMinutes,
|
||||
#[serde(rename = "15-min")]
|
||||
FifteenMinutes,
|
||||
#[serde(rename = "30-min")]
|
||||
ThirtyMinutes,
|
||||
#[serde(rename = "1-hr")]
|
||||
OneHour,
|
||||
#[serde(rename = "2-hr")]
|
||||
TwoHours,
|
||||
#[serde(rename = "6-hr")]
|
||||
SixHours,
|
||||
#[serde(rename = "8-hr")]
|
||||
EightHours,
|
||||
#[serde(rename = "12-hr")]
|
||||
TwelveHours,
|
||||
#[serde(rename = "1-day")]
|
||||
OneDay,
|
||||
#[serde(rename = "3-day")]
|
||||
ThreeDay,
|
||||
#[serde(rename = "1-wk")]
|
||||
OneWeek,
|
||||
#[serde(rename = "2-wk")]
|
||||
TwoWeeks,
|
||||
#[serde(rename = "30-day")]
|
||||
ThirtyDays,
|
||||
}
|
||||
|
||||
pub fn unix_from_monitor_ts(ts: &str) -> anyhow::Result<i64> {
|
||||
Ok(
|
||||
DateTime::parse_from_rfc3339(ts)
|
||||
.context("failed to parse rfc3339 timestamp")?
|
||||
.timestamp_millis(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn monitor_ts_from_unix(ts: i64) -> anyhow::Result<String> {
|
||||
match Utc.timestamp_millis_opt(ts) {
|
||||
LocalResult::Single(dt) => {
|
||||
Ok(dt.to_rfc3339_opts(SecondsFormat::Millis, false))
|
||||
}
|
||||
LocalResult::None => {
|
||||
Err(anyhow!("out of bounds timestamp passed"))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
38
bin/migrator/src/legacy/v0/periphery_command.rs
Normal file
38
bin/migrator/src/legacy/v0/periphery_command.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Command, PermissionsMap};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct PeripheryCommand {
|
||||
#[serde(
|
||||
default,
|
||||
rename = "_id",
|
||||
skip_serializing_if = "String::is_empty",
|
||||
with = "hex_string_as_object_id"
|
||||
)]
|
||||
pub id: String,
|
||||
|
||||
pub name: String, // must be formatted to be compat with docker
|
||||
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
|
||||
pub server_id: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub permissions: PermissionsMap,
|
||||
|
||||
#[serde(default)]
|
||||
pub command: Command,
|
||||
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub created_at: String,
|
||||
#[serde(default)]
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct CommandActionState {
|
||||
pub running: bool,
|
||||
}
|
||||
79
bin/migrator/src/legacy/v0/procedure.rs
Normal file
79
bin/migrator/src/legacy/v0/procedure.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::PermissionsMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct Procedure {
|
||||
#[serde(
|
||||
default,
|
||||
rename = "_id",
|
||||
skip_serializing_if = "String::is_empty",
|
||||
with = "hex_string_as_object_id"
|
||||
)]
|
||||
pub id: String,
|
||||
|
||||
pub name: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub stages: Vec<ProcedureStage>,
|
||||
|
||||
#[serde(default)]
|
||||
pub webhook_branches: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub permissions: PermissionsMap,
|
||||
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub created_at: String,
|
||||
#[serde(default)]
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Default, PartialEq,
|
||||
)]
|
||||
pub struct ProcedureStage {
|
||||
pub operation: ProcedureOperation,
|
||||
pub target_id: String,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Hash,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Default,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProcedureOperation {
|
||||
// do nothing
|
||||
#[default]
|
||||
None,
|
||||
|
||||
// server
|
||||
PruneImagesServer,
|
||||
PruneContainersServer,
|
||||
PruneNetworksServer,
|
||||
|
||||
// build
|
||||
BuildBuild,
|
||||
|
||||
// deployment
|
||||
DeployContainer,
|
||||
StopContainer,
|
||||
StartContainer,
|
||||
RemoveContainer,
|
||||
PullDeployment,
|
||||
RecloneDeployment,
|
||||
|
||||
// procedure
|
||||
RunProcedure,
|
||||
}
|
||||
324
bin/migrator/src/legacy/v0/server.rs
Normal file
324
bin/migrator/src/legacy/v0/server.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{unix_from_monitor_ts, PermissionsMap, Timelength};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Server {
|
||||
#[serde(
|
||||
default,
|
||||
rename = "_id",
|
||||
skip_serializing_if = "String::is_empty",
|
||||
with = "hex_string_as_object_id"
|
||||
)]
|
||||
pub id: String,
|
||||
|
||||
pub name: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
|
||||
pub address: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub permissions: PermissionsMap,
|
||||
|
||||
pub enabled: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub to_notify: Vec<String>, // slack users to notify
|
||||
|
||||
#[serde(default)]
|
||||
pub auto_prune: bool,
|
||||
|
||||
#[serde(default = "default_cpu_alert")]
|
||||
pub cpu_alert: f32,
|
||||
|
||||
#[serde(default = "default_mem_alert")]
|
||||
pub mem_alert: f64,
|
||||
|
||||
#[serde(default = "default_disk_alert")]
|
||||
pub disk_alert: f64,
|
||||
|
||||
#[serde(default)]
|
||||
pub stats_interval: Timelength,
|
||||
|
||||
pub region: Option<String>,
|
||||
|
||||
pub instance_id: Option<String>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub created_at: String,
|
||||
#[serde(default)]
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl Default for Server {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Default::default(),
|
||||
name: Default::default(),
|
||||
address: Default::default(),
|
||||
permissions: Default::default(),
|
||||
enabled: true,
|
||||
auto_prune: true,
|
||||
to_notify: Default::default(),
|
||||
cpu_alert: default_cpu_alert(),
|
||||
mem_alert: default_mem_alert(),
|
||||
disk_alert: default_disk_alert(),
|
||||
stats_interval: Default::default(),
|
||||
region: Default::default(),
|
||||
instance_id: Default::default(),
|
||||
description: Default::default(),
|
||||
created_at: Default::default(),
|
||||
updated_at: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_cpu_alert() -> f32 {
|
||||
95.0
|
||||
}
|
||||
|
||||
fn default_mem_alert() -> f64 {
|
||||
80.0
|
||||
}
|
||||
|
||||
fn default_disk_alert() -> f64 {
|
||||
75.0
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ServerWithStatus {
|
||||
pub server: Server,
|
||||
pub status: ServerStatus,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct ServerActionState {
|
||||
pub pruning_networks: bool,
|
||||
pub pruning_containers: bool,
|
||||
pub pruning_images: bool,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Hash,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Default,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ServerStatus {
|
||||
Ok,
|
||||
#[default]
|
||||
NotOk,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
|
||||
pub struct SystemStatsQuery {
|
||||
#[serde(default)]
|
||||
pub cpus: bool,
|
||||
#[serde(default)]
|
||||
pub disks: bool,
|
||||
#[serde(default)]
|
||||
pub networks: bool,
|
||||
#[serde(default)]
|
||||
pub components: bool,
|
||||
#[serde(default)]
|
||||
pub processes: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
pub struct SystemStats {
|
||||
#[serde(default)]
|
||||
pub system_load: f64,
|
||||
pub cpu_perc: f32,
|
||||
pub cpu_freq_mhz: f64,
|
||||
pub mem_used_gb: f64, // in GB
|
||||
pub mem_total_gb: f64, // in GB
|
||||
pub disk: DiskUsage,
|
||||
#[serde(default)]
|
||||
pub cpus: Vec<SingleCpuUsage>,
|
||||
#[serde(default)]
|
||||
pub networks: Vec<SystemNetwork>,
|
||||
#[serde(default)]
|
||||
pub components: Vec<SystemComponent>,
|
||||
#[serde(default)]
|
||||
pub processes: Vec<SystemProcess>,
|
||||
pub polling_rate: Timelength,
|
||||
pub refresh_ts: u128,
|
||||
pub refresh_list_ts: u128,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct SingleCpuUsage {
|
||||
pub name: String,
|
||||
pub usage: f32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
pub struct DiskUsage {
|
||||
pub used_gb: f64, // in GB
|
||||
pub total_gb: f64, // in GB
|
||||
pub read_kb: f64, // in kB
|
||||
pub write_kb: f64, // in kB
|
||||
#[serde(default)]
|
||||
pub disks: Vec<SingleDiskUsage>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct SingleDiskUsage {
|
||||
pub mount: PathBuf,
|
||||
pub used_gb: f64, // in GB
|
||||
pub total_gb: f64, // in GB
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct SystemNetwork {
|
||||
pub name: String,
|
||||
pub recieved_kb: f64, // in kB
|
||||
pub transmitted_kb: f64, // in kB
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct SystemComponent {
|
||||
pub label: String,
|
||||
pub temp: f32,
|
||||
pub max: f32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub critical: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct SystemProcess {
|
||||
pub pid: u32,
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub exe: String,
|
||||
pub cmd: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub start_time: f64,
|
||||
pub cpu_perc: f32,
|
||||
pub mem_mb: f64,
|
||||
pub disk_read_kb: f64,
|
||||
pub disk_write_kb: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
pub struct SystemStatsRecord {
|
||||
#[serde(
|
||||
default,
|
||||
rename = "_id",
|
||||
skip_serializing_if = "String::is_empty",
|
||||
with = "hex_string_as_object_id"
|
||||
)]
|
||||
pub id: String,
|
||||
pub server_id: String,
|
||||
pub ts: f64, // unix ts milliseconds
|
||||
#[serde(default)]
|
||||
pub system_load: f64,
|
||||
pub cpu_perc: f32, // in %
|
||||
#[serde(default)]
|
||||
pub cpu_freq_mhz: f64, // in MHz
|
||||
pub mem_used_gb: f64, // in GB
|
||||
pub mem_total_gb: f64, // in GB
|
||||
pub disk: DiskUsage,
|
||||
#[serde(default)]
|
||||
pub cpus: Vec<SingleCpuUsage>,
|
||||
#[serde(default)]
|
||||
pub networks: Vec<SystemNetwork>,
|
||||
#[serde(default)]
|
||||
pub components: Vec<SystemComponent>,
|
||||
#[serde(default)]
|
||||
pub processes: Vec<SystemProcess>,
|
||||
pub polling_rate: Timelength,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct HistoricalStatsQuery {
|
||||
#[serde(default = "default_interval")]
|
||||
pub interval: Timelength,
|
||||
#[serde(default = "default_limit")]
|
||||
pub limit: f64,
|
||||
#[serde(default)]
|
||||
pub page: f64,
|
||||
#[serde(default)]
|
||||
pub networks: bool,
|
||||
#[serde(default)]
|
||||
pub components: bool,
|
||||
}
|
||||
|
||||
impl Default for HistoricalStatsQuery {
|
||||
fn default() -> Self {
|
||||
HistoricalStatsQuery {
|
||||
interval: default_interval(),
|
||||
limit: default_limit(),
|
||||
page: Default::default(),
|
||||
networks: Default::default(),
|
||||
components: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_interval() -> Timelength {
|
||||
Timelength::OneHour
|
||||
}
|
||||
|
||||
fn default_limit() -> f64 {
|
||||
100.0
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct SystemInformation {
|
||||
pub name: Option<String>,
|
||||
pub os: Option<String>,
|
||||
pub kernel: Option<String>,
|
||||
pub core_count: Option<u32>,
|
||||
pub host_name: Option<String>,
|
||||
pub cpu_brand: String,
|
||||
}
|
||||
|
||||
impl TryFrom<Server> for monitor_client::entities::server::Server {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: Server) -> Result<Self, Self::Error> {
|
||||
let server = Self {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
// permissions: value
|
||||
// .permissions
|
||||
// .into_iter()
|
||||
// .map(|(id, p)| (id, p.into()))
|
||||
// .collect(),
|
||||
updated_at: unix_from_monitor_ts(&value.updated_at)?,
|
||||
tags: Vec::new(),
|
||||
info: (),
|
||||
config: monitor_client::entities::server::ServerConfig {
|
||||
address: value.address,
|
||||
enabled: value.enabled,
|
||||
auto_prune: value.auto_prune,
|
||||
send_unreachable_alerts: true,
|
||||
stats_monitoring: true,
|
||||
send_cpu_alerts: true,
|
||||
send_mem_alerts: true,
|
||||
send_disk_alerts: true,
|
||||
region: value.region.unwrap_or_default(),
|
||||
cpu_warning: value.cpu_alert,
|
||||
cpu_critical: value.cpu_alert,
|
||||
mem_warning: value.mem_alert,
|
||||
mem_critical: value.mem_alert,
|
||||
disk_warning: value.disk_alert,
|
||||
disk_critical: value.disk_alert,
|
||||
},
|
||||
};
|
||||
Ok(server)
|
||||
}
|
||||
}
|
||||
112
bin/migrator/src/legacy/v0/traits.rs
Normal file
112
bin/migrator/src/legacy/v0/traits.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use super::{
|
||||
Build, BuildActionState, CloneArgs, CommandActionState, Deployment,
|
||||
DeploymentActionState, Group, PeripheryCommand, PermissionLevel,
|
||||
PermissionsMap, Procedure, Server, ServerActionState,
|
||||
};
|
||||
|
||||
pub trait Permissioned {
|
||||
fn permissions_map(&self) -> &PermissionsMap;
|
||||
|
||||
fn get_user_permissions(&self, user_id: &str) -> PermissionLevel {
|
||||
*self.permissions_map().get(user_id).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Permissioned for Deployment {
|
||||
fn permissions_map(&self) -> &PermissionsMap {
|
||||
&self.permissions
|
||||
}
|
||||
}
|
||||
|
||||
impl Permissioned for Build {
|
||||
fn permissions_map(&self) -> &PermissionsMap {
|
||||
&self.permissions
|
||||
}
|
||||
}
|
||||
|
||||
impl Permissioned for Server {
|
||||
fn permissions_map(&self) -> &PermissionsMap {
|
||||
&self.permissions
|
||||
}
|
||||
}
|
||||
|
||||
impl Permissioned for Procedure {
|
||||
fn permissions_map(&self) -> &PermissionsMap {
|
||||
&self.permissions
|
||||
}
|
||||
}
|
||||
|
||||
impl Permissioned for Group {
|
||||
fn permissions_map(&self) -> &PermissionsMap {
|
||||
&self.permissions
|
||||
}
|
||||
}
|
||||
|
||||
impl Permissioned for PeripheryCommand {
|
||||
fn permissions_map(&self) -> &PermissionsMap {
|
||||
&self.permissions
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Busy {
|
||||
fn busy(&self) -> bool;
|
||||
}
|
||||
|
||||
impl Busy for ServerActionState {
|
||||
fn busy(&self) -> bool {
|
||||
self.pruning_containers
|
||||
|| self.pruning_images
|
||||
|| self.pruning_networks
|
||||
}
|
||||
}
|
||||
|
||||
impl Busy for DeploymentActionState {
|
||||
fn busy(&self) -> bool {
|
||||
self.deploying
|
||||
|| self.pulling
|
||||
|| self.recloning
|
||||
|| self.removing
|
||||
|| self.starting
|
||||
|| self.stopping
|
||||
|| self.updating
|
||||
|| self.renaming
|
||||
}
|
||||
}
|
||||
|
||||
impl Busy for BuildActionState {
|
||||
fn busy(&self) -> bool {
|
||||
self.building || self.updating
|
||||
}
|
||||
}
|
||||
|
||||
impl Busy for CommandActionState {
|
||||
fn busy(&self) -> bool {
|
||||
self.running
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Deployment> for CloneArgs {
|
||||
fn from(d: &Deployment) -> Self {
|
||||
CloneArgs {
|
||||
name: d.name.clone(),
|
||||
repo: d.repo.clone(),
|
||||
branch: d.branch.clone(),
|
||||
on_clone: d.on_clone.clone(),
|
||||
on_pull: d.on_pull.clone(),
|
||||
github_account: d.github_account.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Build> for CloneArgs {
|
||||
fn from(b: &Build) -> Self {
|
||||
CloneArgs {
|
||||
name: b.name.clone(),
|
||||
repo: b.repo.clone(),
|
||||
branch: b.branch.clone(),
|
||||
on_clone: b.pre_build.clone(),
|
||||
on_pull: None,
|
||||
github_account: b.github_account.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
145
bin/migrator/src/legacy/v0/update.rs
Normal file
145
bin/migrator/src/legacy/v0/update.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use monitor_client::entities::update::ResourceTarget;
|
||||
use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
unix_from_monitor_ts, Build, Deployment, Group, Operation,
|
||||
Procedure, Server, Version,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct Update {
|
||||
#[serde(
|
||||
default,
|
||||
rename = "_id",
|
||||
skip_serializing_if = "String::is_empty",
|
||||
with = "hex_string_as_object_id"
|
||||
)]
|
||||
pub id: String,
|
||||
pub target: UpdateTarget,
|
||||
pub operation: Operation,
|
||||
pub logs: Vec<Log>,
|
||||
pub start_ts: String,
|
||||
pub end_ts: Option<String>,
|
||||
pub status: UpdateStatus,
|
||||
pub success: bool,
|
||||
pub operator: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub version: Option<Version>,
|
||||
}
|
||||
|
||||
impl TryFrom<Update> for monitor_client::entities::update::Update {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: Update) -> Result<Self, Self::Error> {
|
||||
let target: Option<ResourceTarget> = value.target.into();
|
||||
let update = Self {
|
||||
id: value.id,
|
||||
operation: value.operation.into(),
|
||||
start_ts: unix_from_monitor_ts(&value.start_ts)?,
|
||||
success: value.success,
|
||||
operator: value.operator,
|
||||
target: target.unwrap_or_default(),
|
||||
logs: value
|
||||
.logs
|
||||
.into_iter()
|
||||
.map(|log| log.try_into())
|
||||
.collect::<anyhow::Result<
|
||||
Vec<monitor_client::entities::update::Log>,
|
||||
>>()?,
|
||||
end_ts: value
|
||||
.end_ts
|
||||
.and_then(|ts| unix_from_monitor_ts(&ts).ok()),
|
||||
status: value.status.into(),
|
||||
version: value.version.map(|v| v.into()).unwrap_or_default(),
|
||||
};
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct Log {
|
||||
pub stage: String,
|
||||
pub command: String,
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
pub success: bool,
|
||||
pub start_ts: String,
|
||||
pub end_ts: String,
|
||||
}
|
||||
|
||||
impl TryFrom<Log> for monitor_client::entities::update::Log {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: Log) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
stage: value.stage,
|
||||
command: value.command,
|
||||
stdout: value.stdout,
|
||||
stderr: value.stderr,
|
||||
success: value.success,
|
||||
start_ts: unix_from_monitor_ts(&value.start_ts)?,
|
||||
end_ts: unix_from_monitor_ts(&value.end_ts)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
#[serde(tag = "type", content = "id")]
|
||||
pub enum UpdateTarget {
|
||||
#[default]
|
||||
System,
|
||||
Build(String),
|
||||
Deployment(String),
|
||||
Server(String),
|
||||
Procedure(String),
|
||||
Group(String),
|
||||
Command(String),
|
||||
}
|
||||
|
||||
impl From<UpdateTarget>
|
||||
for Option<monitor_client::entities::update::ResourceTarget>
|
||||
{
|
||||
fn from(value: UpdateTarget) -> Self {
|
||||
use monitor_client::entities::update::ResourceTarget::*;
|
||||
match value {
|
||||
UpdateTarget::System => Some(System("system".to_string())),
|
||||
UpdateTarget::Build(id) => Some(Build(id)),
|
||||
UpdateTarget::Deployment(id) => Some(Deployment(id)),
|
||||
UpdateTarget::Server(id) => Some(Server(id)),
|
||||
UpdateTarget::Procedure(_) => None,
|
||||
UpdateTarget::Group(_) => None,
|
||||
UpdateTarget::Command(id) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Hash,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Default,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UpdateStatus {
|
||||
Queued,
|
||||
InProgress,
|
||||
#[default]
|
||||
Complete,
|
||||
}
|
||||
|
||||
impl From<UpdateStatus>
|
||||
for monitor_client::entities::update::UpdateStatus
|
||||
{
|
||||
fn from(value: UpdateStatus) -> Self {
|
||||
use monitor_client::entities::update::UpdateStatus::*;
|
||||
match value {
|
||||
UpdateStatus::Queued => Queued,
|
||||
UpdateStatus::InProgress => InProgress,
|
||||
UpdateStatus::Complete => Complete,
|
||||
}
|
||||
}
|
||||
}
|
||||
114
bin/migrator/src/legacy/v0/user.rs
Normal file
114
bin/migrator/src/legacy/v0/user.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use anyhow::anyhow;
|
||||
use monitor_client::entities::user::UserConfig;
|
||||
use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::unix_from_monitor_ts;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct User {
|
||||
#[serde(
|
||||
default,
|
||||
rename = "_id",
|
||||
skip_serializing_if = "String::is_empty",
|
||||
with = "hex_string_as_object_id"
|
||||
)]
|
||||
pub id: String,
|
||||
|
||||
pub username: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub admin: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub create_server_permissions: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub create_build_permissions: bool,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub avatar: Option<String>,
|
||||
|
||||
// used with auth
|
||||
#[serde(default)]
|
||||
pub secrets: Vec<ApiSecret>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub password: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub github_id: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub google_id: Option<String>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub created_at: String,
|
||||
#[serde(default)]
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Default, PartialEq,
|
||||
)]
|
||||
pub struct ApiSecret {
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub hash: String,
|
||||
pub created_at: String,
|
||||
pub expires: Option<String>,
|
||||
}
|
||||
|
||||
// impl TryFrom<ApiSecret>
|
||||
// for monitor_client::entities::user::ApiSecret
|
||||
// {
|
||||
// type Error = anyhow::Error;
|
||||
// fn try_from(value: ApiSecret) -> Result<Self, Self::Error> {
|
||||
// let secret = Self {
|
||||
// name: value.name,
|
||||
// hash: value.hash,
|
||||
// created_at: unix_from_monitor_ts(&value.created_at)?,
|
||||
// expires: value
|
||||
// .expires
|
||||
// .and_then(|exp| unix_from_monitor_ts(&exp).ok()),
|
||||
// };
|
||||
// Ok(secret)
|
||||
// }
|
||||
// }
|
||||
|
||||
impl TryFrom<User> for monitor_client::entities::user::User {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: User) -> Result<Self, Self::Error> {
|
||||
let config =
|
||||
match (value.password, value.github_id, value.google_id) {
|
||||
(Some(password), _, _) => UserConfig::Local { password },
|
||||
(None, Some(github_id), _) => UserConfig::Github {
|
||||
github_id,
|
||||
avatar: value.avatar.unwrap_or_default(),
|
||||
},
|
||||
(None, None, Some(google_id)) => UserConfig::Google {
|
||||
google_id,
|
||||
avatar: value.avatar.unwrap_or_default(),
|
||||
},
|
||||
_ => {
|
||||
return Err(anyhow!("user is not local, github, or google"))
|
||||
}
|
||||
};
|
||||
let user = Self {
|
||||
config,
|
||||
id: value.id,
|
||||
username: value.username,
|
||||
enabled: value.enabled,
|
||||
admin: value.admin,
|
||||
create_server_permissions: value.create_server_permissions,
|
||||
create_build_permissions: value.create_build_permissions,
|
||||
last_update_view: Default::default(),
|
||||
recently_viewed: Default::default(),
|
||||
updated_at: unix_from_monitor_ts(&value.updated_at)?,
|
||||
};
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user