Compare commits

...

554 Commits

Author SHA1 Message Date
mbecker20
d6aaef24cd bump rust to 1.93.0 2026-01-27 13:42:20 -08:00
mbecker20
3dce84c1f5 deploy 2.0.0-dev-114 2026-01-27 12:58:57 -08:00
mbecker20
8b16f97caf check update available against all digests for image 2026-01-27 12:58:32 -08:00
mbecker20
c98a809db4 serve static ui index.html with ETag / no-cache 2026-01-25 12:36:40 -08:00
mbecker20
c066b4fe16 mogh_server = "1.2.0" 2026-01-25 11:26:17 -08:00
mbecker20
b54fa235bc dev-113 2026-01-24 12:48:38 -08:00
mbecker20
05fd1aba2c batch check for updates, add check to table multi select actions 2026-01-24 12:48:13 -08:00
mbecker20
1a5290e124 cleanup 2026-01-24 11:33:49 -08:00
mbecker20
538e11ef3b dev 112 2026-01-24 11:33:22 -08:00
mbecker20
9208e9bf86 skip auto update for images with pinned digest 2026-01-24 11:30:41 -08:00
mbecker20
31640e3df8 fix require to import 2026-01-24 01:06:15 -08:00
mbecker20
42751d1b90 fix tailwind config module 2026-01-24 01:04:31 -08:00
mbecker20
673ab5c17d bump node version in all the dockerfiles 2026-01-24 01:01:05 -08:00
mbecker20
ad00f38758 bump frontend node version 2026-01-24 00:59:42 -08:00
mbecker20
b2459b5a64 align dashboard icon with sidebar 2026-01-24 00:58:23 -08:00
mbecker20
5923dd0e22 use mogh auth for passkey conversion 2026-01-24 00:45:32 -08:00
mbecker20
51294edd73 bump ts types and fix SwarmConfig type name conflict 2026-01-23 20:26:43 -08:00
mbecker20
baf77bf701 add api docs link 2026-01-23 14:59:22 -08:00
mbecker20
ddd99dc85d deploy 2.0.0-dev-111 2026-01-23 14:39:36 -08:00
mbecker20
a4c902282d improve swarm service / stack state inference 2026-01-23 14:39:00 -08:00
mbecker20
d7a2d4aadc clean up dangling action api keys on startup 2026-01-23 11:37:50 -08:00
mbecker20
c930468811 fix Action import from terminal 2026-01-23 03:11:47 -08:00
mbecker20
27efc81451 stack level terminal page 2026-01-23 03:07:57 -08:00
mbecker20
e44bd324f7 finish version upgrades page, and use prism theme oneLight and oneDark 2026-01-23 02:10:16 -08:00
mbecker20
b64c7e7289 deploy 2.0.0-dev-110 2026-01-23 01:10:32 -08:00
mbecker20
5c1a17fc91 improve config styling a bit more 2026-01-23 01:10:06 -08:00
mbecker20
50c3d77aed improve config quick links styling 2026-01-23 00:50:51 -08:00
mbecker20
bba1e3ae0c typegen 2026-01-23 00:24:27 -08:00
mbecker20
fc26fede66 add back legacy Action "exec" method calling convention 2026-01-23 00:23:54 -08:00
mbecker20
9cee723a1a deploy 2.0.0-dev-109 2026-01-22 19:12:55 -08:00
mbecker20
b922e86d29 deploy 2.0.0-dev-108 2026-01-22 18:38:12 -08:00
mbecker20
85dd3c0fe9 cache latest image digests and use this for image update alerting / auto update 2026-01-22 18:37:43 -08:00
mbecker20
4e3ecf7e6c clean up 2026-01-21 03:38:48 -08:00
mbecker20
18ffe635c8 remove user api, mogh auth handles api keys 2026-01-21 02:24:21 -08:00
mbecker20
8758c9ff8b gen types 2026-01-20 03:08:43 -08:00
mbecker20
5da48ece82 fmt and bump mogh auth 2026-01-20 02:58:45 -08:00
mbecker20
9a48edc43f add the write openapi definitions 2026-01-19 23:49:12 -08:00
mbecker20
93430f6dc3 more write openapi 2026-01-19 23:34:18 -08:00
mbecker20
d893ae9241 most of write openapi 2026-01-19 23:02:48 -08:00
mbecker20
24ed3cfcb1 server / stack exec openapi 2026-01-19 22:18:41 -08:00
mbecker20
36ee7ed72e most of execute openapi 2026-01-19 22:10:16 -08:00
mbecker20
5895fb843e modularize openapi structs 2026-01-19 20:50:08 -08:00
mbecker20
98f53020fb add most basic read apis 2026-01-19 19:33:18 -08:00
mbecker20
1d10873e77 improve toml comment 2026-01-19 18:03:36 -08:00
mbecker20
f5ee42029f review / fix some openapi definitions 2026-01-19 17:57:59 -08:00
mbecker20
525402f6f2 add openapi definitions to most read apis 2026-01-19 17:42:04 -08:00
mbecker20
9f877cc188 move openapi definition into client lib under feature gate 2026-01-19 15:41:38 -08:00
mbecker20
69d6b8e6ec remove the patch 2026-01-19 01:24:50 -08:00
mbecker20
526bcc1907 deploy 2.0.0-dev-107 2026-01-19 01:22:59 -08:00
mbecker20
f7badf5579 define set logging module targets so log level becomes useful 2026-01-19 01:18:50 -08:00
mbecker20
86fd7a155d try deno npm: style dep 2026-01-19 00:25:11 -08:00
mbecker20
628afc3baa deploy 2.0.0-dev-106 2026-01-18 23:37:19 -08:00
mbecker20
04f944fcc2 use full mogh dep names 2026-01-18 23:36:52 -08:00
mbecker20
f39447b513 move to mogh_resolver 2026-01-18 22:56:13 -08:00
mbecker20
612acd02ad remove derive_variants, strum's "EnumDiscriminants" covers it 2026-01-18 22:30:54 -08:00
mbecker20
90557ae514 fix cors options 2026-01-18 19:35:21 -08:00
mbecker20
0c292f7157 deploy 2.0.0-dev-105 2026-01-18 18:16:32 -08:00
mbecker20
673ee05fe9 tmp back to 104 2026-01-18 18:16:30 -08:00
mbecker20
94b5bd6660 use public ts mogh auth client 2026-01-18 16:52:49 -08:00
mbecker20
be40110004 use mogh_auth embedded api 2026-01-18 16:31:45 -08:00
mbecker20
2d4a34b820 move to mogh deps 2026-01-17 23:22:58 -08:00
mbecker20
2feb08725b fix_ws_address adds 'ws://' to direct IPs instead of 'wss://' which won't work 2026-01-06 10:14:10 -08:00
mbecker20
161393805f refactor openapi -> utoipa feature 2026-01-05 12:45:52 -08:00
mbecker20
f4b692080d remove user_id from jwt response -- its encoded transparently in jwt 2026-01-05 12:38:17 -08:00
mbecker20
e1c6c866a2 docs.html 2025-12-30 14:33:34 -08:00
mbecker20
7106328390 add authentication to openapi docs 2025-12-30 14:32:29 -08:00
mbecker20
338acba5c6 start utoipa openapi support 2025-12-30 13:02:27 -08:00
mbecker20
dcf6f2431e tweak 2025-12-29 22:31:19 -08:00
mbecker20
58b520e651 improve OIDC client init to only as needed 2025-12-29 11:22:49 -08:00
mbecker20
8be0846733 bump axum-extra 2025-12-28 10:15:00 -08:00
mbecker20
5b651f2527 tweak 2025-12-27 15:11:13 -08:00
mbecker20
026ba4fca8 use User constructor 2025-12-27 02:14:16 -08:00
mbecker20
4bca07d4ba update deps 2025-12-27 01:47:54 -08:00
mbecker20
9091f14711 update password refetch user 2025-12-26 02:07:44 -08:00
mbecker20
8b78bb838e fix alert error messages - reference correct alerter type 2025-12-26 01:57:23 -08:00
mbecker20
d384f7ca78 update to v2 docs 2025-12-24 00:46:07 -08:00
mbecker20
48475b55d5 Periphery GetLatestImageDigest + RollbackSwarmService 2025-12-21 00:58:57 -08:00
mbecker20
64d7959c75 bump deps 2025-12-21 00:57:46 -08:00
mbecker20
d1856c2325 deploy 2.0.0-dev-104 2025-12-20 12:30:03 -08:00
mbecker20
61b7fcdb0a onboarding key fix_existing_servers / priviledged 2025-12-20 12:29:24 -08:00
mbecker20
2693f13829 deploy 2.0.0-dev-103 2025-12-19 12:22:51 -08:00
mbecker20
7fea06861b improve swarm node page 2025-12-19 12:22:20 -08:00
mbecker20
abd155193a join swarm command 2025-12-19 12:10:25 -08:00
mbecker20
7a4de0343d config / secret has service and task tables 2025-12-18 15:30:37 -08:00
mbecker20
c638e3876e deploy 2.0.0-dev-102 2025-12-18 14:52:04 -08:00
mbecker20
fc4ea40b9c task links to configs / secrets 2025-12-18 13:43:16 -08:00
mbecker20
62b84328d8 link attached configs / secrets in status 2025-12-18 13:29:38 -08:00
mbecker20
a5ee0de394 tweaks 2025-12-18 02:32:08 -08:00
mbecker20
bf0ab26d22 improve swarm deployments 2025-12-18 01:36:31 -08:00
mbecker20
0dd2ade574 fix config / secret rotation 2025-12-17 12:59:08 -08:00
mbecker20
f0172c2721 move inspect to swarm tabs top level 2025-12-15 22:55:37 -08:00
mbecker20
363d330868 similar but inherently limited swarm secret management 2025-12-15 22:49:45 -08:00
mbecker20
2843711796 swarm config create / edit / delete 2025-12-15 17:04:03 -08:00
mbecker20
4f044ccde8 Service aware config rotation 2025-12-14 14:42:53 -08:00
mbecker20
9be21b54bd fix deployment / stack toml when swarm attached 2025-12-14 00:06:12 -08:00
mbecker20
057ecbcfef add load passkey frontend indicator screen 2025-12-13 23:43:13 -08:00
mbecker20
e91d63f262 deploy 2.0.0-dev-101 2025-12-13 18:37:39 -08:00
mbecker20
3b799edb30 improve the swarm tables 2025-12-13 18:37:14 -08:00
mbecker20
41c7faf48d add resources tab to swarms 2025-12-13 11:21:24 -08:00
mbecker20
e1d745aee0 status colors for swarm resources 2025-12-13 11:12:26 -08:00
mbecker20
01df3add41 Fix task state check 2025-12-13 01:01:22 -08:00
mbecker20
577973841b better api / onboarding key shape 2025-12-12 23:59:42 -08:00
mbecker20
3e5311f413 user profile mobile 2025-12-12 18:30:00 -08:00
mbecker20
46e99783f2 deploy 2.0.0-dev-100 2025-12-12 18:00:28 -08:00
mbecker20
a6590804a7 skip 2fa for third party logins 2025-12-12 18:00:03 -08:00
mbecker20
bd752b7c72 deploy 2.0.0-dev-99 2025-12-12 17:08:45 -08:00
mbecker20
de83b3a5a0 user link multiple primary login methods 2025-12-12 17:08:03 -08:00
mbecker20
c91963fdbb fix table_borders in cli config 2025-12-11 01:03:43 -08:00
mbecker20
973aae82fd improve 2fa ux 2025-12-10 21:39:56 -08:00
mbecker20
c3626e0949 cli support clearing users 2fa methods 2025-12-10 21:39:56 -08:00
mbecker20
5ba96a58e4 binaries use bookworm 2025-12-10 21:39:56 -08:00
mbecker20
c6a1cf2867 deploy 2.0.0-dev-98 2025-12-10 21:39:56 -08:00
mbecker20
bd39aec1fd support passkey 2fa 2025-12-10 21:39:56 -08:00
mbecker20
079895a64d fix imports in permissions module 2025-12-10 21:39:56 -08:00
mbecker20
5af1c646a5 deploy 2.0.0-dev-97 2025-12-10 21:39:56 -08:00
mbecker20
747e03b5c4 Fix incorrect permissions query due to resource RBAC update query filter. Align logic with Alert resource RBAC filter. Re #1011 2025-12-10 21:39:56 -08:00
mbecker20
78c6761eca add swarm specific permission config 2025-12-10 21:39:56 -08:00
mbecker20
a949891d26 apply login credential lock (for demo users) to 2FA 2025-12-10 21:39:56 -08:00
mbecker20
9d256e420a improve totp enrollment dialog closing, fix redirect away from /login when using two factor 2025-12-10 21:39:56 -08:00
mbecker20
b460504b5a deploy 2.0.0-dev-96 2025-12-10 21:39:56 -08:00
mbecker20
e60316f2c1 TOTP 2FA with third party logins 2025-12-10 21:39:56 -08:00
mbecker20
c45e3eab16 deploy 2.0.0-dev-95 2025-12-10 21:39:56 -08:00
mbecker20
ec12f63272 double recovery code length 2025-12-10 21:39:56 -08:00
mbecker20
f2f77b4eb6 totp unenroll 2025-12-10 21:39:56 -08:00
mbecker20
677a65f8b0 local login TOTP 2fa support 2025-12-10 21:39:56 -08:00
mbecker20
a5a1090177 deploy 2.0.0-dev-94 2025-12-10 21:39:56 -08:00
mbecker20
ccb18fc88a fix server auto rotate keys default for sync 2025-12-10 21:39:56 -08:00
mbecker20
0ca583fe5e deploy Deployments to swarms 2025-12-10 21:39:56 -08:00
mbecker20
4013cd3f1e dev-93 2025-12-10 21:39:56 -08:00
mbecker20
5854a7de99 Deployment as swarm service 2025-12-10 21:39:56 -08:00
mbecker20
8c52232852 swarm stack deploy and destroy 2025-12-10 21:39:56 -08:00
mbecker20
fb9e5b1860 ts types 2025-12-10 21:39:56 -08:00
mbecker20
a1c00a8d32 implement the swarm resource associations for stack / deployment targeting swarm 2025-12-10 21:39:56 -08:00
mbecker20
775f5bf703 more stack / deployment deploy on swarm 2025-12-10 21:39:56 -08:00
mbecker20
effe737ffe periphery swarm stack deploy 2025-12-10 21:39:56 -08:00
mbecker20
436959cbcd docker service create 2025-12-10 21:39:56 -08:00
mbecker20
9957287443 remove buttons for swarm resources 2025-12-10 21:39:56 -08:00
mbecker20
c77de8683c swarm stack logs and Remove 2025-12-10 21:39:56 -08:00
mbecker20
087bcb7044 improve swarm stack page 2025-12-10 21:39:56 -08:00
mbecker20
0dfcaaf06a add swarm remove executions to procedure UI excluded types 2025-12-10 21:39:56 -08:00
mbecker20
6624a821a5 deploy 2.0.0-dev-92 2025-12-10 21:39:56 -08:00
mbecker20
c32af84e02 remove swarm entities apis 2025-12-10 21:39:56 -08:00
mbecker20
9d9add7b34 Uppercase server docker ops logs 2025-12-10 21:39:56 -08:00
mbecker20
db099dbe1e improve config with tall linkto sidebars 2025-12-10 21:39:56 -08:00
mbecker20
413a8abc84 default rate limit attempts to 5 per 15 sec 2025-12-10 21:34:30 -08:00
mbecker20
36243e83c1 KL-4 must fallback to axum extracted IP for cases not using reverse proxy 2025-12-10 21:34:30 -08:00
mbecker20
076113a1de KL-4: Align log consistency 2025-12-10 21:34:30 -08:00
mbecker20
c998edaa73 KL-1: core_allowed_origins example config should be empty 2025-12-10 21:34:30 -08:00
mbecker20
4de32b08e5 KL-8 modify action state internal behavior comments 2025-12-10 21:34:30 -08:00
mbecker20
262999c58f KL-7 Improve typescript safety: disable allow any 2025-12-10 21:34:30 -08:00
mbecker20
be57f52e6e KL-6 Improve error handling in startup paths 2025-12-10 21:34:30 -08:00
mbecker20
3591789316 KL-6 Improve alert cache error handling 2025-12-10 21:34:30 -08:00
mbecker20
83f2bd65fe KL-6 Remove monitoring_interval panic 2025-12-10 21:34:30 -08:00
mbecker20
5e5cdb81f8 KL-5 ext: Tighten the skew tolerance re https://curity.io/resources/learn/jwt-best-practices/?utm_source=chatgpt.com#9-dealing-with-time-based-claims 2025-12-10 21:34:30 -08:00
mbecker20
721038a1df KL-5 JWT clock skew tolerance 2025-12-10 21:34:30 -08:00
mbecker20
7de98ad519 cargo clippy and bump to rust 1.91.1 2025-12-10 21:34:30 -08:00
mbecker20
8c62f2b5c5 KL-4 ext 2: Improve rate limiting / attempt state conveyance with response 2025-12-10 21:34:30 -08:00
mbecker20
85787781ee KL-4 ext: Remove brute-force compromising credential failure reasons to improve auth rate limiter effectiveness 2025-12-10 21:34:30 -08:00
mbecker20
25e2d4e926 KL-4 Authentication rate limiting 2025-12-10 21:34:30 -08:00
mbecker20
b9b8d45cbc KL-2/3 Input validation for local auth, service users, api keys, and variables 2025-12-10 21:34:30 -08:00
mbecker20
2f2e863dbf KL-1 Configurable CORS support 2025-12-10 21:34:30 -08:00
mbecker20
a3a01f1625 add schemas for swarm commands 2025-11-27 23:47:22 -08:00
mbecker20
aec8fa2bf1 task view page with logs 2025-11-23 23:45:28 -08:00
mbecker20
7ff2dba82f swarm service page with logs 2025-11-23 12:48:23 -08:00
mbecker20
9c86b2d239 swarm service logs api 2025-11-23 00:43:59 -08:00
mbecker20
b1fec7c663 docker swarm stack read apis 2025-11-22 16:05:51 -08:00
mbecker20
8341c6b802 sward resources use list items 2025-11-22 02:41:49 -08:00
mbecker20
73285c4374 move swarm to list item + inspect on pages 2025-11-21 14:34:20 -08:00
mbecker20
32d48cdb02 swarm configs view 2025-11-21 13:21:14 -08:00
mbecker20
8e081fd09c swarm config apis 2025-11-21 12:55:37 -08:00
mbecker20
04531f1dea Explore swarm info 2025-11-20 13:00:29 -08:00
mbecker20
80f439d472 basic swarm 2025-11-19 18:02:50 -08:00
mbecker20
d5e03d6d16 prog on swarm 2025-11-19 01:48:43 -08:00
mbecker20
a9f55bb8e6 fix Terminals permissions when no perm on Server 2025-11-17 15:44:14 -08:00
mbecker20
9e765f93f5 bump deps 2025-11-17 15:43:56 -08:00
mbecker20
b3aa8e906f add context to onboarding login error and error messages sent over the communication websocket in general 2025-11-17 13:23:44 -08:00
mbecker20
03fe442aa0 in prog 2025-11-17 12:59:08 -08:00
ChanningHe
d268009a6a validate compose_cmd_wrapper for required placeholder and add interpolation support (#977) 2025-11-12 10:09:15 -08:00
mbecker20
f0697e812a shift + N open new variable dialog 2025-11-11 14:22:56 -08:00
mbecker20
78766463d6 create variable Enter submit 2025-11-11 14:18:28 -08:00
mbecker20
0fa1edba2c deploy 2.0.0-dev-90 2025-11-11 14:13:55 -08:00
mbecker20
bbd968cac3 bump toml pretty with fix syncing procedure executions with multiline batch patterns 2025-11-11 14:13:25 -08:00
mbecker20
5f24fc1be3 deploy 2.0.0-dev-89 2025-11-11 00:44:49 -08:00
mbecker20
7ecd2b0b0b improve cmd wrapper with comment removal support 2025-11-11 00:43:54 -08:00
mbecker20
7bf44d2e04 fix some broken tabs 2025-11-11 00:35:24 -08:00
mbecker20
24e0672384 dashboard resets page title 2025-11-11 00:18:16 -08:00
mbecker20
04f081631f deploy 2.0.0-dev-88 2025-11-11 00:16:07 -08:00
mbecker20
b1af956b63 fix dashboard pie chart code splitting issue 2025-11-11 00:05:32 -08:00
mbecker20
370712b29f gen served client types 2025-11-11 00:05:02 -08:00
mbecker20
2b6c552964 canius lite update 2025-11-11 00:04:52 -08:00
mbecker20
434a1d8ea9 clippy lint 2025-11-11 00:04:39 -08:00
ChanningHe
0b7f28360f Add optional command wrapper for Docker Compose in StackConfig (#973) 2025-11-10 23:59:09 -08:00
ChanningHe
3c8ef0ab29 Add track option for Additional Env Files (#955) 2025-11-10 23:47:07 -08:00
mbecker20
930b2423c3 deploy 2.0.0-dev-87 2025-11-07 10:33:23 -08:00
mbecker20
546747b5f2 add timeout to dns ip resolve, only use ipv4 2025-11-07 10:32:55 -08:00
mbecker20
c6df866755 better aws builder config organization 2025-11-07 10:04:45 -08:00
mbecker20
ea5e684915 better useUserTargetPermissions 2025-11-06 22:18:31 -08:00
mbecker20
64db8933de RefreshBuildCache after build 2025-11-04 00:27:34 -08:00
mbecker20
7a5580de57 builder uppercase login 2025-11-04 00:06:46 -08:00
mbecker20
b1656bb174 log about enabling user linger 2025-11-03 10:29:50 -08:00
Badal Singh
559ce103da Update setup-periphery.py (#958) 2025-11-03 09:57:49 -08:00
mbecker20
75e278a57b builder fix partial_default 2025-10-30 00:41:27 -07:00
mbecker20
430f3ddc34 fix omni search container double select on same name 2025-10-29 00:02:32 -07:00
mbecker20
6c30c202e9 add Terminals to omni search 2025-10-28 23:59:41 -07:00
mbecker20
c5401de1c5 tweak user level tab view 2025-10-28 11:42:29 -07:00
mbecker20
7a3d9e0ef6 tweak description 2025-10-28 00:32:39 -07:00
mbecker20
595e3ece42 deploy 2.0.0-dev-86 2025-10-27 21:05:13 -07:00
mbecker20
a3bc895755 fix terminal disconnect 2025-10-27 21:04:46 -07:00
mbecker20
3e3def03ec terminal init properly lexes init command 2025-10-27 21:01:15 -07:00
mbecker20
bc672d9649 deploy 2.0.0-dev-85 2025-10-27 20:01:18 -07:00
mbecker20
ea6dee4d51 clippy lint 2025-10-27 19:13:43 -07:00
mbecker20
b985f18c74 deploy 2.0.0-dev-84 2025-10-27 19:12:54 -07:00
mbecker20
45909b2f04 pid1 reaper doesn't work, init: true should be required in compose 2025-10-27 19:06:50 -07:00
mbecker20
2b5a54ce89 deploy 2.0.0-dev-83 2025-10-27 18:31:56 -07:00
mbecker20
a18f33b95e formalize the terminal message variants 2025-10-27 18:31:30 -07:00
mbecker20
f35b00ea95 bump clap dependency 2025-10-27 16:18:30 -07:00
mbecker20
70fab08520 clean up terminal modules 2025-10-27 16:17:20 -07:00
mbecker20
0331780a5f rename variables shell -> command 2025-10-27 11:08:57 -07:00
mbecker20
06cdfd2bbc Terminal -> Terminals tabs 2025-10-27 02:53:06 -07:00
mbecker20
1555202569 Create Terminal don't auto set request after changed 2025-10-27 02:42:06 -07:00
mbecker20
5139622aad deploy 2.0.0-dev-82 2025-10-27 02:28:48 -07:00
mbecker20
61ce2ee3db improve new terminal 2025-10-27 02:04:15 -07:00
mbecker20
3171c14f2b comment on spawn process reaper 2025-10-27 01:41:06 -07:00
mbecker20
521db748d8 deploy 2.0.0-dev-81 2025-10-27 01:27:42 -07:00
mbecker20
35bf224080 deploy 2.0.0-dev-80 2025-10-27 01:21:44 -07:00
mbecker20
e0b31cfe51 CreateTerminal only shows resources which are actually available to connect to 2025-10-27 00:44:56 -07:00
mbecker20
0a890078b0 deploy 2.0.0-dev-79 2025-10-27 00:38:08 -07:00
mbecker20
df97ced7a4 deploy 2.0.0-dev-78 2025-10-27 00:03:26 -07:00
mbecker20
d4e5e2e6d8 add execute_<>_terminal convenience methods 2025-10-26 23:35:17 -07:00
mbecker20
19aa60dcb5 deploy 2.0.0-dev-77 2025-10-26 23:21:15 -07:00
mbecker20
fc19c53e6f deploy 2.0.0-dev-76 2025-10-26 23:00:59 -07:00
mbecker20
4f0af960db Big Terminal refactor + most commands run directly / bypass 'sh -c "..."' 2025-10-26 23:00:35 -07:00
mbecker20
e2ec5258fb add "New" kb shortcut 2025-10-23 23:55:24 -07:00
mbecker20
49b6545a02 reorder cli command list 2025-10-23 23:53:10 -07:00
mbecker20
0aabaa9e62 deploy 2.0.0-dev-75 2025-10-23 12:23:10 -07:00
mbecker20
dc65986eab binaries still built with bullseye for compat, but final images use trixie 2025-10-23 12:22:50 -07:00
mbecker20
1d8f28437d km attach <CONTAINER> 2025-10-23 12:22:02 -07:00
mbecker20
c1502e89c2 deploy 2.0.0-dev-74 2025-10-23 11:51:40 -07:00
mbecker20
0bd15fc442 ResourceQuery.names supports names or ids 2025-10-23 11:23:37 -07:00
mbecker20
5a3621b02e km exec 2025-10-23 01:55:50 -07:00
mbecker20
38192e2dac deploy 2.0.0-dev-73 2025-10-23 00:56:15 -07:00
mbecker20
5d271d5547 use Ping timeout to handle reconnect if for some reason network cuts but ws doesn't receive Close 2025-10-23 00:55:51 -07:00
mbecker20
11fb67a35b ssh use cancel token so stdout.write_all isn't cancelled mid-write, which leads to undefined behavior 2025-10-23 00:14:17 -07:00
mbecker20
a80499dcc4 improve stack config files responsive 2025-10-22 19:02:30 -07:00
mbecker20
8c76b8487f alert responsive, better Server terminal disabled 2025-10-22 13:48:08 -07:00
mbecker20
2b32d9042a deploy 2.0.0-dev-72 2025-10-22 01:00:19 -07:00
mbecker20
dc48f1f2ca deploy 2.0.0-dev-71 2025-10-22 00:50:02 -07:00
mbecker20
8e7b7bdcf1 deploy 2.0.0-dev-70 2025-10-22 00:44:54 -07:00
mbecker20
f11d64f72e add 'init' param to make 'execute_terminal' in single call possible 2025-10-22 00:44:33 -07:00
mbecker20
2ffae85180 dashboard table section headers link to resources page 2025-10-22 00:03:12 -07:00
mbecker20
bd79d0f1e0 km ssh <SERVER> [COMMAND] -n [NAME] 2025-10-21 23:55:36 -07:00
mbecker20
e890b1f675 deploy 2.0.0-dev-69 2025-10-21 23:32:18 -07:00
mbecker20
3b7de25c30 Shift + X - Terminals, Shift + N - New (Resource, Terminal) 2025-10-21 16:11:27 -07:00
mbecker20
793bb99f31 nav to terminal on create 2025-10-21 16:00:50 -07:00
mbecker20
d465c9f273 deploy 2.0.0-dev-68 2025-10-21 15:51:38 -07:00
mbecker20
ce641a8974 terminal page 2025-10-21 15:51:18 -07:00
mbecker20
1b89ceb122 deploy 2.0.0-dev-67 2025-10-21 02:50:21 -07:00
mbecker20
2dbc011d26 remove unneeded log on client terminal disconnect 2025-10-21 02:33:19 -07:00
mbecker20
246da88ae1 deploy 2.0.0-dev-66 2025-10-21 02:29:12 -07:00
mbecker20
a8c16f64b1 km ssh 2025-10-21 02:28:42 -07:00
mbecker20
a5b711a348 stack tabs localstorage increment 2025-10-20 20:35:08 -07:00
mbecker20
9666e9ad83 Fix monitoring table with proper server version component 2025-10-20 03:01:07 -07:00
mbecker20
7479640c73 add hover information for mysterious server header icons 2025-10-20 02:53:18 -07:00
mbecker20
4823825035 give websocket indicator info on hover 2025-10-20 02:35:12 -07:00
mbecker20
23897a7acf clippy 2025-10-20 02:16:52 -07:00
mbecker20
20d5588b5c deploy 2.0.0-dev-65 2025-10-20 02:15:15 -07:00
mbecker20
f7e15ccde5 progress on terminals page 2025-10-20 02:14:51 -07:00
mbecker20
cf7623b1fc combine all resources / table view into dashboard 2025-10-20 01:40:27 -07:00
mbecker20
d3c464c05d start Terminals management page 2025-10-20 00:42:45 -07:00
mbecker20
5c9d416aa4 prog on docs update 2025-10-19 23:33:41 -07:00
mbecker20
aabcd88312 update connect-servers docs 2025-10-19 23:07:50 -07:00
mbecker20
9d2624c6bc clarify root directory in periphery config file 2025-10-19 23:07:19 -07:00
mbecker20
ee11fb0b6c clean up setup script 2025-10-19 23:07:02 -07:00
mbecker20
45adfbddd0 mounting custom CA 2025-10-19 23:06:48 -07:00
mbecker20
d26d035dc6 clean up docs intro 2025-10-19 22:03:17 -07:00
mbecker20
e673ba0adf deploy 2.0.0-dev-64 2025-10-19 21:48:15 -07:00
mbecker20
f876facfa7 improve git status message / failure propogation 2025-10-19 21:47:29 -07:00
mbecker20
3a47d57478 container class px-[1.2rem] 2025-10-19 20:31:40 -07:00
mbecker20
a707028277 responsive tweaks 2025-10-19 20:07:30 -07:00
mbecker20
0c6276c677 fix Resources / Containers mobile 2025-10-19 19:51:28 -07:00
mbecker20
fc9c6706f1 keep more descriptive settings header mobile 2025-10-19 13:24:44 -07:00
mbecker20
7674269ce9 fix user dropdown not showing username mobile 2025-10-19 13:11:34 -07:00
mbecker20
3b511c5adc improve server terminal mobile responsiveness 2025-10-19 13:00:30 -07:00
mbecker20
87221a10e9 fix mobile ContainerTerminal responsiveness 2025-10-19 12:56:11 -07:00
mbecker20
450cb6a148 fix stack config files mobile responsiveness 2025-10-19 12:46:51 -07:00
mbecker20
f252cefb21 responsive server docker tab 2025-10-19 12:37:26 -07:00
mbecker20
7855e9d688 run dkf 2025-10-19 12:30:59 -07:00
mbecker20
feb263c15f more type safe tabs 2025-10-19 12:27:55 -07:00
mbecker20
4f8d1c22cc rest of tabs also use mobile friendly 2025-10-19 12:11:11 -07:00
mbecker20
60bd47834e deploy 2.0.0-dev-63 2025-10-19 11:48:09 -07:00
mbecker20
4d632a6b61 improve resources mobile tabs responsiveness 2025-10-19 11:47:47 -07:00
mbecker20
381dd76723 deploy 2.0.0-dev-62 2025-10-19 01:37:10 -07:00
mbecker20
077e28a5fe fix ConfigList too wide on mobile 2025-10-19 01:36:50 -07:00
mbecker20
6b02aaed7d hide core pubkey copy if origin not https 2025-10-19 01:28:45 -07:00
mbecker20
e466944c05 improve mobile settings view 2025-10-19 01:24:41 -07:00
mbecker20
8ff94b7465 deploy 2.0.0-dev-61 2025-10-19 00:35:26 -07:00
mbecker20
b17df5ed7b show host public ip 2025-10-19 00:34:52 -07:00
mbecker20
207dc30206 cli is distroless, no shell / update-ca-certificates 2025-10-18 22:12:44 -07:00
mbecker20
c3eb386bdb fix copy entrypoint 2025-10-18 22:07:16 -07:00
mbecker20
4279e46892 deploy 2.0.0-dev-60 2025-10-18 12:59:19 -07:00
mbecker20
8d3d2fee12 use entrypoint scripts to make update-ca-certificates consistent when using custom CMD 2025-10-18 12:58:55 -07:00
mbecker20
1df36c4266 deploy 2.0.0-dev-59 2025-10-18 11:36:07 -07:00
mbecker20
36f7ad33c7 core and periphery images auto run update-ca-certificates on start, only need to mount in. 2025-10-18 11:35:45 -07:00
mbecker20
ec34b2c139 deploy 2.0.0-dev-58 2025-10-18 11:02:11 -07:00
mbecker20
d14c28d1f2 new otel instrumentation 2025-10-18 11:01:47 -07:00
mbecker20
68f7a0e9ce all info menu to top of settings 2025-10-18 00:45:59 -07:00
mbecker20
50f0376f0a Add Core title and public key to top of Settings 2025-10-18 00:01:41 -07:00
mbecker20
bbd53747ad fix km ps -h description 2025-10-17 17:17:18 -07:00
mbecker20
6a2adf1f83 tweak logs 2025-10-16 01:06:37 -07:00
mbecker20
128b15b94f deploy 2.0.0-dev-57 2025-10-16 00:59:46 -07:00
mbecker20
8d74b377b7 more otel refinements 2025-10-16 00:59:20 -07:00
mbecker20
d7e972e5c6 stack ui doesn't show project missing when deploying 2025-10-15 23:49:26 -07:00
mbecker20
e5cb4aac5a Fix: Webhook triggered checks linked repo branch for build, stack, sync 2025-10-15 18:06:43 -07:00
mbecker20
d0f62f8326 rework tracing events / improve opentelemetry output 2025-10-15 01:41:18 -07:00
mbecker20
47c4091a4b onboarding key uses recognizable key 2025-10-14 16:57:35 -07:00
mbecker20
973480e2b3 remove all the unnecessary instrument debug 2025-10-14 00:33:53 -07:00
mbecker20
b9e1cc87d2 remove instrument from validate_cancel_repo_build 2025-10-13 23:52:55 -07:00
mbecker20
05d20c8603 deploy 2.0.0-dev-56 2025-10-13 22:05:07 -07:00
mbecker20
fe2d68a001 fix config loading 2025-10-13 22:04:42 -07:00
mbecker20
26fd5b2a6d deploy 2.0.0-dev-55 2025-10-13 20:30:40 -07:00
mbecker20
76457bcb61 apply env / shell interpolation as *final* config loading stage, to include env vars. 2025-10-13 20:26:13 -07:00
mbecker20
ebd2c2238d bump deps 2025-10-13 19:51:05 -07:00
mbecker20
b7fc1bef7b refine default env 2025-10-13 13:53:12 -07:00
mbecker20
50b9f2e1bf deploy 2.0.0-dev-54 2025-10-13 13:06:23 -07:00
mbecker20
41ce86f6ab deploy 2.0.0-dev-53 2025-10-12 20:00:47 -07:00
mbecker20
7a21c01e52 tweak 2025-10-12 19:59:09 -07:00
mbecker20
e63e282510 small clean up 2025-10-12 19:56:15 -07:00
mbecker20
5456b36c18 deploy 2.0.0-dev-52 2025-10-12 13:55:39 -07:00
mbecker20
fcfb58a7e9 periphery with server disabled can initialize core public key file 2025-10-12 13:55:15 -07:00
mbecker20
2203004a74 move periphery in memory state to state.rs 2025-10-12 13:15:52 -07:00
mbecker20
996fb49823 periphery server_enabled /version route 2025-10-12 12:56:29 -07:00
mbecker20
35d22c77a2 Core add non authed /version route 2025-10-12 12:55:14 -07:00
mbecker20
44ab89600f Simpllify Option + Result into one encoding layer 2025-10-12 03:24:00 -07:00
mbecker20
0900e48cb8 ntfy / pushover url interpolation 2025-10-12 01:34:07 -07:00
mbecker20
c530a46a27 deploy 2.0.0-dev-51 2025-10-12 01:09:35 -07:00
mbecker20
f69c8db3ea pass through whether Periphery docker daemon connection is ok 2025-10-12 01:08:45 -07:00
mbecker20
48f2f651e1 periphery runs with logs if couldn't connect to docker daemon 2025-10-12 01:05:20 -07:00
mbecker20
bdb5b4185e rename some websocket fields 2025-10-12 00:28:55 -07:00
mbecker20
42a7b8c19b move connection queries to periphery_client 2025-10-12 00:08:59 -07:00
mbecker20
ded17e4840 more encoding refine 2025-10-12 00:05:16 -07:00
mbecker20
80fb1e6889 more on encoding 2025-10-11 14:11:07 -07:00
mbecker20
1dc861f538 fix periphery keys init when config.private_key is not explicitly defined. 2025-10-11 12:00:29 -07:00
mbecker20
3da63395fd fix EncodedOption docs 2025-10-10 00:32:05 -07:00
mbecker20
c40cbc4d77 deploy 2.0.0-dev-50 2025-10-09 23:40:42 -07:00
mbecker20
05e352e88c attach working 2025-10-09 23:28:27 -07:00
mbecker20
5884c09fb8 fix fe 2025-10-09 22:05:36 -07:00
mbecker20
f8add38043 backend for container attach 2025-10-09 21:53:26 -07:00
mbecker20
501f734e8b deploy 2.0.0-dev-49 2025-10-09 20:13:17 -07:00
mbecker20
de62732ac8 bump jwt lib 2025-10-09 20:12:51 -07:00
mbecker20
bfa61058cd remove github only managed repo webhooks feature. Not well implemented or documented, also provider specific. 2025-10-09 20:06:06 -07:00
mbecker20
72ca6d9910 deploy 2.0.0-dev-48 2025-10-09 19:36:47 -07:00
mbecker20
4d1ac32ad3 clippy 2025-10-09 18:24:50 -07:00
mbecker20
927e5959fa move encoded message schemas between core / periphery into periphery_client 2025-10-09 18:23:44 -07:00
mbecker20
37ccc6e1ef isolate encoding out of transport 2025-10-09 18:11:49 -07:00
mbecker20
deaa8754f3 slowly better ergonomics 2025-10-09 17:29:05 -07:00
mbecker20
dd8ac67c72 clippy and fmt 2025-10-09 16:26:06 -07:00
mbecker20
be4457c9cf deploy 2.0.0-dev-47 2025-10-09 15:41:06 -07:00
mbecker20
1868421815 strictly typed transport bytes encoding 2025-10-09 15:40:42 -07:00
mbecker20
366f7a12b4 Enumerated transport message types 2025-10-08 20:36:16 -07:00
mbecker20
75119370df standardize key rotation with wrapper 2025-10-08 17:45:45 -07:00
mbecker20
9e85b9d4c8 deploy 2.0.0-dev-46 2025-10-08 03:59:22 -07:00
mbecker20
8afbbf23dc deploy 2.0.0-dev-45 2025-10-08 02:48:24 -07:00
mbecker20
770a1116a1 fix: RotateCoreKeys also needs to store the new keys in mem 2025-10-08 02:48:01 -07:00
mbecker20
0b4aebbc24 periphery refresh panics if server_enabled, and core public key fails to parse. 2025-10-08 02:32:52 -07:00
mbecker20
f1696e26e4 deploy 2.0.0-dev-44 2025-10-08 02:11:38 -07:00
mbecker20
1a7b682301 RotateCoreKeys api 2025-10-08 02:11:17 -07:00
mbecker20
b0110b05aa deploy 2.0.0-dev-43 2025-10-08 00:02:34 -07:00
mbecker20
561b490f26 write files potentially containing secrets as 0600 2025-10-07 23:59:53 -07:00
mbecker20
cac1f0b42e align server standard and monitoring tables 2025-10-07 19:41:57 -07:00
mbecker20
28886fb304 fix typos 2025-10-07 14:32:00 -07:00
mbecker20
fb84d4cf7d deploy 2.0.0-dev-42 2025-10-07 01:59:04 -07:00
mbecker20
31e9624556 auto_rotate_keys config 2025-10-07 01:58:13 -07:00
mbecker20
3864bb7115 onboarding key expiry view 2025-10-07 01:16:28 -07:00
mbecker20
cea8601246 remove deleted server from onboarding key copy server 2025-10-07 00:44:36 -07:00
mbecker20
a546364bf3 deploy 2.0.0-dev-41 2025-10-07 00:29:33 -07:00
mbecker20
c8c62ea562 core public keys improve refresh 2025-10-07 00:27:47 -07:00
mbecker20
845e8780c7 improve server stats UI 2025-10-06 23:45:26 -07:00
mbecker20
db60347566 deploy 2.0.0-dev-40 2025-10-06 22:12:40 -07:00
mbecker20
c3ea0239d6 fix passkey support 2025-10-06 22:12:15 -07:00
mbecker20
e9d13449bf improve ws trait ergonomics 2025-10-06 20:02:06 -07:00
mbecker20
2daa92a639 working with safer transport message api 2025-10-06 19:16:03 -07:00
mbecker20
6473080078 deploy 2.0.0-dev-39 2025-10-06 03:14:15 -07:00
mbecker20
d3957f65dc schedule alert send before not after 2025-10-06 03:13:48 -07:00
mbecker20
cb34969f1e move skip label to be built into images 2025-10-06 03:07:11 -07:00
mbecker20
55a0a8cd05 deploy 2.0.0-dev-38 2025-10-06 02:38:42 -07:00
mbecker20
89f08372c6 CloseAlert 2025-10-06 02:38:16 -07:00
mbecker20
6a3ce2d426 config log some errors 2025-10-06 02:13:36 -07:00
mbecker20
4928378d46 fmt 2025-10-06 02:03:03 -07:00
mbecker20
eea222cfba simplify periphery config by removing option 2025-10-06 02:02:31 -07:00
mbecker20
6e9cc2dc77 deploy 2.0.0-dev-37 2025-10-06 01:40:24 -07:00
mbecker20
55d45084d0 comment 2025-10-06 01:40:00 -07:00
mbecker20
9657a44049 Improve config toml / yaml / json interpolation support 2025-10-06 01:38:48 -07:00
mbecker20
51fa9ae3c2 update setup script readme 2025-10-06 00:29:35 -07:00
mbecker20
5fd256444e improve the setup script 2025-10-05 23:53:21 -07:00
mbecker20
059716f178 deploy 2.0.0-dev-36 2025-10-05 18:23:43 -07:00
mbecker20
0bee1fe2c5 fix: connect and connect insecure are swapped 2025-10-05 18:20:29 -07:00
mbecker20
1e58c1a958 deploy 2.0.0-dev-35 2025-10-05 17:45:16 -07:00
mbecker20
ed1431db0a improve v1 downgrade 2025-10-05 17:43:46 -07:00
mbecker20
dc769ff159 document periphery_public_key 2025-10-05 17:39:11 -07:00
mbecker20
098f23ac4c configure Core -> Periphery insecure_tls 2025-10-05 17:36:07 -07:00
mbecker20
03f577d22f forgiving periphery public key parsing 2025-10-05 17:02:08 -07:00
mbecker20
95ca217362 deploy 2.0.0-dev-34 2025-10-05 16:52:33 -07:00
mbecker20
6d61045764 support KOMODO_PERIPHERY_PUBLIC_KEY 2025-10-05 16:52:00 -07:00
mbecker20
34e075eaf3 periphery support core_tls_insecure_skip_verify 2025-10-05 16:12:04 -07:00
mbecker20
232dc0bb4e deploy 2.0.0-dev-33 2025-10-05 14:59:23 -07:00
mbecker20
0cc0ee2aab load periphery_public_key 2025-10-05 14:58:52 -07:00
mbecker20
edebe925ff add km maintenance tasks aliases 2025-10-05 14:50:20 -07:00
mbecker20
5fd45bbc7b deploy 2.0.0-dev-32 2025-10-05 14:39:09 -07:00
mbecker20
0a490dadb2 rotation maintenance execution doesn't make individual updates 2025-10-05 14:38:45 -07:00
mbecker20
23847c15bc deploy 2.0.0-dev-31 2025-10-05 14:21:18 -07:00
mbecker20
0d238aee4f onboarding create_builder 2025-10-05 14:20:58 -07:00
mbecker20
98ad6cf5fa create update use uppercase 2025-10-05 13:41:42 -07:00
mbecker20
e35b81630b deploy 2.0.0-dev-30 2025-10-05 13:30:47 -07:00
mbecker20
1215852fe4 onboarding set Server tags 2025-10-05 13:24:38 -07:00
mbecker20
4164b76ff5 onboarded server needs to be enabled 2025-10-05 12:36:04 -07:00
mbecker20
26a9daffeb deploy 2.0.0-dev-29 2025-10-05 05:49:29 -07:00
mbecker20
8bb9f16e9b onboarding save copy server selection 2025-10-05 05:47:51 -07:00
mbecker20
b6eaf76497 Include templates in onboarding selector 2025-10-05 05:44:58 -07:00
mbecker20
073893da0e deploy 2.0.0-dev-28 2025-10-05 05:18:48 -07:00
mbecker20
e71547f1c2 configure server onboarding key 2025-10-05 05:17:56 -07:00
mbecker20
1991627990 move periphery public key to Server info (keep it out of resource sync) 2025-10-05 04:18:59 -07:00
mbecker20
3434d827a3 deploy 2.0.0-dev-27 2025-10-05 02:57:58 -07:00
mbecker20
1ef8b9878a rotate all server keys task 2025-10-05 02:57:27 -07:00
mbecker20
07ddaa8377 tweak 2025-10-05 01:41:44 -07:00
mbecker20
142c08cde4 deploy 2.0.0-dev-26 2025-10-05 01:19:21 -07:00
mbecker20
1aa1422faa periphery private key rotation 2025-10-05 01:18:56 -07:00
mbecker20
1394e8a6b1 Rotate Server private keys 2025-10-05 00:54:56 -07:00
mbecker20
420ee10211 tweaks 2025-10-04 23:59:14 -07:00
mbecker20
e918461dc5 refine onboarding key 2025-10-04 23:36:37 -07:00
mbecker20
4dc9ca27be refactor Periphery onboarding 2025-10-04 16:43:02 -07:00
mbecker20
f49b186f2f consolidate periphery docker apis into single mod 2025-10-04 16:17:32 -07:00
mbecker20
6e039b41f1 deploy 2.0.0-dev-25 2025-10-03 17:51:46 -07:00
mbecker20
e7cd77b022 tweaks 2025-10-03 17:06:14 -07:00
mbecker20
556cbd04c7 server onboarding flow using onboarding key 2025-10-03 17:01:58 -07:00
mbecker20
4e3d181466 default documented setup now uses Periphery -> Core setup 2025-10-03 12:55:06 -07:00
mbecker20
5d4326f46f NOT_FOUND if server not found 2025-10-03 03:17:37 -07:00
mbecker20
4bb486ad0a deploy 2.0.0-dev-24 2025-10-03 02:30:20 -07:00
mbecker20
d29c5112d8 Confirm server public key flow 2025-10-03 02:29:53 -07:00
mbecker20
d41315b8a4 don't navigate to /login for network errors 2025-10-03 01:58:23 -07:00
mbecker20
847404388c deploy 2.0.0-dev-23 2025-10-03 00:48:11 -07:00
mbecker20
eef8ec59b8 deploy 2.0.0-dev-22 2025-10-03 00:19:43 -07:00
mbecker20
9eb32f9ff5 store attempted public keys 2025-10-03 00:13:55 -07:00
mbecker20
859bfe67ef Improve Core side connection handling and fix Periphery -> Core error report 2025-10-02 23:03:58 -07:00
mbecker20
21ea469cd4 add login message 2 sec timeout 2025-10-02 16:00:45 -07:00
mbecker20
7fb902b892 deploy 2.0.0-dev-21 2025-10-02 03:12:59 -07:00
mbecker20
c9c4ac47ee fix clippy 2025-10-02 02:34:23 -07:00
mbecker20
f228cd31f3 deploy 2.0.0-dev-20 2025-10-02 02:33:33 -07:00
mbecker20
4feecb4b97 write key pem files by default when not otherwise provided. 2025-10-02 02:32:13 -07:00
mbecker20
e2680d0942 fix deploy 2025-10-01 21:35:27 -07:00
mbecker20
7422c0730d deploy 2.0.0-dev-19 2025-10-01 21:27:59 -07:00
mbecker20
37ac0dc7e3 update deploy 2025-10-01 21:17:43 -07:00
mbecker20
dccaca1df4 make sure not a config file before include as compose file 2025-10-01 20:32:52 -07:00
mbecker20
886aea4c36 deploy 2.0.0-dev-18 2025-10-01 19:48:40 -07:00
mbecker20
cbca070bae load keys from files 2025-10-01 19:41:32 -07:00
mbecker20
b4bdd401f6 fix unneeded base64 prefix 2025-10-01 02:36:26 -07:00
mbecker20
e546166240 use pkcs8 and spki for private / public key encoding, matching openssl 2025-10-01 02:25:41 -07:00
mbecker20
21689ce0ad periphery support same key gen functions 2025-09-29 23:32:47 -07:00
mbecker20
941787db64 slack client 0.5.0 2025-09-29 12:38:39 -07:00
mbecker20
d4b1aacac3 comment out 2025-09-29 02:21:07 -07:00
mbecker20
30f89461bf deploy 2.0.0-dev-17 2025-09-29 00:57:19 -07:00
mbecker20
a42d1397e9 back to bullseye (for max GLIBC compatibility) 2025-09-29 00:56:19 -07:00
mbecker20
b29313c28f deploy 2.0.0-dev-16 2025-09-29 00:47:17 -07:00
mbecker20
08a246a90c bullseye -> trixie 2025-09-29 00:46:51 -07:00
mbecker20
1a08df28d0 docs and config clean up 2025-09-29 00:06:35 -07:00
mbecker20
a226ffc256 fix json config load from interpolated 2025-09-28 23:20:04 -07:00
mbecker20
b385ee5ec3 start on docs update 2025-09-28 22:59:58 -07:00
mbecker20
c78c34357d remove unnecessary connected to core websocket log 2025-09-28 18:14:57 -07:00
mbecker20
4b7c692f00 deploy 2.0.0-dev-15 2025-09-28 18:02:18 -07:00
mbecker20
1ac98a096e bump async timing util to 1.1.0 to support for timelengths 2025-09-28 17:57:12 -07:00
mbecker20
281a2dc1ce first server configuration works with Periphery -> Core 2025-09-28 14:39:11 -07:00
mbecker20
0fe91378a6 tweak key gen output 2025-09-28 14:12:41 -07:00
mbecker20
11e76d1cf2 deploy 2.0.0-dev-14 2025-09-28 13:10:00 -07:00
mbecker20
a3bcd71105 simplify cache refresh with single periphery call 2025-09-28 13:05:45 -07:00
mbecker20
3ecc56dd76 clean up crypto provider install 2025-09-27 21:40:20 -07:00
mbecker20
7239cbb19b remove extra install crypto provider 2025-09-27 19:37:50 -07:00
mbecker20
a0540f7011 deploy 2.0.0-dev-13 2025-09-27 16:54:00 -07:00
mbecker20
37aea7605e gen types 2025-09-27 14:33:14 -07:00
mbecker20
78be913541 fix stuff after main rebase 2025-09-27 14:26:58 -07:00
mbecker20
c34f5ebf49 update config and compose envs 2025-09-27 14:23:49 -07:00
mbecker20
e5822cefb8 clean up socket handling 2025-09-27 14:23:49 -07:00
mbecker20
4baab194cf centralize the terminal stuff 2025-09-27 14:23:49 -07:00
mbecker20
a896583da6 deploy 2.0.0-dev-12 2025-09-27 14:23:49 -07:00
mbecker20
7b2674c38b deploy 2.0.0-dev-11 2025-09-27 14:23:42 -07:00
mbecker20
d1e32989e3 allow any number of simultaneous inbound / outbound connections (to different Cores) 2025-09-27 14:23:36 -07:00
mbecker20
e802bb3882 periphery support multiple core_public_keys 2025-09-27 14:23:36 -07:00
mbecker20
27a38b1bf5 periphery support multiple simultaneous core connections 2025-09-27 14:23:36 -07:00
mbecker20
2bc8a754be clean up passkey login 2025-09-27 14:23:36 -07:00
mbecker20
7a2a54bec1 dev-10 2025-09-27 14:23:36 -07:00
mbecker20
6a15150d59 don't cleanup server type builders 2025-09-27 14:23:31 -07:00
mbecker20
1b1dca76da deploy 2.0.0-dev-9 2025-09-27 14:23:31 -07:00
mbecker20
a032f0f4ff move system info to server cache 2025-09-27 14:23:25 -07:00
mbecker20
2749d49435 Core -> Periphery connection prefers noise handshake if 'core_public_key' is set 2025-09-27 14:23:25 -07:00
mbecker20
d88e42ef2d add specific server passkey support back 2025-09-27 14:23:25 -07:00
mbecker20
a370e7d121 support passkey auth for Core -> Periphery connection to remove the breaking change 2025-09-27 14:23:25 -07:00
mbecker20
d139ad2b3d always fallback to core config 'periphery_public_key' 2025-09-27 14:23:25 -07:00
mbecker20
8d2d180398 deploy 2.0.0-dev-8 2025-09-27 14:22:48 -07:00
mbecker20
37ca4ca986 fix server update hang 2025-09-27 14:22:42 -07:00
mbecker20
33e73b8543 use warn log 2025-09-27 14:22:42 -07:00
mbecker20
cf6e36e90c periphery server avoid auth fail log spam 2025-09-27 14:22:42 -07:00
mbecker20
9eb8b32f4a create and delete connections on demand 2025-09-27 14:22:42 -07:00
mbecker20
b400add6f1 deploy 2.0.0-dev-7 2025-09-27 14:22:41 -07:00
mbecker20
24adb89d25 execute container exec waits a bit for terminal to init before sending command 2025-09-27 14:22:36 -07:00
mbecker20
4674b2badb deploy 2.0.0-dev-6 2025-09-27 14:22:36 -07:00
mbecker20
65d1a69cb9 Mount ExecuteContainerExec periphery api 2025-09-27 14:22:27 -07:00
mbecker20
0da5718991 store connection channels under the connection 2025-09-27 14:22:27 -07:00
mbecker20
6b26cd120c simplify most of periphery client into bin/core 2025-09-27 14:22:27 -07:00
mbecker20
28e1bb19a4 deploy 2.0.0-dev-5 2025-09-27 14:22:27 -07:00
mbecker20
166107ac07 bail_if_not_connected 2025-09-27 14:22:21 -07:00
mbecker20
d77201880f dashboard Active include GlobalAutoUpdate 2025-09-27 14:22:21 -07:00
mbecker20
1d7629e9b2 Update server address description and placeholders 2025-09-27 14:22:21 -07:00
mbecker20
198f690ca5 Got invalid public key: {public_key} 2025-09-27 14:22:21 -07:00
mbecker20
531c79a144 deploy 2.0.0-dev-4 2025-09-27 14:22:21 -07:00
mbecker20
d685862713 improve Core - Periphery auth error messages 2025-09-27 14:22:09 -07:00
mbecker20
af0f245b5b deploy 2.0.0-dev-3 2025-09-27 14:22:09 -07:00
mbecker20
cba36861b7 deploy 2.0.0-dev-2 2025-09-27 14:22:02 -07:00
mbecker20
2c2c1d47b4 dev-2 2025-09-27 14:22:02 -07:00
mbecker20
3a6b997241 Json and JsonPretty formatting 2025-09-27 14:21:54 -07:00
mbecker20
7122f79b9d add -f json option to key utils (for use with jquery etc. 2025-09-27 14:21:54 -07:00
mbecker20
9bcee8122b tweak 2025-09-27 14:21:54 -07:00
mbecker20
a49c98946e add copy pubkeys 2025-09-27 14:21:54 -07:00
mbecker20
7d222a7241 dev-1 2025-09-27 14:21:54 -07:00
mbecker20
33501dac3e fix Core -> Periphery reconnection 2025-09-27 14:21:44 -07:00
mbecker20
4675dfa736 improve the logging 2025-09-27 14:21:44 -07:00
mbecker20
0be51dc784 move core connection handlers into core binary 2025-09-27 14:21:44 -07:00
mbecker20
52453d1320 set default allowed periphery public key 2025-09-27 14:21:44 -07:00
mbecker20
25da97ac1a basic configure auth 2025-09-27 14:21:44 -07:00
mbecker20
02db5a11d3 pipe through core side private / public key handling 2025-09-27 14:21:44 -07:00
mbecker20
89a5272246 rename passkey -> private_key 2025-09-27 14:21:44 -07:00
mbecker20
ae51ea1ad6 Copy core public key 2025-09-27 14:21:44 -07:00
mbecker20
3bdb4bea16 Core includes public key in CoreInfo 2025-09-27 14:21:44 -07:00
mbecker20
677bb14b5d auth forward error 2025-09-27 14:21:44 -07:00
mbecker20
6700700a80 clean up websocket handlers with many params 2025-09-27 14:21:44 -07:00
mbecker20
996d4aa129 standardize server size header identifier extraction 2025-09-27 14:21:44 -07:00
mbecker20
75894a7282 wire through private keys 2025-09-27 14:21:44 -07:00
mbecker20
2a065edcf1 avoid looping periphery client error 2025-09-27 14:21:44 -07:00
mbecker20
6f3703acfb periphery client makes more sense 2025-09-27 14:21:44 -07:00
mbecker20
59e989ecdf noise library and cli key utilities 2025-09-27 14:21:44 -07:00
mbecker20
951ff34a9e abstract websocket handling implementations on both sides 2025-09-27 14:21:12 -07:00
mbecker20
2d83105500 clean up 2025-09-27 14:21:12 -07:00
mbecker20
3d455f5142 implement noise auth basic 2025-09-27 14:21:12 -07:00
mbecker20
01de8c4a9b use standardized websocket wrappers / traits 2025-09-27 14:21:12 -07:00
mbecker20
d5de338561 outbound connection mode working 2025-09-27 14:21:12 -07:00
mbecker20
58c1afb8ef add login draft for transport 2025-09-27 14:21:12 -07:00
mbecker20
230f357b5a everything over ws working 2025-09-27 14:21:12 -07:00
mbecker20
991c95fff0 execute basically working, still need to clear the response channel upon completion 2025-09-27 14:21:12 -07:00
mbecker20
f6243fe6b1 more cleanup 2025-09-27 14:21:12 -07:00
mbecker20
9feeccba6e container terminal over connection 2025-09-27 14:21:12 -07:00
mbecker20
673c7f3a6b multiplex requests + terminal over single WS 2025-09-27 14:21:12 -07:00
mbecker20
39f900d651 standardize and consolidate logic in transport lib 2025-09-27 14:21:12 -07:00
mbecker20
8a06a0d6ce is work 2025-09-27 14:21:12 -07:00
mbecker20
7789ee4f4a prog 2025-09-27 14:21:12 -07:00
mbecker20
0472b6a7f7 fix after 1.19.4 2025-09-27 14:21:12 -07:00
mbecker20
d1d2227d36 prog 2025-09-27 14:21:11 -07:00
mbecker20
cea7c5fc5e prog on ws connect 2025-09-27 14:21:11 -07:00
623 changed files with 65729 additions and 26997 deletions

123
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,123 @@
# Komodo AI Coding Instructions
## Project Overview
Komodo is a distributed system for building and deploying software across multiple servers. It consists of:
- **Core** (`bin/core`): Central Rust/Axum API server managing resources, scheduling, monitoring
- **Periphery** (`bin/periphery`): Rust agent deployed to managed servers, executes docker/git operations
- **Frontend** (`frontend`): React/TypeScript SPA using TanStack Query and shadcn/ui components
- **Client Libraries** (`client/core/{rs,ts}`): Type-safe API clients generated from shared entity definitions
## Architecture Fundamentals
### Core ↔ Periphery Communication
- Core and Periphery communicate via **WebSocket connections** that can be initiated from either direction
- Periphery can connect outbound to Core (common) or Core can connect inbound to Periphery (less common)
- Connection management is in `bin/core/src/connection` and `bin/periphery/src/connection`
- Use `state::periphery_connections()` in Core to send requests to Periphery agents
### Type Generation Pipeline
**Critical**: Rust entities are the source of truth for all types across the stack:
1. Entities defined in `client/core/rs/src/entities/` with `#[typeshare]` macro
2. Run `node ./client/core/ts/generate_types.mjs` (uses typeshare CLI) → generates `client/core/ts/src/types.ts`
3. TypeScript client is built and linked to frontend via `yarn link komodo_client`
4. Frontend imports types: `import { Types } from "komodo_client"`
**When modifying entities**: Always run the `gen-client` (alias: `gc`) runfile task to regenerate TypeScript types.
### API Pattern (Resolver-Based)
Core and Periphery use `mogh_resolver::Resolve` trait for RPC-style endpoints:
- Request enums in `client/*/rs/src/api/` implement `Resolve<Response = T, Error = E>`
- Core handlers in `bin/core/src/api/{read,write,execute}/*.rs`
- Periphery handlers in `bin/periphery/src/api/*.rs`
- Router maps `/{variant}` paths to enum variants automatically
Example request handling:
```rust
#[derive(Serialize, Deserialize, Resolve)]
#[response(MyResponse)]
#[error(anyhow::Error)]
pub struct MyRequest { /* fields */ }
```
### State Management
Core maintains singleton state via `OnceLock` patterns in `bin/core/src/state.rs`:
- `db_client()`: MongoDB connection
- `periphery_connections()`: WebSocket connections to Periphery agents
- `action_states()`, `build_states()`, etc.: In-memory caches using `CloneCache<K, V>`
- Always use `state::*()` accessors, never instantiate directly
## Development Workflows
### Running Services Locally
```bash
# Start all local services (MongoDB, Core, Periphery, Frontend)
run dc # dev-core: runs Core in release mode with .dev/core.config.toml
run dp # dev-periphery: runs Periphery with .dev/periphery.config.toml
# Or use docker-compose for full stack
run dev-compose # deploys dev.compose.yaml with ferretdb
```
### Frontend Development
```bash
cd frontend && yarn dev # Vite dev server on port 5173
# After changing Rust entities, regenerate client:
run gc # gen-client: typeshare → build TS client → copy to frontend/public/client
```
### Building & Deploying
- Deno scripts in `action/` automate version bumps and deployments
- `run dk` (deploy-komodo): Full build and deploy via Komodo Action
- `run bk` (build-komodo): Build binaries without deploying
## Code Conventions
### Error Handling
- Use `anyhow::Result<T>` for application code
- Use `serror` for serializable errors sent over API boundaries
- Response helper: `response::Response::from(value)` for JSON responses
- Never use `unwrap()` in production code paths; prefer `context()` for error messages
### Naming & Structure
- **Entities**: CamelCase structs in `client/core/rs/src/entities/*/mod.rs`
- **API requests**: CamelCase request types in `client/core/rs/src/api/*.rs`
- **Config derivations**: Use `derive_builder::Builder` for config update structs
- **Variants**: `derive_variants::EnumVariants` generates `TypeVariant` enums for discriminants
### Frontend Patterns
- **API hooks**: `useRead("GetStack", { stack: "my-stack" })` for queries
- **Mutations**: `useWrite("UpdateStack", { onSuccess: () => invalidate(...) })`
- **Components**: Located in `frontend/src/components/`, use shadcn/ui primitives from `@ui/*`
- **State**: Prefer TanStack Query for server state, Jotai atoms for local UI state
## Key Files & Directories
### Must-Read for Context
- [readme.md](readme.md): Project overview and deployment info
- [runfile.toml](runfile.toml): All development task definitions
- [bin/core/src/state.rs](bin/core/src/state.rs): Core's global state singletons
- [client/core/rs/src/entities/mod.rs](client/core/rs/src/entities/mod.rs): All entity definitions
### Common Touch Points
- **Adding new resource type**: Update entities, Core API handlers, Frontend pages/components
- **Changing API**: Modify request/response types in `client/core/rs/src/api/`, regenerate client
- **Frontend features**: Usually start in `frontend/src/pages/`, add hooks in `frontend/src/lib/hooks/`
- **Periphery capabilities**: Add to `periphery_client::api` enum and implement handler in `bin/periphery/src/api/`
## Configuration
- Core config: `config/core.config.toml` (see `client/core/rs/src/entities/config/core.rs` for schema)
- Periphery config: `config/periphery.config.toml`
- Environment variable pattern: Settings support `_FILE` suffix for Docker secrets (see `lib/environment_file`)
## Testing & Validation
- No comprehensive test suite currently
- Validate changes by running locally with `run dc` / `run dp`
- Check Core logs for startup errors (connection to DB, Periphery agents)
- Frontend errors appear in browser console and TanStack Query devtools
## Common Pitfalls
- **Forgetting to regenerate TS types** after Rust entity changes → frontend type errors
- **Not using `state::*()` accessors** → panics from uninitialized `OnceLock`
- **Breaking WebSocket message format** between Core/Periphery → connection failures
- **Yarn link issues**: If frontend can't find `komodo_client`, re-run `run link-client`

4
.gitignore vendored
View File

@@ -6,8 +6,4 @@ deno.lock
.env.development
.DS_Store
.idea
/frontend/build
/lib/ts_client/build
.dev

View File

@@ -3,8 +3,8 @@
"scope": "rust",
"prefix": "resolve",
"body": [
"impl Resolve<${1}, User> for State {",
"\tasync fn resolve(&self, ${1} { ${0} }: ${1}, _: User) -> anyhow::Result<${2}> {",
"impl Resolve<${0}> for ${1} {",
"\tasync fn resolve(self, _: &${0}) -> Result<Self::Response, Self::Error> {",
"\t\ttodo!()",
"\t}",
"}"
@@ -15,9 +15,9 @@
"prefix": "static",
"body": [
"fn ${1}() -> &'static ${2} {",
"\tstatic ${3}: OnceLock<${2}> = OnceLock::new();",
"\t${3}.get_or_init(|| {",
"\t\t${0}",
"\tstatic ${0}: OnceLock<${2}> = OnceLock::new();",
"\t${0}.get_or_init(|| {",
"\t\ttodo!()",
"\t})",
"}"
]

3476
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ members = [
]
[workspace.package]
version = "1.19.5"
version = "2.0.0-dev-114"
edition = "2024"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -22,118 +22,117 @@ strip = "debuginfo"
# LOCAL
komodo_client = { path = "client/core/rs" }
periphery_client = { path = "client/periphery/rs" }
environment_file = { path = "lib/environment_file" }
environment = { path = "lib/environment" }
interpolate = { path = "lib/interpolate" }
formatting = { path = "lib/formatting" }
transport = { path = "lib/transport" }
database = { path = "lib/database" }
response = { path = "lib/response" }
encoding = { path = "lib/encoding" }
command = { path = "lib/command" }
config = { path = "lib/config" }
logger = { path = "lib/logger" }
cache = { path = "lib/cache" }
git = { path = "lib/git" }
# MOGH
run_command = { version = "0.0.6", features = ["async_tokio"] }
serror = { version = "0.5.1", default-features = false }
slack = { version = "0.4.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
slack = { version = "2.0.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
mogh_error = { version = "1.0.3", default-features = false }
derive_default_builder = "0.1.8"
derive_empty_traits = "0.1.0"
async_timing_util = "1.0.0"
partial_derive2 = "0.4.3"
derive_variants = "1.0.0"
async_timing_util = "1.1.0"
mogh_auth_client = "1.2.1"
mogh_auth_server = "1.2.4"
mogh_secret_file = "1.0.0"
mogh_validations = "1.0.0"
mogh_rate_limit = "1.0.0"
partial_derive2 = "0.4.5"
mongo_indexed = "2.0.2"
resolver_api = "3.0.0"
toml_pretty = "1.2.0"
mogh_resolver = "1.0.0"
mogh_config = "1.0.1"
mogh_logger = "1.3.0"
mogh_server = "1.3.0"
toml_pretty = "2.0.0"
mogh_cache = "1.1.1"
mogh_pki = "1.1.0"
mungos = "3.2.2"
svi = "1.2.0"
# ASYNC
reqwest = { version = "0.12.23", default-features = false, features = ["json", "stream", "rustls-tls-native-roots"] }
tokio = { version = "1.47.1", features = ["full"] }
tokio-util = { version = "0.7.16", features = ["io", "codec"] }
tokio-stream = { version = "0.1.17", features = ["sync"] }
reqwest = { version = "0.13.1", default-features = false, features = ["json", "stream", "form", "query", "rustls"] }
tokio = { version = "1.49.0", features = ["full"] }
tokio-util = { version = "0.7.18", features = ["io", "codec"] }
tokio-stream = { version = "0.1.18", features = ["sync"] }
pin-project-lite = "0.2.16"
futures = "0.3.31"
futures-util = "0.3.31"
arc-swap = "1.7.1"
arc-swap = "1.8.0"
# SERVER
tokio-tungstenite = { version = "0.27.0", features = ["rustls-tls-native-roots"] }
axum-extra = { version = "0.10.1", features = ["typed-header"] }
tower-http = { version = "0.6.6", features = ["fs", "cors"] }
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
axum = { version = "0.8.4", features = ["ws", "json", "macros"] }
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-native-roots"] }
axum = { version = "0.8.8", features = ["ws", "json", "macros"] }
axum-extra = { version = "0.12.5", features = ["typed-header"] }
# OPENAPI
utoipa-scalar = { version = "0.3.0", features = ["axum"] }
utoipa = "5.4.0"
# SER/DE
ipnetwork = { version = "0.21.1", features = ["serde"] }
indexmap = { version = "2.11.1", features = ["serde"] }
serde = { version = "1.0.219", features = ["derive"] }
indexmap = { version = "2.13.0", features = ["serde"] }
serde = { version = "1.0.227", features = ["derive"] }
strum = { version = "0.27.2", features = ["derive"] }
bson = { version = "2.15.0" } # must keep in sync with mongodb version
toml = "0.9.11"
serde_yaml_ng = "0.10.0"
serde_json = "1.0.145"
serde_json = "1.0.148"
serde_qs = "0.15.0"
toml = "0.9.5"
url = "2.5.7"
# ERROR
anyhow = "1.0.99"
thiserror = "2.0.16"
anyhow = "1.0.100"
thiserror = "2.0.18"
# LOGGING
opentelemetry-otlp = { version = "0.30.0", features = ["tls-roots", "reqwest-rustls"] }
opentelemetry_sdk = { version = "0.30.0", features = ["rt-tokio"] }
tracing-subscriber = { version = "0.3.20", features = ["json"] }
opentelemetry-semantic-conventions = "0.30.0"
tracing-opentelemetry = "0.31.0"
opentelemetry = "0.30.0"
tracing = "0.1.41"
tracing = "0.1.44"
# CONFIG
clap = { version = "4.5.47", features = ["derive"] }
clap = { version = "4.5.54", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
# CRYPTO / AUTH
uuid = { version = "1.18.1", features = ["v4", "fast-rng", "serde"] }
jsonwebtoken = { version = "9.3.1", default-features = false }
openidconnect = "4.0.1"
uuid = { version = "1.19.0", features = ["v4", "fast-rng", "serde"] }
rustls = { version = "0.23.36", features = ["aws-lc-rs"] }
data-encoding = "2.9.0"
urlencoding = "2.1.3"
nom_pem = "4.0.0"
bcrypt = "0.17.1"
base64 = "0.22.1"
rustls = "0.23.31"
bcrypt = "0.18.0"
hmac = "0.12.1"
sha1 = "0.10.6"
sha2 = "0.10.9"
rand = "0.9.2"
hex = "0.4.3"
# SYSTEM
hickory-resolver = "0.25.2"
portable-pty = "0.9.0"
bollard = "0.19.2"
sysinfo = "0.37.0"
shell-escape = "0.1.5"
crossterm = "0.29.0"
bollard = "0.20.0"
sysinfo = "0.37.1"
shlex = "1.3.0"
# CLOUD
aws-config = "1.8.6"
aws-sdk-ec2 = "1.167.0"
aws-credential-types = "1.2.6"
aws-config = "1.8.12"
aws-sdk-ec2 = "1.200.0"
aws-credential-types = "1.2.11"
## CRON
english-to-cron = "0.1.6"
english-to-cron = "0.1.7"
chrono-tz = "0.10.4"
chrono = "0.4.42"
croner = "3.0.0"
chrono = "0.4.43"
croner = "3.0.1"
# MISC
async-compression = { version = "0.4.30", features = ["tokio", "gzip"] }
async-compression = { version = "0.4.37", features = ["tokio", "gzip"] }
derive_builder = "0.20.2"
comfy-table = "7.2.1"
typeshare = "1.0.4"
octorust = "0.10.0"
dashmap = "6.1.0"
comfy-table = "7.2.2"
typeshare = "1.0.5"
wildcard = "0.3.0"
colored = "3.0.0"
regex = "1.11.2"
bytes = "1.10.1"
shell-escape = "0.1.5"
bytes = "1.11.0"
regex = "1.12.2"

2
action/build.ts Normal file
View File

@@ -0,0 +1,2 @@
import { run } from "./run.ts";
await run("build-komodo");

5
action/deno.json Normal file
View File

@@ -0,0 +1,5 @@
{
"imports": {
"@std/toml": "jsr:@std/toml"
}
}

4
action/deploy-fe.ts Normal file
View File

@@ -0,0 +1,4 @@
const cmd = "km run -y action deploy-komodo-fe-change";
new Deno.Command("bash", {
args: ["-c", cmd],
}).spawn();

2
action/deploy.ts Executable file
View File

@@ -0,0 +1,2 @@
import { run } from "./run.ts";
await run("deploy-komodo");

52
action/run.ts Normal file
View File

@@ -0,0 +1,52 @@
import * as TOML from "@std/toml";
export const run = async (action: string) => {
const branch = await new Deno.Command("bash", {
args: ["-c", "git rev-parse --abbrev-ref HEAD"],
})
.output()
.then((r) => new TextDecoder("utf-8").decode(r.stdout).trim());
const cargo_toml_str = await Deno.readTextFile("Cargo.toml");
const prev_version = (
TOML.parse(cargo_toml_str) as {
workspace: { package: { version: string } };
}
).workspace.package.version;
const [version, tag, count] = prev_version.split("-");
const next_count = Number(count) + 1;
const next_version = `${version}-${tag}-${next_count}`;
await Deno.writeTextFile(
"Cargo.toml",
cargo_toml_str.replace(
`version = "${prev_version}"`,
`version = "${next_version}"`
)
);
// Cargo check first here to make sure lock file is updated before commit.
const cmd = `
cargo check
echo ""
git add --all
git commit --all --message "deploy ${version}-${tag}-${next_count}"
echo ""
git push
echo ""
km run -y action ${action} "KOMODO_BRANCH=${branch}&KOMODO_VERSION=${version}&KOMODO_TAG=${tag}-${next_count}"
`
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0 && !line.startsWith("//"))
.join(" && ");
new Deno.Command("bash", {
args: ["-c", cmd],
}).spawn();
};

View File

@@ -1,7 +1,7 @@
## Builds the Komodo Core, Periphery, and Util binaries
## for a specific architecture.
## for a specific architecture. Requires OpenSSL 3 or later.
FROM rust:1.89.0-bullseye AS builder
FROM rust:1.93.0-bookworm AS builder
RUN cargo install cargo-strip
WORKDIR /builder
@@ -27,6 +27,6 @@ COPY --from=builder /builder/target/release/core /core
COPY --from=builder /builder/target/release/periphery /periphery
COPY --from=builder /builder/target/release/km /km
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
LABEL org.opencontainers.image.description="Komodo Binaries"
LABEL org.opencontainers.image.licenses=GPL-3.0
LABEL org.opencontainers.image.licenses="GPL-3.0"

View File

@@ -1,9 +1,9 @@
## Builds the Komodo Core, Periphery, and Util binaries
## for a specific architecture.
## for a specific architecture. Requires OpenSSL 3 or later.
## Uses chef for dependency caching to help speed up back-to-back builds.
FROM lukemathwalker/cargo-chef:latest-rust-1.89.0-bullseye AS chef
FROM lukemathwalker/cargo-chef:latest-rust-1.90.0-bookworm AS chef
WORKDIR /builder
# Plan just the RECIPE to see if things have changed
@@ -31,6 +31,6 @@ COPY --from=builder /builder/target/release/core /core
COPY --from=builder /builder/target/release/periphery /periphery
COPY --from=builder /builder/target/release/km /km
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
LABEL org.opencontainers.image.description="Komodo Binaries"
LABEL org.opencontainers.image.licenses=GPL-3.0
LABEL org.opencontainers.image.licenses="GPL-3.0"

View File

@@ -14,15 +14,18 @@ path = "src/main.rs"
[dependencies]
# local
environment_file.workspace = true
komodo_client.workspace = true
komodo_client = { workspace = true, features = ["cli"] }
mogh_secret_file.workspace = true
mogh_pki.workspace = true
database.workspace = true
config.workspace = true
logger.workspace = true
mogh_config.workspace = true
mogh_logger.workspace = true
# external
futures-util.workspace = true
comfy-table.workspace = true
tokio-util.workspace = true
serde_json.workspace = true
crossterm.workspace = true
serde_qs.workspace = true
wildcard.workspace = true
tracing.workspace = true

View File

@@ -1,4 +1,4 @@
FROM rust:1.89.0-bullseye AS builder
FROM rust:1.93.0-bullseye AS builder
RUN cargo install cargo-strip
WORKDIR /builder
@@ -20,6 +20,6 @@ ENV KOMODO_CLI_CONFIG_PATHS="/config"
CMD [ "km" ]
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
LABEL org.opencontainers.image.description="Komodo CLI"
LABEL org.opencontainers.image.licenses=GPL-3.0
LABEL org.opencontainers.image.licenses="GPL-3.0"

View File

@@ -24,6 +24,6 @@ ENV KOMODO_CLI_CONFIG_PATHS="/config"
CMD [ "km" ]
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
LABEL org.opencontainers.image.description="Komodo CLI"
LABEL org.opencontainers.image.licenses=GPL-3.0
LABEL org.opencontainers.image.licenses="GPL-3.0"

View File

@@ -13,6 +13,6 @@ ENV KOMODO_CLI_CONFIG_PATHS="/config"
CMD [ "km" ]
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
LABEL org.opencontainers.image.description="Komodo CLI"
LABEL org.opencontainers.image.licenses=GPL-3.0
LABEL org.opencontainers.image.licenses="GPL-3.0"

View File

@@ -61,7 +61,8 @@ async fn list_containers(
.map(|s| (s.id.clone(), s))
.collect::<HashMap<_, _>>())),
client.read(ListAllDockerContainers {
servers: Default::default()
servers: Default::default(),
containers: Default::default(),
}),
)?;
@@ -145,7 +146,8 @@ pub async fn inspect_container(
.map(|s| (s.id.clone(), s))
.collect::<HashMap<_, _>>())),
client.read(ListAllDockerContainers {
servers: Default::default()
servers: Default::default(),
containers: Default::default()
}),
)?;

View File

@@ -2,6 +2,7 @@ use std::path::Path;
use anyhow::Context;
use colored::Colorize;
use database::mungos::mongodb::bson::{Document, doc};
use komodo_client::entities::{
config::cli::args::database::DatabaseCommand, optional_string,
};
@@ -21,6 +22,7 @@ pub async fn handle(command: &DatabaseCommand) -> anyhow::Result<()> {
DatabaseCommand::Copy { yes, index, .. } => {
copy(*index, *yes).await
}
DatabaseCommand::V1Downgrade { yes } => v1_downgrade(*yes).await,
}
}
@@ -318,3 +320,52 @@ async fn copy(index: bool, yes: bool) -> anyhow::Result<()> {
database::utils::copy(&source_db, &target_db).await
}
async fn v1_downgrade(yes: bool) -> anyhow::Result<()> {
let config = cli_config();
println!(
"\n🦎 {} Database {} 🦎",
"Komodo".bold(),
"V1 Downgrade".purple().bold()
);
println!(
"\n{}\n",
" - Downgrade the database to V1 compatible data structures."
.dimmed()
);
if let Some(uri) = optional_string(&config.database.uri) {
println!("{}: {}", " - URI".dimmed(), sanitize_uri(&uri));
}
if let Some(address) = optional_string(&config.database.address) {
println!("{}: {address}", " - Address".dimmed());
}
if let Some(username) = optional_string(&config.database.username) {
println!("{}: {username}", " - Username".dimmed());
}
println!(
"{}: {}\n",
" - Db Name".dimmed(),
config.database.db_name,
);
crate::command::wait_for_enter("run downgrade", yes)?;
let db = database::init(&config.database).await?;
db.collection::<Document>("Server")
.update_many(doc! {}, doc! { "$set": { "info": null } })
.await
.context("Failed to downgrade Server schema")?;
db.collection::<Document>("Deployment")
.update_many(doc! {}, doc! { "$set": { "info": null } })
.await
.context("Failed to downgrade Deployment schema")?;
info!(
"V1 Downgrade complete. Ready to downgrade to komodo-core:1 ✅"
);
Ok(())
}

View File

@@ -221,6 +221,33 @@ pub async fn handle(
Execution::SendAlert(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RemoveSwarmNodes(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RemoveSwarmStacks(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RemoveSwarmServices(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CreateSwarmConfig(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RotateSwarmConfig(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RemoveSwarmConfigs(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CreateSwarmSecret(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RotateSwarmSecret(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RemoveSwarmSecrets(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::ClearRepoCache(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -230,6 +257,12 @@ pub async fn handle(
Execution::GlobalAutoUpdate(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RotateAllServerKeys(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RotateCoreKeys(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::Sleep(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -482,6 +515,42 @@ pub async fn handle(
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RemoveSwarmNodes(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RemoveSwarmStacks(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RemoveSwarmServices(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::CreateSwarmConfig(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RotateSwarmConfig(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RemoveSwarmConfigs(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::CreateSwarmSecret(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RotateSwarmSecret(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RemoveSwarmSecrets(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::ClearRepoCache(request) => client
.execute(request)
.await
@@ -494,6 +563,14 @@ pub async fn handle(
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RotateAllServerKeys(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RotateCoreKeys(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::Sleep(request) => {
let duration =
Duration::from_millis(request.duration_ms as u64);

View File

@@ -7,7 +7,7 @@ use komodo_client::{
api::read::{
ListActions, ListAlerters, ListBuilders, ListBuilds,
ListDeployments, ListProcedures, ListRepos, ListResourceSyncs,
ListSchedules, ListServers, ListStacks, ListTags,
ListSchedules, ListServers, ListStacks, ListTags, ListTerminals,
},
entities::{
ResourceTargetVariant,
@@ -35,6 +35,7 @@ use komodo_client::{
ResourceSyncListItem, ResourceSyncListItemInfo,
ResourceSyncState,
},
terminal::Terminal,
},
};
use serde::Serialize;
@@ -74,15 +75,18 @@ pub async fn handle(list: &args::list::List) -> anyhow::Result<()> {
Some(ListCommand::Syncs(filters)) => {
list_resources::<ResourceSyncListItem>(filters, false).await
}
Some(ListCommand::Terminals(filters)) => {
list_terminals(filters).await
}
Some(ListCommand::Schedules(filters)) => {
list_schedules(filters).await
}
Some(ListCommand::Builders(filters)) => {
list_resources::<BuilderListItem>(filters, false).await
}
Some(ListCommand::Alerters(filters)) => {
list_resources::<AlerterListItem>(filters, false).await
}
Some(ListCommand::Schedules(filters)) => {
list_schedules(filters).await
}
}
}
@@ -189,6 +193,26 @@ where
Ok(())
}
async fn list_terminals(
filters: &ResourceFilters,
) -> anyhow::Result<()> {
let client = crate::command::komodo_client().await?;
// let query = ResourceQuery::builder()
// .tags(filters.tags.clone())
// .templates(TemplatesQueryBehavior::Exclude)
// .build();
let terminals = client
.read(ListTerminals {
target: None,
use_names: true,
})
.await?;
if !terminals.is_empty() {
print_items(terminals, filters.format, filters.links)?;
}
Ok(())
}
async fn list_schedules(
filters: &ResourceFilters,
) -> anyhow::Result<()> {
@@ -794,7 +818,7 @@ impl PrintTable for ResourceListItem<ServerListItemInfo> {
Cell::new(self.info.state.to_string())
.fg(color)
.add_attribute(Attribute::Bold),
Cell::new(self.info.address),
Cell::new(self.info.address.as_deref().unwrap_or("inbound")),
Cell::new(self.tags.join(", ")),
];
if links {
@@ -1134,6 +1158,28 @@ impl PrintTable for ResourceListItem<AlerterListItemInfo> {
}
}
impl PrintTable for Terminal {
fn header(_links: bool) -> &'static [&'static str] {
&["Terminal", "Target", "Command", "Size", "Created"]
}
fn row(self, _links: bool) -> Vec<comfy_table::Cell> {
vec![
Cell::new(self.name).add_attribute(Attribute::Bold),
Cell::new(format!("{:?}", self.target)),
Cell::new(self.command),
Cell::new(if self.stored_size_kb < 1.0 {
format!("{:.1} KiB", self.stored_size_kb)
} else {
format!("{:.} KiB", self.stored_size_kb)
}),
Cell::new(
format_timetamp(self.created_at)
.unwrap_or_else(|_| String::from("Invalid created at")),
),
]
}
}
impl PrintTable for Schedule {
fn header(links: bool) -> &'static [&'static str] {
if links {
@@ -1146,7 +1192,7 @@ impl PrintTable for Schedule {
let next_run = if let Some(ts) = self.next_scheduled_run {
Cell::new(
format_timetamp(ts)
.unwrap_or(String::from("Invalid next ts")),
.unwrap_or_else(|_| String::from("Invalid next ts")),
)
.add_attribute(Attribute::Bold)
} else {

View File

@@ -18,6 +18,7 @@ pub mod container;
pub mod database;
pub mod execute;
pub mod list;
pub mod terminal;
pub mod update;
async fn komodo_client() -> anyhow::Result<&'static KomodoClient> {

View File

@@ -0,0 +1,334 @@
use anyhow::{Context, anyhow};
use colored::Colorize;
use komodo_client::{
api::{
read::{ListAllDockerContainers, ListServers},
terminal::InitTerminal,
},
entities::{
config::cli::args::terminal::{Attach, Connect, Exec},
server::ServerQuery,
terminal::{
ContainerTerminalMode, TerminalRecreateMode,
TerminalResizeMessage, TerminalStdinMessage,
},
},
ws::terminal::TerminalWebsocket,
};
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
use tokio_util::sync::CancellationToken;
pub async fn handle_connect(
Connect {
server,
name,
command,
recreate,
}: &Connect,
) -> anyhow::Result<()> {
handle_terminal_forwarding(async {
super::komodo_client()
.await?
.connect_server_terminal(
server.to_string(),
Some(name.to_string()),
Some(InitTerminal {
command: command.clone(),
recreate: if *recreate {
TerminalRecreateMode::Always
} else {
TerminalRecreateMode::DifferentCommand
},
mode: None,
}),
)
.await
})
.await
}
pub async fn handle_exec(
Exec {
server,
container,
shell,
recreate,
}: &Exec,
) -> anyhow::Result<()> {
let server = get_server(server.clone(), container).await?;
handle_terminal_forwarding(async {
super::komodo_client()
.await?
.connect_container_terminal(
server,
container.to_string(),
None,
Some(InitTerminal {
command: Some(shell.to_string()),
recreate: if *recreate {
TerminalRecreateMode::Always
} else {
TerminalRecreateMode::DifferentCommand
},
mode: Some(ContainerTerminalMode::Exec),
}),
)
.await
})
.await
}
pub async fn handle_attach(
Attach {
server,
container,
recreate,
}: &Attach,
) -> anyhow::Result<()> {
let server = get_server(server.clone(), container).await?;
handle_terminal_forwarding(async {
super::komodo_client()
.await?
.connect_container_terminal(
server,
container.to_string(),
None,
Some(InitTerminal {
command: None,
recreate: if *recreate {
TerminalRecreateMode::Always
} else {
TerminalRecreateMode::DifferentCommand
},
mode: Some(ContainerTerminalMode::Attach),
}),
)
.await
})
.await
}
async fn get_server(
server: Option<String>,
container: &str,
) -> anyhow::Result<String> {
if let Some(server) = server {
return Ok(server);
}
let client = super::komodo_client().await?;
let mut containers = client
.read(ListAllDockerContainers {
servers: Default::default(),
containers: vec![container.to_string()],
})
.await?;
if containers.is_empty() {
return Err(anyhow!(
"Did not find any container matching {container}"
));
}
if containers.len() == 1 {
return containers
.pop()
.context("Shouldn't happen")?
.server_id
.context("Container doesn't have server_id");
}
let servers = containers
.into_iter()
.flat_map(|container| container.server_id)
.collect::<Vec<_>>();
let servers = client
.read(ListServers {
query: ServerQuery::builder().names(servers).build(),
})
.await?
.into_iter()
.map(|server| format!("\t- {}", server.name.bold()))
.collect::<Vec<_>>()
.join("\n");
Err(anyhow!(
"Multiple containers matching '{}' on Servers:\n{servers}",
container.bold(),
))
}
async fn handle_terminal_forwarding<
C: Future<Output = anyhow::Result<TerminalWebsocket>>,
>(
connect: C,
) -> anyhow::Result<()> {
// Need to forward multiple sources into ws write
let (write_tx, mut write_rx) =
tokio::sync::mpsc::channel::<TerminalStdinMessage>(1024);
// ================
// SETUP RESIZING
// ================
// Subscribe to SIGWINCH for resize messages
let mut sigwinch = tokio::signal::unix::signal(
tokio::signal::unix::SignalKind::window_change(),
)
.context("failed to register SIGWINCH handler")?;
// Send first resize messsage, bailing if it fails to get the size.
write_tx.send(resize_message()?).await?;
let cancel = CancellationToken::new();
let forward_resize = async {
while future_or_cancel(sigwinch.recv(), &cancel)
.await
.flatten()
.is_some()
{
if let Ok(resize_message) = resize_message()
&& write_tx.send(resize_message).await.is_err()
{
break;
}
}
cancel.cancel();
};
let forward_stdin = async {
let mut stdin = tokio::io::stdin();
let mut buf = [0u8; 8192];
while let Some(Ok(n)) =
future_or_cancel(stdin.read(&mut buf), &cancel).await
{
// EOF
if n == 0 {
break;
}
let bytes = &buf[..n];
// Check for disconnect sequence (alt + q)
if bytes == [197, 147] {
break;
}
// Forward bytes
if write_tx
.send(TerminalStdinMessage::Forward(bytes.to_vec()))
.await
.is_err()
{
break;
};
}
cancel.cancel();
};
// =====================
// CONNECT AND FORWARD
// =====================
let (mut ws_write, mut ws_read) = connect.await?.split();
let forward_write = async {
while let Some(message) =
future_or_cancel(write_rx.recv(), &cancel).await.flatten()
{
if let Err(e) = ws_write.send_stdin_message(message).await {
cancel.cancel();
return Some(e);
};
}
cancel.cancel();
None
};
let forward_read = async {
let mut stdout = tokio::io::stdout();
while let Some(msg) =
future_or_cancel(ws_read.receive_stdout(), &cancel).await
{
let bytes = match msg {
Ok(Some(bytes)) => bytes,
Ok(None) => break,
Err(e) => {
cancel.cancel();
return Some(e.context("Websocket read error"));
}
};
if let Err(e) = stdout
.write_all(&bytes)
.await
.context("Failed to write text to stdout")
{
cancel.cancel();
return Some(e);
}
let _ = stdout.flush().await;
}
cancel.cancel();
None
};
let guard = RawModeGuard::enable_raw_mode()?;
let (_, _, write_error, read_error) = tokio::join!(
forward_resize,
forward_stdin,
forward_write,
forward_read
);
drop(guard);
if let Some(e) = write_error {
eprintln!("\nFailed to forward stdin | {e:#}");
}
if let Some(e) = read_error {
eprintln!("\nFailed to forward stdout | {e:#}");
}
println!("\n\n{} {}", "connection".bold(), "closed".red().bold());
// It doesn't seem to exit by itself after the raw mode stuff.
std::process::exit(0)
}
fn resize_message() -> anyhow::Result<TerminalStdinMessage> {
let (cols, rows) = crossterm::terminal::size()
.context("Failed to get terminal size")?;
Ok(TerminalStdinMessage::Resize(TerminalResizeMessage {
rows,
cols,
}))
}
struct RawModeGuard;
impl RawModeGuard {
fn enable_raw_mode() -> anyhow::Result<Self> {
crossterm::terminal::enable_raw_mode()
.context("Failed to enable terminal raw mode")?;
Ok(Self)
}
}
impl Drop for RawModeGuard {
fn drop(&mut self) {
if let Err(e) = crossterm::terminal::disable_raw_mode() {
eprintln!("Failed to disable terminal raw mode | {e:?}");
}
}
}
async fn future_or_cancel<T, F: Future<Output = T>>(
fut: F,
cancel: &CancellationToken,
) -> Option<T> {
tokio::select! {
res = fut => Some(res),
_ = cancel.cancelled() => None
}
}

View File

@@ -26,6 +26,9 @@ pub async fn update(
UpdateUserCommand::SuperAdmin { enabled, yes } => {
update_super_admin(username, *enabled, *yes).await
}
UpdateUserCommand::Clear2fa { yes } => {
clear_2fa(username, *yes).await
}
}
}
@@ -120,3 +123,20 @@ async fn update_super_admin(
Ok(())
}
async fn clear_2fa(username: &str, yes: bool) -> anyhow::Result<()> {
println!("\n{}: Clear 2FA Methods\n", "Mode".dimmed());
println!(" - {}: {username}", "Username".dimmed());
crate::command::wait_for_enter("clear user 2FA methods", yes)?;
info!("Clearing 2FA methods...");
let db = database::Client::new(&cli_config().database).await?;
db.clear_user_2fa_methods(username).await?;
info!("2FA methods cleared ✅");
Ok(())
}

View File

@@ -3,7 +3,6 @@ use std::{path::PathBuf, sync::OnceLock};
use anyhow::Context;
use clap::Parser;
use colored::Colorize;
use environment_file::maybe_read_item_from_file;
use komodo_client::entities::{
config::{
DatabaseConfig,
@@ -14,6 +13,7 @@ use komodo_client::entities::{
},
logger::LogConfig,
};
use mogh_secret_file::maybe_read_item_from_file;
pub fn cli_args() -> &'static CliArgs {
static CLI_ARGS: OnceLock<CliArgs> = OnceLock::new();
@@ -28,7 +28,7 @@ pub fn cli_env() -> &'static Env {
{
Ok(env) => env,
Err(e) => {
panic!("{e:?}");
panic!("{e:?}")
}
}
})
@@ -74,7 +74,7 @@ pub fn cli_config() -> &'static CliConfig {
"Config File Keywords".dimmed(),
);
}
let mut unparsed_config = (config::ConfigLoader {
let mut unparsed_config = (mogh_config::ConfigLoader {
paths: &config_paths
.iter()
.map(PathBuf::as_path)
@@ -166,7 +166,7 @@ pub fn cli_config() -> &'static CliConfig {
else {
panic!("Profile config is not Object type.");
};
config::merge_config(
mogh_config::merge_config(
unparsed_config,
profile_config.clone(),
env.komodo_cli_merge_nested_config,
@@ -261,12 +261,18 @@ pub fn cli_config() -> &'static CliConfig {
.komodo_cli_logging_pretty
.unwrap_or(config.cli_logging.pretty),
location: false,
ansi: env
.komodo_cli_logging_ansi
.unwrap_or(config.cli_logging.ansi),
otlp_endpoint: env
.komodo_cli_logging_otlp_endpoint
.unwrap_or(config.cli_logging.otlp_endpoint),
opentelemetry_service_name: env
.komodo_cli_logging_opentelemetry_service_name
.unwrap_or(config.cli_logging.opentelemetry_service_name),
opentelemetry_scope_name: env
.komodo_cli_logging_opentelemetry_scope_name
.unwrap_or(config.cli_logging.opentelemetry_scope_name),
},
profile: config.profile,
}

View File

@@ -2,6 +2,7 @@
extern crate tracing;
use anyhow::Context;
use colored::Colorize;
use komodo_client::entities::config::cli::args;
use crate::config::cli_config;
@@ -11,7 +12,7 @@ mod config;
async fn app() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
logger::init(&config::cli_config().cli_logging)?;
mogh_logger::init(&config::cli_config().cli_logging)?;
let args = config::cli_args();
let env = config::cli_env();
let debug_load =
@@ -54,6 +55,18 @@ async fn app() -> anyhow::Result<()> {
args::Command::Update { command } => {
command::update::handle(command).await
}
args::Command::Connect(connect) => {
command::terminal::handle_connect(connect).await
}
args::Command::Exec(exec) => {
command::terminal::handle_exec(exec).await
}
args::Command::Attach(attach) => {
command::terminal::handle_attach(attach).await
}
args::Command::Key { command } => {
mogh_pki::cli::handle(command, mogh_pki::PkiKind::Mutual).await
}
args::Command::Database { command } => {
command::database::handle(command).await
}
@@ -66,7 +79,18 @@ async fn main() -> anyhow::Result<()> {
tokio::signal::unix::SignalKind::terminate(),
)?;
tokio::select! {
res = tokio::spawn(app()) => res?,
_ = term_signal.recv() => Ok(()),
res = tokio::spawn(app()) => match res {
Ok(Err(e)) => {
eprintln!("{}: {e}", "ERROR".red());
std::process::exit(1)
}
Err(e) => {
eprintln!("{}: {e}", "ERROR".red());
std::process::exit(1)
},
Ok(_) => {}
},
_ = term_signal.recv() => {},
}
Ok(())
}

View File

@@ -15,69 +15,72 @@ path = "src/main.rs"
[dependencies]
# local
komodo_client = { workspace = true, features = ["mongo"] }
komodo_client = { workspace = true, features = ["core"] }
periphery_client.workspace = true
environment_file.workspace = true
mogh_validations.workspace = true
interpolate.workspace = true
mogh_secret_file.workspace = true
formatting.workspace = true
mogh_rate_limit.workspace = true
transport.workspace = true
database.workspace = true
response.workspace = true
encoding.workspace = true
command.workspace = true
config.workspace = true
logger.workspace = true
cache.workspace = true
mogh_config.workspace = true
mogh_logger.workspace = true
mogh_cache.workspace = true
mogh_pki.workspace = true
git.workspace = true
# mogh
serror = { workspace = true, features = ["axum"] }
mogh_error = { workspace = true, features = ["axum"] }
mogh_auth_client = { workspace = true, features = ["utoipa"] }
mogh_auth_server.workspace = true
async_timing_util.workspace = true
partial_derive2.workspace = true
derive_variants.workspace = true
resolver_api.workspace = true
mogh_resolver.workspace = true
mogh_server.workspace = true
toml_pretty.workspace = true
slack.workspace = true
svi.workspace = true
# external
aws-credential-types.workspace = true
tokio-tungstenite.workspace = true
english-to-cron.workspace = true
openidconnect.workspace = true
jsonwebtoken.workspace = true
axum-server.workspace = true
urlencoding.workspace = true
data-encoding.workspace = true
serde_yaml_ng.workspace = true
utoipa-scalar.workspace = true
futures-util.workspace = true
aws-sdk-ec2.workspace = true
urlencoding.workspace = true
aws-config.workspace = true
tokio-util.workspace = true
axum-extra.workspace = true
tower-http.workspace = true
serde_json.workspace = true
serde_yaml_ng.workspace = true
typeshare.workspace = true
chrono-tz.workspace = true
indexmap.workspace = true
octorust.workspace = true
wildcard.workspace = true
arc-swap.workspace = true
serde_qs.workspace = true
colored.workspace = true
dashmap.workspace = true
tracing.workspace = true
reqwest.workspace = true
futures.workspace = true
nom_pem.workspace = true
dotenvy.workspace = true
anyhow.workspace = true
bcrypt.workspace = true
croner.workspace = true
chrono.workspace = true
bcrypt.workspace = true
base64.workspace = true
rustls.workspace = true
utoipa.workspace = true
bytes.workspace = true
tokio.workspace = true
serde.workspace = true
strum.workspace = true
regex.workspace = true
axum.workspace = true
toml.workspace = true
uuid.workspace = true
envy.workspace = true
rand.workspace = true
hmac.workspace = true
sha2.workspace = true
hex.workspace = true
url.workspace = true

View File

@@ -1,7 +1,7 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
# Build Core
FROM rust:1.89.0-bullseye AS core-builder
FROM rust:1.93.0-trixie AS core-builder
RUN cargo install cargo-strip
WORKDIR /builder
@@ -18,7 +18,7 @@ RUN cargo build -p komodo_core --release && \
cargo strip
# Build Frontend
FROM node:20.12-alpine AS frontend-builder
FROM node:22.12-alpine AS frontend-builder
WORKDIR /builder
COPY ./frontend ./frontend
COPY ./client/core/ts ./client
@@ -26,7 +26,7 @@ RUN cd client && yarn && yarn build && yarn link
RUN cd frontend && yarn link komodo_client && yarn && yarn build
# Final Image
FROM debian:bullseye-slim
FROM debian:trixie-slim
COPY ./bin/core/starship.toml /starship.toml
COPY ./bin/core/debian-deps.sh .
@@ -48,6 +48,9 @@ RUN mkdir /action-cache && \
cd /action-cache && \
deno install jsr:@std/yaml jsr:@std/toml
COPY ./bin/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# Hint at the port
EXPOSE 9120
@@ -55,9 +58,11 @@ ENV KOMODO_CLI_CONFIG_PATHS="/config"
# This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*`
ENV KOMODO_CLI_CONFIG_KEYWORDS="*config.*,*komodo.cli*.*"
CMD [ "core" ]
CMD [ "/bin/bash", "-c", "update-ca-certificates && core" ]
# Label to prevent Komodo from stopping with StopAllContainers
LABEL komodo.skip="true"
# Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
LABEL org.opencontainers.image.description="Komodo Core"
LABEL org.opencontainers.image.licenses=GPL-3.0
LABEL org.opencontainers.image.licenses="GPL-3.0"

View File

@@ -13,7 +13,7 @@ FROM ${AARCH64_BINARIES} AS aarch64
FROM ${FRONTEND_IMAGE} AS frontend
# Final Image
FROM debian:bullseye-slim
FROM debian:trixie-slim
COPY ./bin/core/starship.toml /starship.toml
COPY ./bin/core/debian-deps.sh .
@@ -28,7 +28,7 @@ COPY --from=x86_64 /core /app/core/linux/amd64
COPY --from=aarch64 /core /app/core/linux/arm64
RUN mv /app/core/${TARGETPLATFORM} /usr/local/bin/core && rm -r /app/core
# Same for util
# Same for km
COPY --from=x86_64 /km /app/km/linux/amd64
COPY --from=aarch64 /km /app/km/linux/arm64
RUN mv /app/km/${TARGETPLATFORM} /usr/local/bin/km && rm -r /app/km
@@ -44,6 +44,9 @@ RUN mkdir /action-cache && \
cd /action-cache && \
deno install jsr:@std/yaml jsr:@std/toml
COPY ./bin/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# Hint at the port
EXPOSE 9120
@@ -51,9 +54,12 @@ ENV KOMODO_CLI_CONFIG_PATHS="/config"
# This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*`
ENV KOMODO_CLI_CONFIG_KEYWORDS="*config.*,*komodo.cli*.*"
ENTRYPOINT [ "entrypoint.sh" ]
CMD [ "core" ]
# Label to prevent Komodo from stopping with StopAllContainers
LABEL komodo.skip="true"
# Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
LABEL org.opencontainers.image.description="Komodo Core"
LABEL org.opencontainers.image.licenses=GPL-3.0
LABEL org.opencontainers.image.licenses="GPL-3.0"

View File

@@ -7,14 +7,14 @@ ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
FROM ${BINARIES_IMAGE} AS binaries
# Build Frontend
FROM node:20.12-alpine AS frontend-builder
FROM node:22.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 komodo_client && yarn && yarn build
FROM debian:bullseye-slim
FROM debian:trixie-slim
COPY ./bin/core/starship.toml /starship.toml
COPY ./bin/core/debian-deps.sh .
@@ -33,6 +33,9 @@ RUN mkdir /action-cache && \
cd /action-cache && \
deno install jsr:@std/yaml jsr:@std/toml
COPY ./bin/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# Hint at the port
EXPOSE 9120
@@ -40,9 +43,12 @@ ENV KOMODO_CLI_CONFIG_PATHS="/config"
# This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*`
ENV KOMODO_CLI_CONFIG_KEYWORDS="*config.*,*komodo.cli*.*"
ENTRYPOINT [ "entrypoint.sh" ]
CMD [ "core" ]
# Label to prevent Komodo from stopping with StopAllContainers
LABEL komodo.skip="true"
# Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.source="https://github.com/moghtech/komodo"
LABEL org.opencontainers.image.description="Komodo Core"
LABEL org.opencontainers.image.licenses=GPL-3.0
LABEL org.opencontainers.image.licenses="GPL-3.0"

View File

@@ -4,7 +4,6 @@ use serde::Serialize;
use super::*;
#[instrument(level = "debug")]
pub async fn send_alert(
url: &str,
alert: &Alert,
@@ -17,6 +16,24 @@ pub async fn send_alert(
"{level} | If you see this message, then Alerter **{name}** is **working**\n{link}"
)
}
AlertData::SwarmUnhealthy { id, name, err } => {
let link = resource_link(ResourceTargetVariant::Swarm, id);
match alert.level {
SeverityLevel::Ok => {
format!("{level} | Swarm **{name}** is now **healthy**\n{link}")
}
SeverityLevel::Critical => {
let err = err
.as_ref()
.map(|e| format!("\n**error**: {e}"))
.unwrap_or_default();
format!(
"{level} | Swarm **{name}** is **unhealthy** ❌\n{link}{err}"
)
}
_ => unreachable!(),
}
}
AlertData::ServerVersionMismatch {
id,
name,
@@ -50,7 +67,7 @@ pub async fn send_alert(
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | **{name}**{region} is now **reachable**\n{link}"
"{level} | **{name}**{region} is now **connected**\n{link}"
)
}
SeverityLevel::Critical => {
@@ -109,6 +126,8 @@ pub async fn send_alert(
AlertData::ContainerStateChange {
id,
name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
from,
@@ -116,37 +135,64 @@ pub async fn send_alert(
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let to = fmt_docker_container_state(to);
let target = if let Some(swarm) = swarm_name {
format!("\nswarm: **{swarm}**")
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
format!("")
};
format!(
"📦 Deployment **{name}** is now **{to}**\nserver: **{server_name}**\nprevious: **{from}**\n{link}"
"📦 Deployment **{name}** is now **{to}**{target}\nprevious: **{from}**\n{link}"
)
}
AlertData::DeploymentImageUpdateAvailable {
id,
name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let target = if let Some(swarm) = swarm_name {
format!("\nswarm: **{swarm}**")
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
format!("")
};
format!(
"⬆ Deployment **{name}** has an update available\nserver: **{server_name}**\nimage: **{image}**\n{link}"
"⬆ Deployment **{name}** has an update available{target}\nimage: **{image}**\n{link}"
)
}
AlertData::DeploymentAutoUpdated {
id,
name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let target = if let Some(swarm) = swarm_name {
format!("\nswarm: **{swarm}**")
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
format!("")
};
format!(
"⬆ Deployment **{name}** was updated automatically ⏫\nserver: **{server_name}**\nimage: **{image}**\n{link}"
"⬆ Deployment **{name}** was updated automatically ⏫{target}\nimage: **{image}**\n{link}"
)
}
AlertData::StackStateChange {
id,
name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
from,
@@ -154,26 +200,44 @@ pub async fn send_alert(
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let to = fmt_stack_state(to);
let target = if let Some(swarm) = swarm_name {
format!("\nswarm: **{swarm}**")
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
format!("")
};
format!(
"🥞 Stack **{name}** is now {to}\nserver: **{server_name}**\nprevious: **{from}**\n{link}"
"🥞 Stack **{name}** is now {to}{target}\nprevious: **{from}**\n{link}"
)
}
AlertData::StackImageUpdateAvailable {
id,
name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
service,
image,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let target = if let Some(swarm) = swarm_name {
format!("\nswarm: **{swarm}**")
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
format!("")
};
format!(
"⬆ Stack **{name}** has an update available\nserver: **{server_name}**\nservice: **{service}**\nimage: **{image}**\n{link}"
"⬆ Stack **{name}** has an update available{target}\nservice: **{service}**\nimage: **{image}**\n{link}"
)
}
AlertData::StackAutoUpdated {
id,
name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
images,
@@ -182,8 +246,15 @@ pub async fn send_alert(
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images = images.join(", ");
let target = if let Some(swarm) = swarm_name {
format!("\nswarm: **{swarm}**")
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
format!("")
};
format!(
"⬆ Stack **{name}** was updated automatically ⏫\nserver: **{server_name}**\n{images_label}: **{images}**\n{link}"
"⬆ Stack **{name}** was updated automatically ⏫{target}\n{images_label}: **{images}**\n{link}"
)
}
AlertData::AwsBuilderTerminationFailed {
@@ -233,39 +304,41 @@ pub async fn send_alert(
format!(
"{level} | {message}{}",
if details.is_empty() {
format_args!("")
String::new()
} else {
format_args!("\n{details}")
format!("\n{details}")
}
)
}
AlertData::None {} => Default::default(),
};
if !content.is_empty() {
let VariablesAndSecrets { variables, secrets } =
get_variables_and_secrets().await?;
let mut url_interpolated = url.to_string();
let mut interpolator =
Interpolator::new(Some(&variables), &secrets);
interpolator.interpolate_string(&mut url_interpolated)?;
send_message(&url_interpolated, &content)
.await
.map_err(|e| {
let replacers = interpolator
.secret_replacers
.into_iter()
.collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with slack request: {sanitized_error}"
))
})?;
if content.is_empty() {
return Ok(());
}
Ok(())
let VariablesAndSecrets { variables, secrets } =
get_variables_and_secrets().await?;
let mut url_interpolated = url.to_string();
let mut interpolator =
Interpolator::new(Some(&variables), &secrets);
interpolator.interpolate_string(&mut url_interpolated)?;
send_message(&url_interpolated, &content)
.await
.map_err(|e| {
let replacers = interpolator
.secret_replacers
.into_iter()
.collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with request to Discord: {sanitized_error}"
))
})
}
async fn send_message(

View File

@@ -1,8 +1,6 @@
use ::slack::types::Block;
use anyhow::{Context, anyhow};
use database::mungos::{find::find_collect, mongodb::bson::doc};
use derive_variants::ExtractVariant;
use futures::future::join_all;
use futures_util::future::join_all;
use interpolate::Interpolator;
use komodo_client::entities::{
ResourceTargetVariant,
@@ -12,7 +10,6 @@ use komodo_client::entities::{
komodo_timestamp,
stack::StackState,
};
use tracing::Instrument;
use crate::helpers::query::get_variables_and_secrets;
use crate::helpers::{
@@ -25,40 +22,32 @@ mod ntfy;
mod pushover;
mod slack;
#[instrument(level = "debug")]
pub async fn send_alerts(alerts: &[Alert]) {
if alerts.is_empty() {
return;
}
let span =
info_span!("send_alerts", alerts = format!("{alerts:?}"));
async {
let Ok(alerters) = find_collect(
&db_client().alerters,
doc! { "config.enabled": true },
None,
)
.await
.inspect_err(|e| {
error!(
let Ok(alerters) = find_collect(
&db_client().alerters,
doc! { "config.enabled": true },
None,
)
.await
.inspect_err(|e| {
error!(
"ERROR sending alerts | failed to get alerters from db | {e:#}"
)
}) else {
return;
};
}) else {
return;
};
let handles = alerts
.iter()
.map(|alert| send_alert_to_alerters(&alerters, alert));
let handles = alerts
.iter()
.map(|alert| send_alert_to_alerters(&alerters, alert));
join_all(handles).await;
}
.instrument(span)
.await
join_all(handles).await;
}
#[instrument(level = "debug")]
async fn send_alert_to_alerters(alerters: &[Alerter], alert: &Alert) {
if alerters.is_empty() {
return;
@@ -91,14 +80,14 @@ pub async fn send_alert_to_alerter(
return Ok(());
}
let alert_type = alert.data.extract_variant();
let alert_variant: AlertDataVariant = (&alert.data).into();
// In the test case, we don't want the filters inside this
// block to stop the test from being sent to the alerting endpoint.
if alert_type != AlertDataVariant::Test {
if alert_variant != AlertDataVariant::Test {
// Don't send if alert type not configured on the alerter
if !alerter.config.alert_types.is_empty()
&& !alerter.config.alert_types.contains(&alert_type)
&& !alerter.config.alert_types.contains(&alert_variant)
{
return Ok(());
}
@@ -162,7 +151,6 @@ pub async fn send_alert_to_alerter(
}
}
#[instrument(level = "debug")]
async fn send_custom_alert(
url: &str,
alert: &Alert,
@@ -263,6 +251,22 @@ fn standard_alert_content(alert: &Alert) -> String {
"{level} | If you see this message, then Alerter {name} is working\n{link}",
)
}
AlertData::SwarmUnhealthy { id, name, err } => {
let link = resource_link(ResourceTargetVariant::Swarm, id);
match alert.level {
SeverityLevel::Ok => {
format!("{level} | Swarm {name} is now healthy\n{link}")
}
SeverityLevel::Critical => {
let err = err
.as_ref()
.map(|e| format!("\nerror: {e}"))
.unwrap_or_default();
format!("{level} | Swarm {name} is unhealthy ❌\n{link}{err}")
}
_ => unreachable!(),
}
}
AlertData::ServerVersionMismatch {
id,
name,
@@ -295,7 +299,7 @@ fn standard_alert_content(alert: &Alert) -> String {
let link = resource_link(ResourceTargetVariant::Server, id);
match alert.level {
SeverityLevel::Ok => {
format!("{level} | {name}{region} is now reachable\n{link}")
format!("{level} | {name}{region} is now connected\n{link}")
}
SeverityLevel::Critical => {
let err = err
@@ -353,6 +357,8 @@ fn standard_alert_content(alert: &Alert) -> String {
AlertData::ContainerStateChange {
id,
name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
from,
@@ -360,37 +366,64 @@ fn standard_alert_content(alert: &Alert) -> String {
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let to_state = fmt_docker_container_state(to);
let target = if let Some(swarm) = swarm_name {
format!("\nswarm: {swarm}")
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
format!("")
};
format!(
"📦Deployment {name} is now {to_state}\nserver: {server_name}\nprevious: {from}\n{link}",
"📦Deployment {name} is now {to_state}{target}\nprevious: {from}\n{link}",
)
}
AlertData::DeploymentImageUpdateAvailable {
id,
name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let target = if let Some(swarm) = swarm_name {
format!("\nswarm: {swarm}")
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
format!("")
};
format!(
"⬆ Deployment {name} has an update available\nserver: {server_name}\nimage: {image}\n{link}",
"⬆ Deployment {name} has an update available{target}\nimage: {image}\n{link}",
)
}
AlertData::DeploymentAutoUpdated {
id,
name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let target = if let Some(swarm) = swarm_name {
format!("\nswarm: {swarm}")
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
format!("")
};
format!(
"⬆ Deployment {name} was updated automatically\nserver: {server_name}\nimage: {image}\n{link}",
"⬆ Deployment {name} was updated automatically{target}\nimage: {image}\n{link}",
)
}
AlertData::StackStateChange {
id,
name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
from,
@@ -398,26 +431,44 @@ fn standard_alert_content(alert: &Alert) -> String {
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let to_state = fmt_stack_state(to);
let target = if let Some(swarm) = swarm_name {
format!("\nswarm: {swarm}")
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
format!("")
};
format!(
"🥞 Stack {name} is now {to_state}\nserver: {server_name}\nprevious: {from}\n{link}",
"🥞 Stack {name} is now {to_state}{target}\nprevious: {from}\n{link}",
)
}
AlertData::StackImageUpdateAvailable {
id,
name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
service,
image,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let target = if let Some(swarm) = swarm_name {
format!("\nswarm: {swarm}")
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
format!("")
};
format!(
"⬆ Stack {name} has an update available\nserver: {server_name}\nservice: {service}\nimage: {image}\n{link}",
"⬆ Stack {name} has an update available{target}\nservice: {service}\nimage: {image}\n{link}",
)
}
AlertData::StackAutoUpdated {
id,
name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
images,
@@ -426,8 +477,15 @@ fn standard_alert_content(alert: &Alert) -> String {
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images_str = images.join(", ");
let target = if let Some(swarm) = swarm_name {
format!("\nswarm: {swarm}")
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
format!("")
};
format!(
"⬆ Stack {name} was updated automatically ⏫\nserver: {server_name}\n{images_label}: {images_str}\n{link}",
"⬆ Stack {name} was updated automatically ⏫{target}\n{images_label}: {images_str}\n{link}",
)
}
AlertData::AwsBuilderTerminationFailed {
@@ -477,9 +535,9 @@ fn standard_alert_content(alert: &Alert) -> String {
format!(
"{level} | {message}{}",
if details.is_empty() {
format_args!("")
String::new()
} else {
format_args!("\n{details}")
format!("\n{details}")
}
)
}

View File

@@ -2,17 +2,38 @@ use std::sync::OnceLock;
use super::*;
#[instrument(level = "debug")]
pub async fn send_alert(
url: &str,
email: Option<&str>,
alert: &Alert,
) -> anyhow::Result<()> {
let content = standard_alert_content(alert);
if !content.is_empty() {
send_message(url, email, content).await?;
if content.is_empty() {
return Ok(());
}
Ok(())
let VariablesAndSecrets { variables, secrets } =
get_variables_and_secrets().await?;
let mut url_interpolated = url.to_string();
let mut interpolator =
Interpolator::new(Some(&variables), &secrets);
interpolator.interpolate_string(&mut url_interpolated)?;
send_message(&url_interpolated, email, content)
.await
.map_err(|e| {
let replacers = interpolator
.secret_replacers
.into_iter()
.collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with request to Ntfy: {sanitized_error}"
))
})
}
async fn send_message(
@@ -22,7 +43,7 @@ async fn send_message(
) -> anyhow::Result<()> {
let mut request = http_client()
.post(url)
.header("Title", "ntfy Alert")
.header("Title", "Komodo Alert")
.body(content);
if let Some(email) = email {
@@ -43,9 +64,7 @@ async fn send_message(
)
})?;
Err(anyhow!(
"Failed to send message to ntfy | {} | {}",
status,
text
"Failed to send message to ntfy | {status} | {text}",
))
}
}

View File

@@ -2,16 +2,35 @@ use std::sync::OnceLock;
use super::*;
#[instrument(level = "debug")]
pub async fn send_alert(
url: &str,
alert: &Alert,
) -> anyhow::Result<()> {
let content = standard_alert_content(alert);
if !content.is_empty() {
send_message(url, content).await?;
if content.is_empty() {
return Ok(());
}
Ok(())
let VariablesAndSecrets { variables, secrets } =
get_variables_and_secrets().await?;
let mut url_interpolated = url.to_string();
let mut interpolator =
Interpolator::new(Some(&variables), &secrets);
interpolator.interpolate_string(&mut url_interpolated)?;
send_message(&url_interpolated, content).await.map_err(|e| {
let replacers = interpolator
.secret_replacers
.into_iter()
.collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with request to Pushover: {sanitized_error}"
))
})
}
async fn send_message(

View File

@@ -1,6 +1,7 @@
use ::slack::types::OwnedBlock as Block;
use super::*;
#[instrument(level = "debug")]
pub async fn send_alert(
url: &str,
alert: &Alert,
@@ -23,6 +24,41 @@ pub async fn send_alert(
];
(text, blocks.into())
}
AlertData::SwarmUnhealthy { id, name, err } => {
match alert.level {
SeverityLevel::Ok => {
let text =
format!("{level} | Swarm *{name}* is now *healthy*");
let blocks = vec![
Block::header(level),
Block::section(format!(
"Swarm *{name}* is now *healthy*"
)),
];
(text, blocks.into())
}
SeverityLevel::Critical => {
let text =
format!("{level} | Swarm *{name}* is *unhealthy* ❌");
let err = err
.as_ref()
.map(|e| format!("\nerror: {e}"))
.unwrap_or_default();
let blocks = vec![
Block::header(level),
Block::section(format!(
"Swarm *{name}* is *unhealthy* ❌{err}"
)),
Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
];
(text, blocks.into())
}
_ => unreachable!(),
}
}
AlertData::ServerVersionMismatch {
id,
name,
@@ -62,11 +98,11 @@ pub async fn send_alert(
match alert.level {
SeverityLevel::Ok => {
let text =
format!("{level} | *{name}*{region} is now *reachable*");
format!("{level} | *{name}*{region} is now *connected*");
let blocks = vec![
Block::header(level),
Block::section(format!(
"*{name}*{region} is now *reachable*"
"*{name}*{region} is now *connected*"
)),
];
(text, blocks.into())
@@ -238,19 +274,26 @@ pub async fn send_alert(
}
AlertData::ContainerStateChange {
name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
from,
to,
id,
..
} => {
let to = fmt_docker_container_state(to);
let text = format!("📦 Container *{name}* is now *{to}*");
let target = if let Some(swarm) = swarm_name {
format!("swarm: *{swarm}*\n")
} else if let Some(server) = server_name {
format!("server: *{server}*\n")
} else {
format!("")
};
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: {server_name}\nprevious: {from}",
)),
Block::section(format!("{target}previous: {from}",)),
Block::section(resource_link(
ResourceTargetVariant::Deployment,
id,
@@ -261,17 +304,24 @@ pub async fn send_alert(
AlertData::DeploymentImageUpdateAvailable {
id,
name,
server_name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
image,
} => {
let text =
format!("⬆ Deployment *{name}* has an update available");
let target = if let Some(swarm) = swarm_name {
format!("swarm: *{swarm}*\n")
} else if let Some(server) = server_name {
format!("server: *{server}*\n")
} else {
format!("")
};
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\nimage: *{image}*",
)),
Block::section(format!("{target}image: *{image}*",)),
Block::section(resource_link(
ResourceTargetVariant::Deployment,
id,
@@ -282,17 +332,24 @@ pub async fn send_alert(
AlertData::DeploymentAutoUpdated {
id,
name,
server_name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
image,
} => {
let text =
format!("⬆ Deployment *{name}* was updated automatically ⏫");
let target = if let Some(swarm) = swarm_name {
format!("swarm: *{swarm}*\n")
} else if let Some(server) = server_name {
format!("server: *{server}*\n")
} else {
format!("")
};
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\nimage: *{image}*",
)),
Block::section(format!("{target}image: *{image}*",)),
Block::section(resource_link(
ResourceTargetVariant::Deployment,
id,
@@ -302,19 +359,26 @@ pub async fn send_alert(
}
AlertData::StackStateChange {
name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
from,
to,
id,
..
} => {
let to = fmt_stack_state(to);
let text = format!("🥞 Stack *{name}* is now *{to}*");
let target = if let Some(swarm) = swarm_name {
format!("swarm: *{swarm}*\n")
} else if let Some(server) = server_name {
format!("server: *{server}*\n")
} else {
format!("")
};
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\nprevious: *{from}*",
)),
Block::section(format!("{target}previous: *{from}*",)),
Block::section(resource_link(
ResourceTargetVariant::Stack,
id,
@@ -325,16 +389,25 @@ pub async fn send_alert(
AlertData::StackImageUpdateAvailable {
id,
name,
server_name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
service,
image,
} => {
let text = format!("⬆ Stack *{name}* has an update available");
let target = if let Some(swarm) = swarm_name {
format!("swarm: *{swarm}*\n")
} else if let Some(server) = server_name {
format!("server: *{server}*\n")
} else {
format!("")
};
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\nservice: *{service}*\nimage: *{image}*",
"{target}service: *{service}*\nimage: *{image}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Stack,
@@ -346,8 +419,10 @@ pub async fn send_alert(
AlertData::StackAutoUpdated {
id,
name,
server_name,
swarm_id: _swarm_id,
swarm_name,
server_id: _server_id,
server_name,
images,
} => {
let text =
@@ -355,11 +430,18 @@ pub async fn send_alert(
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images = images.join(", ");
let target = if let Some(swarm) = swarm_name {
format!("swarm: *{swarm}*\n")
} else if let Some(server) = server_name {
format!("server: *{server}*\n")
} else {
format!("")
};
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\n{images_label}: *{images}*",
)),
Block::section(
format!("{target}{images_label}: *{images}*",),
),
Block::section(resource_link(
ResourceTargetVariant::Stack,
id,
@@ -466,18 +548,23 @@ pub async fn send_alert(
}
AlertData::None {} => Default::default(),
};
if !text.is_empty() {
let VariablesAndSecrets { variables, secrets } =
get_variables_and_secrets().await?;
let mut url_interpolated = url.to_string();
if text.is_empty() {
return Ok(());
}
let VariablesAndSecrets { variables, secrets } =
get_variables_and_secrets().await?;
let mut url_interpolated = url.to_string();
let mut interpolator =
Interpolator::new(Some(&variables), &secrets);
let mut interpolator =
Interpolator::new(Some(&variables), &secrets);
interpolator.interpolate_string(&mut url_interpolated)?;
interpolator.interpolate_string(&mut url_interpolated)?;
let slack = ::slack::Client::new(url_interpolated);
slack.send_message(text, blocks).await.map_err(|e| {
let slack = ::slack::Client::new(url_interpolated);
slack
.send_owned_message_single(&text, None, blocks.as_deref())
.await
.map_err(|e| {
let replacers = interpolator
.secret_replacers
.into_iter()
@@ -485,9 +572,8 @@ pub async fn send_alert(
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with slack request: {sanitized_error}"
"Error with request to Slack: {sanitized_error}"
))
})?;
}
Ok(())
}

View File

@@ -1,163 +0,0 @@
use std::{sync::OnceLock, time::Instant};
use axum::{Router, extract::Path, http::HeaderMap, routing::post};
use derive_variants::{EnumVariants, ExtractVariant};
use komodo_client::{api::auth::*, entities::user::User};
use reqwest::StatusCode;
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serror::{AddStatusCode, 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},
oidc::{self, client::oidc_client},
},
config::core_config,
helpers::query::get_user,
state::jwt_client,
};
use super::Variant;
#[derive(Default)]
pub struct AuthArgs {
pub headers: HeaderMap,
}
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
)]
#[args(AuthArgs)]
#[response(Response)]
#[error(serror::Error)]
#[variant_derive(Debug)]
#[serde(tag = "type", content = "params")]
#[allow(clippy::enum_variant_names, clippy::large_enum_variant)]
pub enum AuthRequest {
GetLoginOptions(GetLoginOptions),
SignUpLocalUser(SignUpLocalUser),
LoginLocalUser(LoginLocalUser),
ExchangeForJwt(ExchangeForJwt),
GetUser(GetUser),
}
pub fn router() -> Router {
let mut router = Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler));
if core_config().local_auth {
info!("🔑 Local Login Enabled");
}
if github_oauth_client().is_some() {
info!("🔑 Github Login Enabled");
router = router.nest("/github", github::router())
}
if google_oauth_client().is_some() {
info!("🔑 Google Login Enabled");
router = router.nest("/google", google::router())
}
if core_config().oidc_enabled {
info!("🔑 OIDC Login Enabled");
router = router.nest("/oidc", oidc::router())
}
router
}
async fn variant_handler(
headers: HeaderMap,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<axum::response::Response> {
let req: AuthRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
}))?;
handler(headers, Json(req)).await
}
#[instrument(name = "AuthHandler", level = "debug", skip(headers))]
async fn handler(
headers: HeaderMap,
Json(request): Json<AuthRequest>,
) -> serror::Result<axum::response::Response> {
let timer = Instant::now();
let req_id = Uuid::new_v4();
debug!(
"/auth request {req_id} | METHOD: {:?}",
request.extract_variant()
);
let res = request.resolve(&AuthArgs { headers }).await;
if let Err(e) = &res {
debug!("/auth request {req_id} | error: {:#}", e.error);
}
let elapsed = timer.elapsed();
debug!("/auth request {req_id} | resolve time: {elapsed:?}");
res.map(|res| res.0)
}
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: github_oauth_client().is_some(),
google: google_oauth_client().is_some(),
oidc: oidc_client().load().is_some(),
registration_disabled: config.disable_user_registration,
}
})
}
impl Resolve<AuthArgs> for GetLoginOptions {
#[instrument(name = "GetLoginOptions", level = "debug", skip(self))]
async fn resolve(
self,
_: &AuthArgs,
) -> serror::Result<GetLoginOptionsResponse> {
Ok(*login_options_reponse())
}
}
impl Resolve<AuthArgs> for ExchangeForJwt {
#[instrument(name = "ExchangeForJwt", level = "debug", skip(self))]
async fn resolve(
self,
_: &AuthArgs,
) -> serror::Result<ExchangeForJwtResponse> {
jwt_client()
.redeem_exchange_token(&self.token)
.await
.map_err(Into::into)
}
}
impl Resolve<AuthArgs> for GetUser {
#[instrument(name = "GetUser", level = "debug", skip(self))]
async fn resolve(
self,
AuthArgs { headers }: &AuthArgs,
) -> serror::Result<User> {
let user_id = get_user_id_from_headers(headers)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
get_user(&user_id)
.await
.status_code(StatusCode::UNAUTHORIZED)
}
}

View File

@@ -1,22 +1,17 @@
use std::{
collections::HashSet,
path::{Path, PathBuf},
str::FromStr,
sync::OnceLock,
};
use anyhow::Context;
use command::run_komodo_command;
use config::merge_objects;
use command::run_komodo_standard_command;
use database::mungos::{
by_id::update_one_by_id, mongodb::bson::to_document,
};
use interpolate::Interpolator;
use komodo_client::{
api::{
execute::{BatchExecutionResponse, BatchRunAction, RunAction},
user::{CreateApiKey, CreateApiKeyResponse, DeleteApiKey},
},
api::execute::{BatchExecutionResponse, BatchRunAction, RunAction},
entities::{
FileFormat, JsonObject,
action::Action,
@@ -24,21 +19,29 @@ use komodo_client::{
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
random_string,
update::Update,
user::action_user,
},
parsers::parse_key_value_list,
};
use resolver_api::Resolve;
use mogh_auth_client::api::manage::{
CreateApiKey, CreateApiKeyResponse,
};
use mogh_auth_server::api::manage::api_key::{
create_api_key, delete_api_key,
};
use mogh_config::merge_objects;
use mogh_resolver::Resolve;
use tokio::fs;
use crate::{
alert::send_alerts,
api::{execute::ExecuteRequest, user::UserArgs},
api::execute::ExecuteRequest,
auth::KomodoAuthImpl,
config::core_config,
helpers::{
query::{VariablesAndSecrets, get_variables_and_secrets},
random_string,
update::update_update,
},
permission::get_check_permissions,
@@ -59,11 +62,19 @@ impl super::BatchExecute for BatchRunAction {
}
impl Resolve<ExecuteArgs> for BatchRunAction {
#[instrument(name = "BatchRunAction", skip(self, user), fields(user_id = user.id))]
#[instrument(
"BatchRunAction",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
)
)]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunAction>(&self.pattern, user)
.await?,
@@ -72,11 +83,20 @@ impl Resolve<ExecuteArgs> for BatchRunAction {
}
impl Resolve<ExecuteArgs> for RunAction {
#[instrument(name = "RunAction", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"RunAction",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
action = self.action,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let mut action = get_check_permissions::<Action>(
&self.action,
user,
@@ -119,76 +139,87 @@ impl Resolve<ExecuteArgs> for RunAction {
let args = serde_json::to_string(&args)
.context("Failed to serialize action run arguments")?;
let CreateApiKeyResponse { key, secret } = CreateApiKey {
name: update.id.clone(),
expires: 0,
}
.resolve(&UserArgs {
user: action_user().to_owned(),
})
let CreateApiKeyResponse { key, secret } = create_api_key(
&KomodoAuthImpl,
action_user().id.clone(),
CreateApiKey {
name: update.id.clone(),
expires: 0,
},
)
.await?;
let contents = &mut action.config.file_contents;
// Do next steps in seperate error handling block,
// and delete the API key before unwrapping the error.
// If Komodo shuts down during these steps, there will
// be a dangling api key in the DB with user_id: "000000000000000000000002".
// These need to be
let res = async {
let contents = &mut action.config.file_contents;
// Wrap the file contents in the execution context.
*contents = full_contents(contents, &args, &key, &secret);
// Wrap the file contents in the execution context.
*contents = full_contents(contents, &args, &key, &secret);
let replacers =
interpolate(contents, &mut update, key.clone(), secret.clone())
.await?
.into_iter()
.collect::<Vec<_>>();
let replacers = interpolate(
contents,
&mut update,
key.clone(),
secret.clone(),
)
.await?
.into_iter()
.collect::<Vec<_>>();
let file = format!("{}.ts", random_string(10));
let path = core_config().action_directory.join(&file);
let file = format!("{}.ts", random_string(10));
let path = core_config().action_directory.join(&file);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
mogh_secret_file::write_async(&path, contents)
.await
.with_context(|| format!("Failed to initialize Action file parent directory {parent:?}"))?;
.with_context(|| {
format!("Failed to write action file to {path:?}")
})?;
let CoreConfig { ssl_enabled, .. } = core_config();
let https_cert_flag = if *ssl_enabled {
" --unsafely-ignore-certificate-errors=localhost"
} else {
""
};
let reload = if action.config.reload_deno_deps {
" --reload"
} else {
""
};
let mut res = run_komodo_standard_command(
// Keep this stage name as is, the UI will find the latest update log by matching the stage name
"Execute Action",
None,
format!(
"deno run --allow-all{https_cert_flag}{reload} {}",
path.display()
),
)
.await;
res.stdout = svi::replace_in_string(&res.stdout, &replacers)
.replace(&key, "<ACTION_API_KEY>");
res.stderr = svi::replace_in_string(&res.stderr, &replacers)
.replace(&secret, "<ACTION_API_SECRET>");
cleanup_run(file + ".js", &path).await;
update.logs.push(res);
update.finalize();
mogh_error::Ok(update)
}
fs::write(&path, contents).await.with_context(|| {
format!("Failed to write action file to {path:?}")
})?;
let CoreConfig { ssl_enabled, .. } = core_config();
let https_cert_flag = if *ssl_enabled {
" --unsafely-ignore-certificate-errors=localhost"
} else {
""
};
let reload = if action.config.reload_deno_deps {
" --reload"
} else {
""
};
let mut res = run_komodo_command(
// Keep this stage name as is, the UI will find the latest update log by matching the stage name
"Execute Action",
None,
format!(
"deno run --allow-all{https_cert_flag}{reload} {}",
path.display()
),
)
.await;
res.stdout = svi::replace_in_string(&res.stdout, &replacers)
.replace(&key, "<ACTION_API_KEY>");
res.stderr = svi::replace_in_string(&res.stderr, &replacers)
.replace(&secret, "<ACTION_API_SECRET>");
cleanup_run(file + ".js", &path).await;
if let Err(e) = (DeleteApiKey { key })
.resolve(&UserArgs {
user: action_user().to_owned(),
})
.await
if let Err(e) =
delete_api_key(&KomodoAuthImpl, &action_user().id, key).await
{
warn!(
"Failed to delete API key after action execution | {:#}",
@@ -196,8 +227,7 @@ impl Resolve<ExecuteArgs> for RunAction {
);
};
update.logs.push(res);
update.finalize();
let update = res?;
// Need to manually update the update before cache refresh,
// and before broadcast with update_update.
@@ -217,7 +247,6 @@ impl Resolve<ExecuteArgs> for RunAction {
update_update(update.clone()).await?;
if !update.success && action.config.failure_alert {
warn!("action unsuccessful, alerting...");
let target = update.target.clone();
tokio::spawn(async move {
let alert = Alert {
@@ -240,12 +269,13 @@ impl Resolve<ExecuteArgs> for RunAction {
}
}
#[instrument("Interpolate", skip(contents, update, secret))]
async fn interpolate(
contents: &mut String,
update: &mut Update,
key: String,
secret: String,
) -> serror::Result<HashSet<(String, String)>> {
) -> mogh_error::Result<HashSet<(String, String)>> {
let VariablesAndSecrets {
variables,
mut secrets,
@@ -325,6 +355,7 @@ main()
/// Cleans up file at given path.
/// ALSO if $DENO_DIR is set,
/// will clean up the generated file matching "file"
#[instrument("CleanupRun")]
async fn cleanup_run(file: String, path: &Path) {
if let Err(e) = fs::remove_file(path).await {
warn!(
@@ -344,7 +375,7 @@ fn deno_dir() -> Option<&'static Path> {
DENO_DIR
.get_or_init(|| {
let deno_dir = std::env::var("DENO_DIR").ok()?;
PathBuf::from_str(&deno_dir).ok()
Some(PathBuf::from(&deno_dir))
})
.as_deref()
}

View File

@@ -1,6 +1,8 @@
use anyhow::{Context, anyhow};
use formatting::format_serror;
use futures::{TryStreamExt, stream::FuturesUnordered};
use futures_util::{
StreamExt, TryStreamExt, stream::FuturesUnordered,
};
use komodo_client::{
api::execute::{SendAlert, TestAlerter},
entities::{
@@ -10,9 +12,9 @@ use komodo_client::{
permission::PermissionLevel,
},
};
use mogh_error::AddStatusCodeError;
use mogh_resolver::Resolve;
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCodeError;
use crate::{
alert::send_alert_to_alerter, helpers::update::update_update,
@@ -22,10 +24,19 @@ use crate::{
use super::ExecuteArgs;
impl Resolve<ExecuteArgs> for TestAlerter {
#[instrument(name = "TestAlerter", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"TestAlerter",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
alerter = self.alerter,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let alerter = get_check_permissions::<Alerter>(
&self.alerter,
@@ -79,15 +90,24 @@ impl Resolve<ExecuteArgs> for TestAlerter {
//
impl Resolve<ExecuteArgs> for SendAlert {
#[instrument(name = "SendAlert", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"SendAlert",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
request = format!("{self:?}"),
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let alerters = list_full_for_user::<Alerter>(
Default::default(),
user,
PermissionLevel::Execute.into(),
PermissionLevel::Read.into(),
&[],
)
.await?
@@ -102,6 +122,28 @@ impl Resolve<ExecuteArgs> for SendAlert {
})
.collect::<Vec<_>>();
let alerters = if user.admin {
alerters
} else {
// Only keep alerters with execute permissions
alerters
.into_iter()
.map(|alerter| async move {
get_check_permissions::<Alerter>(
&alerter.id,
user,
PermissionLevel::Execute.into(),
)
.await
})
.collect::<FuturesUnordered<_>>()
.collect::<Vec<_>>()
.await
.into_iter()
.flatten()
.collect()
};
if alerters.is_empty() {
return Err(anyhow!(
"Could not find any valid alerters to send to, this required Execute permissions on the Alerter"

View File

@@ -14,12 +14,15 @@ use database::mungos::{
},
};
use formatting::format_serror;
use futures::future::join_all;
use futures_util::future::join_all;
use interpolate::Interpolator;
use komodo_client::{
api::execute::{
BatchExecutionResponse, BatchRunBuild, CancelBuild, Deploy,
RunBuild,
api::{
execute::{
BatchExecutionResponse, BatchRunBuild, CancelBuild, Deploy,
RunBuild,
},
write::RefreshBuildCache,
},
entities::{
alert::{Alert, AlertData, SeverityLevel},
@@ -34,15 +37,17 @@ use komodo_client::{
user::auto_redeploy_user,
},
};
use mogh_resolver::Resolve;
use periphery_client::api;
use resolver_api::Resolve;
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
use crate::{
alert::send_alerts,
api::write::WriteArgs,
helpers::{
build_git_token,
builder::{cleanup_builder_instance, get_builder_periphery},
builder::{cleanup_builder_instance, connect_builder_periphery},
channel::build_cancel_channel,
query::{
VariablesAndSecrets, get_deployment_state,
@@ -66,11 +71,19 @@ impl super::BatchExecute for BatchRunBuild {
}
impl Resolve<ExecuteArgs> for BatchRunBuild {
#[instrument(name = "BatchRunBuild", skip(user), fields(user_id = user.id))]
#[instrument(
"BatchRunBuild",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
)
)]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunBuild>(&self.pattern, user)
.await?,
@@ -79,11 +92,20 @@ impl Resolve<ExecuteArgs> for BatchRunBuild {
}
impl Resolve<ExecuteArgs> for RunBuild {
#[instrument(name = "RunBuild", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"RunBuild",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
build = self.build,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let mut build = get_check_permissions::<Build>(
&self.build,
user,
@@ -168,7 +190,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
update.finalize();
let id = update.id.clone();
if let Err(e) = update_update(update).await {
warn!("failed to modify Update {id} on db | {e:#}");
warn!("Failed to modify Update {id} on db | {e:#}");
}
if !is_server_builder {
cancel_clone.cancel();
@@ -186,7 +208,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
});
// GET BUILDER PERIPHERY
let (periphery, cleanup_data) = match get_builder_periphery(
let (periphery, cleanup_data) = match connect_builder_periphery(
build.name.clone(),
Some(build.config.version),
builder,
@@ -197,12 +219,12 @@ impl Resolve<ExecuteArgs> for RunBuild {
Ok(builder) => builder,
Err(e) => {
warn!(
"failed to get builder for build {} | {e:#}",
"Failed to get Builder for Build {} | {e:#}",
build.name
);
update.logs.push(Log::error(
"get builder",
format_serror(&e.context("failed to get builder").into()),
"Get Builder",
format_serror(&e.context("Failed to get Builder").into()),
));
return handle_early_return(
update, build.id, build.name, false,
@@ -247,18 +269,18 @@ impl Resolve<ExecuteArgs> for RunBuild {
replacers: Default::default(),
}) => res,
_ = cancel.cancelled() => {
debug!("build cancelled during clone, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(cleanup_data, &mut update)
debug!("Build cancelled during clone, cleaning up builder");
update.push_error_log("Build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
info!("builder cleaned up");
info!("Builder cleaned up");
return handle_early_return(update, build.id, build.name, true).await
},
};
let commit_message = match res {
Ok(res) => {
debug!("finished repo clone");
debug!("Finished repo clone");
update.logs.extend(res.res.logs);
update.commit_hash =
res.res.commit_hash.unwrap_or_default().to_string();
@@ -294,11 +316,11 @@ impl Resolve<ExecuteArgs> for RunBuild {
commit_hash: optional_string(&update.commit_hash),
// Unused for now
additional_tags: Default::default(),
}) => res.context("failed at call to periphery to build"),
}) => 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(cleanup_data, &mut update)
info!("Build cancelled during build, cleaning up builder");
update.push_error_log("Build cancelled", String::from("User cancelled build during docker build"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
return handle_early_return(update, build.id, build.name, true).await
},
@@ -310,10 +332,10 @@ impl Resolve<ExecuteArgs> for RunBuild {
update.logs.extend(logs);
}
Err(e) => {
warn!("error in build | {e:#}");
warn!("Error in build | {e:#}");
update.push_error_log(
"build",
format_serror(&e.context("failed to build").into()),
"Build Error",
format_serror(&e.context("Failed to build").into()),
)
}
};
@@ -344,7 +366,8 @@ impl Resolve<ExecuteArgs> for RunBuild {
// If building on temporary cloud server (AWS),
// this will terminate the server.
cleanup_builder_instance(cleanup_data, &mut update).await;
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
@@ -363,13 +386,15 @@ impl Resolve<ExecuteArgs> for RunBuild {
update_update(update.clone()).await?;
let Build { id, name, .. } = build;
if update.success {
// don't hold response up for user
tokio::spawn(async move {
handle_post_build_redeploy(&build.id).await;
handle_post_build_redeploy(&id).await;
});
} else {
warn!("build unsuccessful, alerting...");
let name = name.clone();
let target = update.target.clone();
let version = update.version;
tokio::spawn(async move {
@@ -380,27 +405,33 @@ impl Resolve<ExecuteArgs> for RunBuild {
resolved_ts: Some(komodo_timestamp()),
resolved: true,
level: SeverityLevel::Warning,
data: AlertData::BuildFailed {
id: build.id,
name: build.name,
version,
},
data: AlertData::BuildFailed { id, name, version },
};
send_alerts(&[alert]).await
});
}
if let Err(e) = (RefreshBuildCache { build: name })
.resolve(&WriteArgs { user: user.clone() })
.await
{
update.push_error_log(
"Refresh build cache",
format_serror(&e.error.into()),
);
}
Ok(update.clone())
}
}
#[instrument(skip(update))]
#[instrument("HandleEarlyReturn", skip(update))]
async fn handle_early_return(
mut update: Update,
build_id: String,
build_name: String,
is_cancel: bool,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
update.finalize();
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
@@ -418,7 +449,6 @@ async fn handle_early_return(
}
update_update(update.clone()).await?;
if !update.success && !is_cancel {
warn!("build unsuccessful, alerting...");
let target = update.target.clone();
let version = update.version;
tokio::spawn(async move {
@@ -488,11 +518,20 @@ pub async fn validate_cancel_build(
}
impl Resolve<ExecuteArgs> for CancelBuild {
#[instrument(name = "CancelBuild", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"CancelBuild",
skip(user, update),
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
build = self.build,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let build = get_check_permissions::<Build>(
&self.build,
user,
@@ -539,7 +578,7 @@ impl Resolve<ExecuteArgs> for CancelBuild {
.await
{
warn!(
"failed to set CancelBuild Update status Complete after timeout | {e:#}"
"Failed to set CancelBuild Update status Complete after timeout | {e:#}"
)
}
});
@@ -548,7 +587,7 @@ impl Resolve<ExecuteArgs> for CancelBuild {
}
}
#[instrument]
#[instrument("PostBuildRedeploy")]
async fn handle_post_build_redeploy(build_id: &str) {
let Ok(redeploy_deployments) = find_collect(
&db_client().deployments,
@@ -584,7 +623,11 @@ async fn handle_post_build_redeploy(build_id: &str) {
stop_signal: None,
stop_time: None,
}
.resolve(&ExecuteArgs { user, update })
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
})
.await
}
.await;
@@ -610,13 +653,14 @@ async fn handle_post_build_redeploy(build_id: &str) {
/// This will make sure that a build with non-none image registry has an account attached,
/// and will check the core config for a token matching requirements.
/// Otherwise it is left to periphery.
#[instrument("ValidateRegistryTokens")]
async fn validate_account_extract_registry_tokens(
Build {
config: BuildConfig { image_registry, .. },
..
}: &Build,
// Maps (domain, account) -> token
) -> serror::Result<Vec<(String, String, String)>> {
) -> mogh_error::Result<Vec<(String, String, String)>> {
let mut res = HashMap::with_capacity(image_registry.capacity());
for (domain, account) in image_registry

View File

@@ -1,37 +1,39 @@
use std::sync::OnceLock;
use anyhow::{Context, anyhow};
use cache::TimeoutCache;
use formatting::format_serror;
use interpolate::Interpolator;
use komodo_client::{
api::execute::*,
entities::{
Version,
SwarmOrServer, Version,
build::{Build, ImageRegistryConfig},
deployment::{
Deployment, DeploymentImage, extract_registry_domain,
Deployment, DeploymentImage, DeploymentInfo,
extract_registry_domain,
},
komodo_timestamp, optional_string,
permission::PermissionLevel,
server::Server,
update::{Log, Update},
user::User,
},
};
use mogh_cache::TimeoutCache;
use mogh_error::AddStatusCodeError;
use mogh_resolver::Resolve;
use periphery_client::api;
use resolver_api::Resolve;
use reqwest::StatusCode;
use crate::{
helpers::{
periphery_client,
query::{VariablesAndSecrets, get_variables_and_secrets},
registry_token,
swarm::swarm_request,
update::update_update,
},
monitor::update_cache_for_server,
permission::get_check_permissions,
resource,
monitor::{update_cache_for_server, update_cache_for_swarm},
resource::{self, setup_deployment_execution},
state::action_states,
};
@@ -49,11 +51,19 @@ impl super::BatchExecute for BatchDeploy {
}
impl Resolve<ExecuteArgs> for BatchDeploy {
#[instrument(name = "BatchDeploy", skip(user), fields(user_id = user.id))]
#[instrument(
"BatchDeploy",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
)
)]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeploy>(&self.pattern, user)
.await?,
@@ -61,39 +71,32 @@ impl Resolve<ExecuteArgs> for BatchDeploy {
}
}
async fn setup_deployment_execution(
deployment: &str,
user: &User,
) -> anyhow::Result<(Deployment, Server)> {
let deployment = get_check_permissions::<Deployment>(
deployment,
user,
PermissionLevel::Execute.into(),
)
.await?;
if deployment.config.server_id.is_empty() {
return Err(anyhow!("Deployment has no Server configured"));
}
let server =
resource::get::<Server>(&deployment.config.server_id).await?;
if !server.config.enabled {
return Err(anyhow!("Attached Server is not enabled"));
}
Ok((deployment, server))
}
impl Resolve<ExecuteArgs> for Deploy {
#[instrument(name = "Deploy", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"Deploy",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
stop_signal = format!("{:?}", self.stop_signal),
stop_time = self.stop_time,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (mut deployment, server) =
setup_deployment_execution(&self.deployment, user).await?;
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (mut deployment, swarm_or_server) =
setup_deployment_execution(
&self.deployment,
user,
PermissionLevel::Execute.into(),
)
.await?;
swarm_or_server.verify_has_target()?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -203,26 +206,72 @@ impl Resolve<ExecuteArgs> for Deploy {
update.version = version;
update_update(update.clone()).await?;
match periphery_client(&server)?
.request(api::container::Deploy {
deployment,
stop_signal: self.stop_signal,
stop_time: self.stop_time,
registry_token,
replacers: secret_replacers.into_iter().collect(),
})
.await
{
Ok(log) => update.logs.push(log),
Err(e) => {
update.push_error_log(
"Deploy Container",
format_serror(&e.into()),
);
}
};
let deployment_id = deployment.id.clone();
update_cache_for_server(&server, true).await;
match swarm_or_server {
SwarmOrServer::None => unreachable!(),
SwarmOrServer::Swarm(swarm) => {
match swarm_request(
&swarm.config.server_ids,
api::swarm::CreateSwarmService {
deployment,
registry_token,
replacers: secret_replacers.into_iter().collect(),
},
)
.await
{
Ok(logs) => {
update_cache_for_swarm(&swarm, true).await;
update.logs.extend(logs)
}
Err(e) => {
update.push_error_log(
"Create Swarm Service",
format_serror(&e.into()),
);
}
};
}
SwarmOrServer::Server(server) => {
match periphery_client(&server)
.await?
.request(api::container::RunContainer {
deployment,
stop_signal: self.stop_signal,
stop_time: self.stop_time,
registry_token,
replacers: secret_replacers.into_iter().collect(),
})
.await
{
Ok(log) => {
update_cache_for_server(&server, true).await;
update.logs.push(log)
}
Err(e) => {
update.push_error_log(
"Deploy Container",
format_serror(&e.into()),
);
}
};
}
}
if let Err(e) = resource::update_info::<Deployment>(
&deployment_id,
&DeploymentInfo {
latest_image_digest: Default::default(),
},
)
.await
{
warn!(
"Failed to update deployment {} info after deploy | {e:#}",
deployment_id
);
}
update.finalize();
update_update(update.clone()).await?;
@@ -242,6 +291,14 @@ fn pull_cache() -> &'static PullCache {
PULL_CACHE.get_or_init(Default::default)
}
#[instrument(
"PullDeploymentInner",
skip_all,
fields(
deployment = deployment.id,
server = server.id
)
)]
pub async fn pull_deployment_inner(
deployment: Deployment,
server: &Server,
@@ -331,8 +388,9 @@ pub async fn pull_deployment_inner(
}
let res = async {
let log = match periphery_client(server)?
.request(api::image::PullImage {
let log = match periphery_client(server)
.await?
.request(api::docker::PullImage {
name: image,
account,
token,
@@ -356,13 +414,33 @@ pub async fn pull_deployment_inner(
}
impl Resolve<ExecuteArgs> for PullDeployment {
#[instrument(name = "PullDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"PullDeployment",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&self.deployment, user).await?;
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
user,
PermissionLevel::Execute.into(),
)
.await?;
let SwarmOrServer::Server(server) = swarm_or_server else {
return Err(
anyhow!("PullDeployment should not be called for Deployment in Swarm Mode")
.status_code(StatusCode::BAD_REQUEST),
);
};
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -390,13 +468,33 @@ impl Resolve<ExecuteArgs> for PullDeployment {
}
impl Resolve<ExecuteArgs> for StartDeployment {
#[instrument(name = "StartDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"StartDeployment",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&self.deployment, user).await?;
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
user,
PermissionLevel::Execute.into(),
)
.await?;
let SwarmOrServer::Server(server) = swarm_or_server else {
return Err(
anyhow!("StartDeployment should not be called for Deployment in Swarm Mode")
.status_code(StatusCode::BAD_REQUEST),
);
};
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -414,7 +512,8 @@ impl Resolve<ExecuteArgs> for StartDeployment {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = match periphery_client(&server)?
let log = match periphery_client(&server)
.await?
.request(api::container::StartContainer {
name: deployment.name,
})
@@ -437,13 +536,33 @@ impl Resolve<ExecuteArgs> for StartDeployment {
}
impl Resolve<ExecuteArgs> for RestartDeployment {
#[instrument(name = "RestartDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"RestartDeployment",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&self.deployment, user).await?;
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
user,
PermissionLevel::Execute.into(),
)
.await?;
let SwarmOrServer::Server(server) = swarm_or_server else {
return Err(
anyhow!("RestartDeployment should not be called for Deployment in Swarm Mode")
.status_code(StatusCode::BAD_REQUEST),
);
};
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -461,7 +580,8 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = match periphery_client(&server)?
let log = match periphery_client(&server)
.await?
.request(api::container::RestartContainer {
name: deployment.name,
})
@@ -486,13 +606,33 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
}
impl Resolve<ExecuteArgs> for PauseDeployment {
#[instrument(name = "PauseDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"PauseDeployment",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&self.deployment, user).await?;
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
user,
PermissionLevel::Execute.into(),
)
.await?;
let SwarmOrServer::Server(server) = swarm_or_server else {
return Err(
anyhow!("PauseDeployment should not be called for Deployment in Swarm Mode")
.status_code(StatusCode::BAD_REQUEST),
);
};
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -510,7 +650,8 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = match periphery_client(&server)?
let log = match periphery_client(&server)
.await?
.request(api::container::PauseContainer {
name: deployment.name,
})
@@ -533,13 +674,33 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
}
impl Resolve<ExecuteArgs> for UnpauseDeployment {
#[instrument(name = "UnpauseDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"UnpauseDeployment",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&self.deployment, user).await?;
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
user,
PermissionLevel::Execute.into(),
)
.await?;
let SwarmOrServer::Server(server) = swarm_or_server else {
return Err(
anyhow!("UnpauseDeployment should not be called for Deployment in Swarm Mode")
.status_code(StatusCode::BAD_REQUEST),
);
};
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -557,7 +718,8 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = match periphery_client(&server)?
let log = match periphery_client(&server)
.await?
.request(api::container::UnpauseContainer {
name: deployment.name,
})
@@ -582,13 +744,35 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
}
impl Resolve<ExecuteArgs> for StopDeployment {
#[instrument(name = "StopDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"StopDeployment",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
signal = format!("{:?}", self.signal),
time = self.time,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&self.deployment, user).await?;
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
user,
PermissionLevel::Execute.into(),
)
.await?;
let SwarmOrServer::Server(server) = swarm_or_server else {
return Err(
anyhow!("StopDeployment should not be called for Deployment in Swarm Mode")
.status_code(StatusCode::BAD_REQUEST),
);
};
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -606,7 +790,8 @@ impl Resolve<ExecuteArgs> for StopDeployment {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = match periphery_client(&server)?
let log = match periphery_client(&server)
.await?
.request(api::container::StopContainer {
name: deployment.name,
signal: self
@@ -648,11 +833,19 @@ impl super::BatchExecute for BatchDestroyDeployment {
}
impl Resolve<ExecuteArgs> for BatchDestroyDeployment {
#[instrument(name = "BatchDestroyDeployment", skip(user), fields(user_id = user.id))]
#[instrument(
"BatchDestroyDeployment",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
)
)]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDestroyDeployment>(
&self.pattern,
@@ -664,13 +857,30 @@ impl Resolve<ExecuteArgs> for BatchDestroyDeployment {
}
impl Resolve<ExecuteArgs> for DestroyDeployment {
#[instrument(name = "DestroyDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"DestroyDeployment",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
signal = format!("{:?}", self.signal),
time = self.time,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&self.deployment, user).await?;
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
user,
PermissionLevel::Execute.into(),
)
.await?;
swarm_or_server.verify_has_target()?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -688,30 +898,62 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = match periphery_client(&server)?
.request(api::container::RemoveContainer {
name: deployment.name,
signal: self
.signal
.unwrap_or(deployment.config.termination_signal)
.into(),
time: self
.time
.unwrap_or(deployment.config.termination_timeout)
.into(),
})
.await
{
Ok(log) => log,
Err(e) => Log::error(
"stop container",
format_serror(&e.context("failed to stop container").into()),
),
let log = match swarm_or_server {
SwarmOrServer::None => unreachable!(),
SwarmOrServer::Swarm(swarm) => {
match swarm_request(
&swarm.config.server_ids,
api::swarm::RemoveSwarmServices {
services: vec![deployment.name],
},
)
.await
{
Ok(log) => {
update_cache_for_swarm(&swarm, true).await;
log
}
Err(e) => Log::error(
"Remove Swarm Service",
format_serror(
&e.context("Failed to remove swarm service").into(),
),
),
}
}
SwarmOrServer::Server(server) => {
match periphery_client(&server)
.await?
.request(api::container::RemoveContainer {
name: deployment.name,
signal: self
.signal
.unwrap_or(deployment.config.termination_signal)
.into(),
time: self
.time
.unwrap_or(deployment.config.termination_timeout)
.into(),
})
.await
{
Ok(log) => {
update_cache_for_server(&server, true).await;
log
}
Err(e) => Log::error(
"Destroy Container",
format_serror(
&e.context("Failed to destroy container").into(),
),
),
}
}
};
update.logs.push(log);
update.finalize();
update_cache_for_server(&server, true).await;
update_update(update.clone()).await?;
Ok(update)

View File

@@ -1,29 +1,42 @@
use std::sync::OnceLock;
use std::{fmt::Write as _, sync::OnceLock};
use anyhow::{Context, anyhow};
use command::run_komodo_command;
use database::mungos::{find::find_collect, mongodb::bson::doc};
use command::run_komodo_standard_command;
use database::{
bson::{Document, doc},
mungos::find::find_collect,
};
use formatting::{bold, format_serror};
use futures_util::{StreamExt, stream::FuturesOrdered};
use komodo_client::{
api::execute::{
BackupCoreDatabase, ClearRepoCache, GlobalAutoUpdate,
RotateAllServerKeys, RotateCoreKeys,
},
entities::{
deployment::DeploymentState, server::ServerState,
SwarmOrServer, deployment::DeploymentState, server::ServerState,
stack::StackState,
},
};
use mogh_error::AddStatusCodeError;
use mogh_resolver::Resolve;
use periphery_client::api;
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCodeError;
use tokio::sync::Mutex;
use crate::{
api::execute::{
ExecuteArgs, pull_deployment_inner, pull_stack_inner,
api::{
execute::ExecuteArgs,
write::{
check_deployment_for_update_inner, check_stack_for_update_inner,
},
},
config::core_config,
helpers::update::update_update,
config::{core_config, core_keys},
helpers::{
periphery_client, query::find_swarm_or_server,
update::update_update,
},
resource::rotate_server_keys,
state::{
db_client, deployment_status_cache, server_status_cache,
stack_status_cache,
@@ -38,13 +51,17 @@ fn clear_repo_cache_lock() -> &'static Mutex<()> {
impl Resolve<ExecuteArgs> for ClearRepoCache {
#[instrument(
name = "ClearRepoCache",
skip(user, update),
fields(user_id = user.id, update_id = update.id)
"ClearRepoCache",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
if !user.admin {
return Err(
@@ -113,13 +130,17 @@ fn backup_database_lock() -> &'static Mutex<()> {
impl Resolve<ExecuteArgs> for BackupCoreDatabase {
#[instrument(
name = "BackupCoreDatabase",
skip(user, update),
fields(user_id = user.id, update_id = update.id)
"BackupCoreDatabase",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
if !user.admin {
return Err(
@@ -136,7 +157,7 @@ impl Resolve<ExecuteArgs> for BackupCoreDatabase {
update_update(update.clone()).await?;
let res = run_komodo_command(
let res = run_komodo_standard_command(
"Backup Core Database",
None,
"km database backup --yes",
@@ -162,13 +183,17 @@ fn global_update_lock() -> &'static Mutex<()> {
impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
#[instrument(
name = "GlobalAutoUpdate",
skip(user, update),
fields(user_id = user.id, update_id = update.id)
"GlobalAutoUpdate",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
if !user.admin {
return Err(
@@ -190,6 +215,9 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
let servers = find_collect(&db_client().servers, None, None)
.await
.context("Failed to query for servers from database")?;
let swarms = find_collect(&db_client().swarms, None, None)
.await
.context("Failed to query for swarms from database")?;
let query = doc! {
"$or": [
@@ -198,11 +226,10 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
]
};
let (stacks, repos) = tokio::try_join!(
find_collect(&db_client().stacks, query.clone(), None),
find_collect(&db_client().repos, None, None)
)
.context("Failed to query for resources from database")?;
let stacks =
find_collect(&db_client().stacks, query.clone(), None)
.await
.context("Failed to query for stacks from database")?;
let server_status_cache = server_status_cache();
let stack_status_cache = stack_status_cache();
@@ -215,10 +242,23 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
else {
continue;
};
// Only pull running stacks.
if !matches!(status.curr.state, StackState::Running) {
continue;
}
let swarm_or_server = find_swarm_or_server(
&stack.config.swarm_id,
&swarms,
&stack.config.server_id,
&servers,
)?;
if let SwarmOrServer::None = &swarm_or_server {
continue;
}
if let Some(server) =
servers.iter().find(|s| s.id == stack.config.server_id)
// This check is probably redundant along with running check
@@ -229,39 +269,28 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
.map(|s| matches!(s.state, ServerState::Ok))
.unwrap_or_default()
{
let name = stack.name.clone();
let repo = if stack.config.linked_repo.is_empty() {
None
} else {
let Some(repo) =
repos.iter().find(|r| r.id == stack.config.linked_repo)
else {
update.push_error_log(
&format!("Pull Stack {name}"),
format!(
"Did not find any Repo matching {}",
stack.config.linked_repo
),
);
continue;
};
Some(repo.clone())
};
if let Err(e) =
pull_stack_inner(stack, Vec::new(), server, repo, None)
.await
if let Err(e) = check_stack_for_update_inner(
stack.id,
&swarm_or_server,
self.skip_auto_update,
true,
false,
)
.await
{
update.push_error_log(
&format!("Pull Stack {name}"),
&format!("Check Stack {}", stack.name),
format_serror(&e.into()),
);
} else {
if !update.logs[0].stdout.is_empty() {
update.logs[0].stdout.push('\n');
}
update.logs[0]
.stdout
.push_str(&format!("Pulled Stack {}", bold(name)));
update.logs[0].stdout.push_str(&format!(
"Checked Stack {}",
bold(&stack.name)
));
}
}
}
@@ -271,43 +300,51 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
find_collect(&db_client().deployments, query, None)
.await
.context("Failed to query for deployments from database")?;
for deployment in deployments {
let Some(status) =
deployment_status_cache.get(&deployment.id).await
else {
continue;
};
// Only pull running deployments.
if !matches!(status.curr.state, DeploymentState::Running) {
continue;
}
if let Some(server) =
servers.iter().find(|s| s.id == deployment.config.server_id)
// This check is probably redundant along with running check
// but shouldn't hurt
&& server_status_cache
.get(&server.id)
.await
.map(|s| matches!(s.state, ServerState::Ok))
.unwrap_or_default()
let swarm_or_server = find_swarm_or_server(
&deployment.config.swarm_id,
&swarms,
&deployment.config.server_id,
&servers,
)?;
if let SwarmOrServer::None = &swarm_or_server {
continue;
}
let name = deployment.name.clone();
if let Err(e) = check_deployment_for_update_inner(
deployment,
&swarm_or_server,
self.skip_auto_update,
true,
)
.await
{
let name = deployment.name.clone();
if let Err(e) =
pull_deployment_inner(deployment, server).await
{
update.push_error_log(
&format!("Pull Deployment {name}"),
format_serror(&e.into()),
);
} else {
if !update.logs[0].stdout.is_empty() {
update.logs[0].stdout.push('\n');
}
update.logs[0].stdout.push_str(&format!(
"Pulled Deployment {}",
bold(name)
));
update.push_error_log(
&format!("Check Deployment {name}"),
format_serror(&e.into()),
);
} else {
if !update.logs[0].stdout.is_empty() {
update.logs[0].stdout.push('\n');
}
update.logs[0]
.stdout
.push_str(&format!("Checked Deployment {}", bold(name)));
}
}
@@ -317,3 +354,256 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
Ok(update)
}
}
//
/// Makes sure the method can only be called once at a time
fn global_rotate_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(Default::default)
}
impl Resolve<ExecuteArgs> for RotateAllServerKeys {
#[instrument(
"RotateAllServerKeys",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
if !user.admin {
return Err(
anyhow!("This method is admin only.")
.status_code(StatusCode::FORBIDDEN),
);
}
let _lock = global_rotate_lock()
.try_lock()
.context("Key rotation already in progress...")?;
let mut update = update.clone();
update_update(update.clone()).await?;
let mut servers = db_client()
.servers
.find(Document::new())
.await
.context("Failed to query servers from database")?;
let server_status_cache = server_status_cache();
let mut log = String::new();
while let Some(server) = servers.next().await {
let server = match server {
Ok(server) => server,
Err(e) => {
warn!("Failed to parse Server | {e:#}");
continue;
}
};
if !server.config.auto_rotate_keys {
let _ = write!(
&mut log,
"\nSkipping {}: Key Rotation Disabled ⚙️",
bold(&server.name)
);
continue;
}
let Some(status) = server_status_cache.get(&server.id).await
else {
let _ = write!(
&mut log,
"\nSkipping {}: No Status ⚠️",
bold(&server.name)
);
continue;
};
match status.state {
ServerState::Disabled => {
let _ = write!(
&mut log,
"\nSkipping {}: Server Disabled ⚙️",
bold(&server.name)
);
continue;
}
ServerState::NotOk => {
let _ = write!(
&mut log,
"\nSkipping {}: Server Not Ok ⚠️",
bold(&server.name)
);
continue;
}
_ => {}
}
match rotate_server_keys(&server).await {
Ok(_) => {
let _ = write!(
&mut log,
"\nRotated keys for {} ✅",
bold(&server.name)
);
}
Err(e) => {
update.push_error_log(
"Key Rotation Failure",
format_serror(
&e.context(format!(
"Failed to rotate {} keys",
bold(&server.name)
))
.into(),
),
);
}
}
}
update.push_simple_log("Rotate Server Keys", log);
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for RotateCoreKeys {
#[instrument(
"RotateCoreKeys",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
force = self.force,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
if !user.admin {
return Err(
anyhow!("This method is admin only.")
.status_code(StatusCode::FORBIDDEN),
);
}
let _lock = global_rotate_lock()
.try_lock()
.context("Key rotation already in progress...")?;
let mut update = update.clone();
update_update(update.clone()).await?;
let core_keys = core_keys();
if !core_keys.rotatable() {
return Err(anyhow!("Core `private_key` must be pointing to file, for example 'file:/config/keys/core.key'").into());
};
let server_status_cache = server_status_cache();
let servers =
find_collect(&db_client().servers, Document::new(), None)
.await
.context("Failed to query servers from database")?
.into_iter()
.map(|server| async move {
let state = server_status_cache
.get(&server.id)
.await
.map(|s| s.state)
.unwrap_or(ServerState::NotOk);
(server, state)
})
.collect::<FuturesOrdered<_>>()
.collect::<Vec<_>>()
.await;
if !self.force
&& let Some((server, _)) = servers
.iter()
.find(|(_, state)| matches!(state, ServerState::NotOk))
{
return Err(
anyhow!("Server {} is NotOk, stopping key rotation. Pass `force: true` to continue anyways.", server.name).into(),
);
}
let public_key = core_keys
.rotate(mogh_pki::PkiKind::Mutual)
.await?
.into_inner();
info!("New Public Key: {public_key}");
let mut log = format!("New Public Key: {public_key}\n");
for (server, state) in servers {
match state {
ServerState::Disabled => {
let _ = write!(
&mut log,
"\nSkipping {}: Server Disabled ⚙️",
bold(&server.name)
);
continue;
}
ServerState::NotOk => {
// Shouldn't be reached unless 'force: true'
let _ = write!(
&mut log,
"\nSkipping {}: Server Not Ok ⚠️",
bold(&server.name)
);
continue;
}
_ => {}
}
let periphery = periphery_client(&server).await?;
let res = periphery
.request(api::keys::RotateCorePublicKey {
public_key: public_key.clone(),
})
.await;
match res {
Ok(_) => {
let _ = write!(
&mut log,
"\nRotated key for {} ✅",
bold(&server.name)
);
}
Err(e) => {
update.push_error_log(
"Key Rotation Failure",
format_serror(
&e.context(format!(
"Failed to rotate for {}. The new Core public key will have to be added manually.",
bold(&server.name)
))
.into(),
),
);
}
}
}
update.push_simple_log("Rotate Core Keys", log);
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}

View File

@@ -1,4 +1,4 @@
use std::{pin::Pin, time::Instant};
use std::pin::Pin;
use anyhow::Context;
use axum::{
@@ -6,9 +6,8 @@ use axum::{
};
use axum_extra::{TypedHeader, headers::ContentType};
use database::mungos::by_id::find_one_by_id;
use derive_variants::{EnumVariants, ExtractVariant};
use formatting::format_serror;
use futures::future::join_all;
use futures_util::future::join_all;
use komodo_client::{
api::execute::*,
entities::{
@@ -18,16 +17,18 @@ use komodo_client::{
user::User,
},
};
use resolver_api::Resolve;
use response::JsonString;
use mogh_auth_server::middleware::authenticate_request;
use mogh_error::Json;
use mogh_error::JsonString;
use mogh_resolver::Resolve;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serror::Json;
use strum::{Display, EnumDiscriminants};
use typeshare::typeshare;
use uuid::Uuid;
use crate::{
auth::auth_request,
auth::KomodoAuthImpl,
helpers::update::{init_execution_update, update_update},
resource::{KomodoResource, list_full_for_user_using_pattern},
state::db_client,
@@ -42,52 +43,29 @@ mod procedure;
mod repo;
mod server;
mod stack;
mod swarm;
mod sync;
use super::Variant;
pub use {
deployment::pull_deployment_inner, stack::pull_stack_inner,
};
pub struct ExecuteArgs {
/// The execution id.
/// Unique for every /execute call.
pub id: Uuid,
pub user: User,
pub update: Update,
}
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants,
)]
#[variant_derive(Debug)]
#[strum_discriminants(name(ExecuteRequestVariant), derive(Display))]
#[args(ExecuteArgs)]
#[response(JsonString)]
#[error(serror::Error)]
#[error(mogh_error::Error)]
#[serde(tag = "type", content = "params")]
pub enum ExecuteRequest {
// ==== SERVER ====
StartContainer(StartContainer),
RestartContainer(RestartContainer),
PauseContainer(PauseContainer),
UnpauseContainer(UnpauseContainer),
StopContainer(StopContainer),
DestroyContainer(DestroyContainer),
StartAllContainers(StartAllContainers),
RestartAllContainers(RestartAllContainers),
PauseAllContainers(PauseAllContainers),
UnpauseAllContainers(UnpauseAllContainers),
StopAllContainers(StopAllContainers),
PruneContainers(PruneContainers),
DeleteNetwork(DeleteNetwork),
PruneNetworks(PruneNetworks),
DeleteImage(DeleteImage),
PruneImages(PruneImages),
DeleteVolume(DeleteVolume),
PruneVolumes(PruneVolumes),
PruneDockerBuilders(PruneDockerBuilders),
PruneBuildx(PruneBuildx),
PruneSystem(PruneSystem),
// ==== STACK ====
DeployStack(DeployStack),
BatchDeployStack(BatchDeployStack),
@@ -138,31 +116,69 @@ pub enum ExecuteRequest {
RunAction(RunAction),
BatchRunAction(BatchRunAction),
// ==== SYNC ====
RunSync(RunSync),
// ==== ALERTER ====
TestAlerter(TestAlerter),
SendAlert(SendAlert),
// ==== SYNC ====
RunSync(RunSync),
// ==== SERVER ====
StartContainer(StartContainer),
RestartContainer(RestartContainer),
PauseContainer(PauseContainer),
UnpauseContainer(UnpauseContainer),
StopContainer(StopContainer),
DestroyContainer(DestroyContainer),
StartAllContainers(StartAllContainers),
RestartAllContainers(RestartAllContainers),
PauseAllContainers(PauseAllContainers),
UnpauseAllContainers(UnpauseAllContainers),
StopAllContainers(StopAllContainers),
PruneContainers(PruneContainers),
DeleteNetwork(DeleteNetwork),
PruneNetworks(PruneNetworks),
DeleteImage(DeleteImage),
PruneImages(PruneImages),
DeleteVolume(DeleteVolume),
PruneVolumes(PruneVolumes),
PruneDockerBuilders(PruneDockerBuilders),
PruneBuildx(PruneBuildx),
PruneSystem(PruneSystem),
// ==== SWARM ====
RemoveSwarmNodes(RemoveSwarmNodes),
RemoveSwarmStacks(RemoveSwarmStacks),
RemoveSwarmServices(RemoveSwarmServices),
CreateSwarmConfig(CreateSwarmConfig),
RotateSwarmConfig(RotateSwarmConfig),
RemoveSwarmConfigs(RemoveSwarmConfigs),
CreateSwarmSecret(CreateSwarmSecret),
RotateSwarmSecret(RotateSwarmSecret),
RemoveSwarmSecrets(RemoveSwarmSecrets),
// ==== MAINTENANCE ====
ClearRepoCache(ClearRepoCache),
BackupCoreDatabase(BackupCoreDatabase),
GlobalAutoUpdate(GlobalAutoUpdate),
RotateAllServerKeys(RotateAllServerKeys),
RotateCoreKeys(RotateCoreKeys),
}
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler))
.layer(middleware::from_fn(auth_request))
.layer(middleware::from_fn(
authenticate_request::<KomodoAuthImpl, true>,
))
}
async fn variant_handler(
user: Extension<User>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<(TypedHeader<ContentType>, String)> {
) -> mogh_error::Result<(TypedHeader<ContentType>, String)> {
let req: ExecuteRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
@@ -173,7 +189,7 @@ async fn variant_handler(
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<ExecuteRequest>,
) -> serror::Result<(TypedHeader<ContentType>, String)> {
) -> mogh_error::Result<(TypedHeader<ContentType>, String)> {
let res = match inner_handler(request, user).await? {
ExecutionResult::Single(update) => serde_json::to_string(&update)
.context("Failed to serialize Update")?,
@@ -201,7 +217,7 @@ pub fn inner_handler(
>,
> {
Box::pin(async move {
let req_id = Uuid::new_v4();
let task_id = Uuid::new_v4();
// Need to validate no cancel is active before any update is created.
// This ensures no double update created if Cancel is called more than once for the same request.
@@ -217,14 +233,14 @@ pub fn inner_handler(
// here either.
if update.operation == Operation::None {
return Ok(ExecutionResult::Batch(
task(req_id, request, user, update).await?,
task(task_id, request, user, update).await?,
));
}
// Spawn a task for the execution which continues
// running after this method returns.
let handle =
tokio::spawn(task(req_id, request, user, update.clone()));
tokio::spawn(task(task_id, request, user, update.clone()));
// Spawns another task to monitor the first for failures,
// and add the log to Update about it (which primary task can't do because it errored out)
@@ -233,11 +249,11 @@ pub fn inner_handler(
async move {
let log = match handle.await {
Ok(Err(e)) => {
warn!("/execute request {req_id} task error: {e:#}",);
warn!("/execute request {task_id} task error: {e:#}",);
Log::error("Task Error", format_serror(&e.into()))
}
Err(e) => {
warn!("/execute request {req_id} spawn error: {e:?}",);
warn!("/execute request {task_id} spawn error: {e:?}",);
Log::error("Spawn Error", format!("{e:#?}"))
}
_ => return,
@@ -251,8 +267,8 @@ pub fn inner_handler(
let mut update =
find_one_by_id(&db_client().updates, &update_id)
.await
.context("failed to query to db")?
.context("no update exists with given id")?;
.context("Failed to query to db")?
.context("No update exists with given id")?;
update.logs.push(log);
update.finalize();
update_update(update).await
@@ -261,7 +277,7 @@ pub fn inner_handler(
if let Err(e) = res {
warn!(
"failed to update update with task error log | {e:#}"
"Failed to update update with task error log | {e:#}"
);
}
}
@@ -271,40 +287,33 @@ pub fn inner_handler(
})
}
#[instrument(
name = "ExecuteRequest",
skip(user, update),
fields(
user_id = user.id,
update_id = update.id,
request = format!("{:?}", request.extract_variant()))
)
]
async fn task(
req_id: Uuid,
id: Uuid,
request: ExecuteRequest,
user: User,
update: Update,
) -> anyhow::Result<String> {
info!("/execute request {req_id} | user: {}", user.username);
let timer = Instant::now();
let variant: ExecuteRequestVariant = (&request).into();
let res = match request.resolve(&ExecuteArgs { user, update }).await
{
Err(e) => Err(e.error),
Ok(JsonString::Err(e)) => Err(
anyhow::Error::from(e).context("failed to serialize response"),
),
Ok(JsonString::Ok(res)) => Ok(res),
};
info!(
"EXECUTE REQUEST {id} | METHOD: {variant} | USER: {} ({})",
user.username, user.id
);
let res =
match request.resolve(&ExecuteArgs { user, update, id }).await {
Err(e) => Err(e.error),
Ok(JsonString::Err(e)) => Err(
anyhow::Error::from(e)
.context("failed to serialize response"),
),
Ok(JsonString::Ok(res)) => Ok(res),
};
if let Err(e) = &res {
warn!("/execute request {req_id} error: {e:#}");
warn!("EXECUTE REQUEST {id} | METHOD: {variant} | ERROR: {e:#}");
}
let elapsed = timer.elapsed();
debug!("/execute request {req_id} | resolve time: {elapsed:?}");
res
}
@@ -313,6 +322,7 @@ trait BatchExecute {
fn single_request(name: String) -> ExecuteRequest;
}
#[instrument("BatchExecute", skip(user))]
async fn batch_execute<E: BatchExecute>(
pattern: &str,
user: &User,
@@ -325,6 +335,7 @@ async fn batch_execute<E: BatchExecute>(
&[],
)
.await?;
let futures = resources.into_iter().map(|resource| {
let user = user.clone();
async move {

View File

@@ -17,7 +17,7 @@ use komodo_client::{
user::User,
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use tokio::sync::Mutex;
use crate::{
@@ -38,11 +38,15 @@ impl super::BatchExecute for BatchRunProcedure {
}
impl Resolve<ExecuteArgs> for BatchRunProcedure {
#[instrument(name = "BatchRunProcedure", skip(user), fields(user_id = user.id))]
#[instrument(
"BatchRunProcedure",
skip_all,
fields(operator = user.id)
)]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunProcedure>(&self.pattern, user)
.await?,
@@ -51,11 +55,20 @@ impl Resolve<ExecuteArgs> for BatchRunProcedure {
}
impl Resolve<ExecuteArgs> for RunProcedure {
#[instrument(name = "RunProcedure", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"RunProcedure",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
procedure = self.procedure,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
Ok(
resolve_inner(self.procedure, user.clone(), update.clone())
.await?,
@@ -146,7 +159,6 @@ fn resolve_inner(
update_update(update.clone()).await?;
if !update.success && procedure.config.failure_alert {
warn!("procedure unsuccessful, alerting...");
let target = update.target.clone();
tokio::spawn(async move {
let alert = Alert {

View File

@@ -22,15 +22,15 @@ use komodo_client::{
update::{Log, Update},
},
};
use mogh_resolver::Resolve;
use periphery_client::api;
use resolver_api::Resolve;
use tokio_util::sync::CancellationToken;
use crate::{
alert::send_alerts,
api::write::WriteArgs,
helpers::{
builder::{cleanup_builder_instance, get_builder_periphery},
builder::{cleanup_builder_instance, connect_builder_periphery},
channel::repo_cancel_channel,
git_token, periphery_client,
query::{VariablesAndSecrets, get_variables_and_secrets},
@@ -51,11 +51,19 @@ impl super::BatchExecute for BatchCloneRepo {
}
impl Resolve<ExecuteArgs> for BatchCloneRepo {
#[instrument(name = "BatchCloneRepo", skip( user), fields(user_id = user.id))]
#[instrument(
"BatchCloneRepo",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchCloneRepo>(&self.pattern, user)
.await?,
@@ -64,11 +72,20 @@ impl Resolve<ExecuteArgs> for BatchCloneRepo {
}
impl Resolve<ExecuteArgs> for CloneRepo {
#[instrument(name = "CloneRepo", skip( user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"CloneRepo",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
repo = self.repo,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let mut repo = get_check_permissions::<Repo>(
&self.repo,
user,
@@ -105,7 +122,7 @@ impl Resolve<ExecuteArgs> for CloneRepo {
let server =
resource::get::<Server>(&repo.config.server_id).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
// interpolate variables / secrets, returning the sanitizing replacers to send to
// periphery so it may sanitize the final command for safe logging (avoids exposing secret values)
@@ -165,11 +182,19 @@ impl super::BatchExecute for BatchPullRepo {
}
impl Resolve<ExecuteArgs> for BatchPullRepo {
#[instrument(name = "BatchPullRepo", skip(user), fields(user_id = user.id))]
#[instrument(
"BatchPullRepo",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
pattern = self.pattern
)
)]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchPullRepo>(&self.pattern, user)
.await?,
@@ -178,11 +203,20 @@ impl Resolve<ExecuteArgs> for BatchPullRepo {
}
impl Resolve<ExecuteArgs> for PullRepo {
#[instrument(name = "PullRepo", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"PullRepo",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
repo = self.repo,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let mut repo = get_check_permissions::<Repo>(
&self.repo,
user,
@@ -220,7 +254,7 @@ impl Resolve<ExecuteArgs> for PullRepo {
let server =
resource::get::<Server>(&repo.config.server_id).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
// interpolate variables / secrets, returning the sanitizing replacers to send to
// periphery so it may sanitize the final command for safe logging (avoids exposing secret values)
@@ -275,10 +309,14 @@ impl Resolve<ExecuteArgs> for PullRepo {
}
}
#[instrument(skip_all, fields(update_id = update.id))]
#[instrument(
"HandleRepoEarlyReturn",
skip_all,
fields(update_id = update.id)
)]
async fn handle_repo_update_return(
update: Update,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
// The Err case of to_document should be unreachable,
@@ -297,7 +335,7 @@ async fn handle_repo_update_return(
Ok(update)
}
#[instrument]
#[instrument("UpdateLastPulledTime")]
async fn update_last_pulled_time(repo_name: &str) {
let res = db_client()
.repos
@@ -321,11 +359,19 @@ impl super::BatchExecute for BatchBuildRepo {
}
impl Resolve<ExecuteArgs> for BatchBuildRepo {
#[instrument(name = "BatchBuildRepo", skip(user), fields(user_id = user.id))]
#[instrument(
"BatchBuildRepo",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
)
)]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchBuildRepo>(&self.pattern, user)
.await?,
@@ -334,11 +380,20 @@ impl Resolve<ExecuteArgs> for BatchBuildRepo {
}
impl Resolve<ExecuteArgs> for BuildRepo {
#[instrument(name = "BuildRepo", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"BuildRepo",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
repo = self.repo,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let mut repo = get_check_permissions::<Repo>(
&self.repo,
user,
@@ -419,7 +474,7 @@ impl Resolve<ExecuteArgs> for BuildRepo {
// GET BUILDER PERIPHERY
let (periphery, cleanup_data) = match get_builder_periphery(
let (periphery, cleanup_data) = match connect_builder_periphery(
repo.name.clone(),
None,
builder,
@@ -463,7 +518,7 @@ impl Resolve<ExecuteArgs> for BuildRepo {
_ = cancel.cancelled() => {
debug!("build cancelled during clone, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(cleanup_data, &mut update)
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
info!("builder cleaned up");
return handle_builder_early_return(update, repo.id, repo.name, true).await
@@ -510,7 +565,8 @@ impl Resolve<ExecuteArgs> for BuildRepo {
// If building on temporary cloud server (AWS),
// this will terminate the server.
cleanup_builder_instance(cleanup_data, &mut update).await;
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
@@ -530,7 +586,6 @@ impl Resolve<ExecuteArgs> for BuildRepo {
update_update(update.clone()).await?;
if !update.success {
warn!("repo build unsuccessful, alerting...");
let target = update.target.clone();
tokio::spawn(async move {
let alert = Alert {
@@ -553,13 +608,13 @@ impl Resolve<ExecuteArgs> for BuildRepo {
}
}
#[instrument(skip(update))]
#[instrument("HandleRepoBuildEarlyReturn", skip(update))]
async fn handle_builder_early_return(
mut update: Update,
repo_id: String,
repo_name: String,
is_cancel: bool,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
update.finalize();
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
@@ -577,7 +632,6 @@ async fn handle_builder_early_return(
}
update_update(update.clone()).await?;
if !update.success && !is_cancel {
warn!("repo build unsuccessful, alerting...");
let target = update.target.clone();
tokio::spawn(async move {
let alert = Alert {
@@ -598,7 +652,6 @@ async fn handle_builder_early_return(
Ok(update)
}
#[instrument(skip_all)]
pub async fn validate_cancel_repo_build(
request: &ExecuteRequest,
) -> anyhow::Result<()> {
@@ -648,11 +701,20 @@ pub async fn validate_cancel_repo_build(
}
impl Resolve<ExecuteArgs> for CancelRepoBuild {
#[instrument(name = "CancelRepoBuild", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"CancelRepoBuild",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
repo = self.repo,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let repo = get_check_permissions::<Repo>(
&self.repo,
user,
@@ -708,6 +770,13 @@ impl Resolve<ExecuteArgs> for CancelRepoBuild {
}
}
#[instrument(
"Interpolate",
skip_all,
fields(
skip_secret_interp = repo.config.skip_secret_interp
)
)]
async fn interpolate(
repo: &mut Repo,
update: &mut Update,

View File

@@ -9,8 +9,8 @@ use komodo_client::{
update::{Log, Update},
},
};
use mogh_resolver::Resolve;
use periphery_client::api;
use resolver_api::Resolve;
use crate::{
helpers::{periphery_client, update::update_update},
@@ -22,11 +22,21 @@ use crate::{
use super::ExecuteArgs;
impl Resolve<ExecuteArgs> for StartContainer {
#[instrument(name = "StartContainer", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"StartContainer",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
container = self.container,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -50,7 +60,7 @@ impl Resolve<ExecuteArgs> for StartContainer {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log = match periphery
.request(api::container::StartContainer {
@@ -60,8 +70,8 @@ impl Resolve<ExecuteArgs> for StartContainer {
{
Ok(log) => log,
Err(e) => Log::error(
"start container",
format_serror(&e.context("failed to start container").into()),
"Start Container",
format_serror(&e.context("Failed to start container").into()),
),
};
@@ -76,11 +86,21 @@ impl Resolve<ExecuteArgs> for StartContainer {
}
impl Resolve<ExecuteArgs> for RestartContainer {
#[instrument(name = "RestartContainer", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"RestartContainer",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
container = self.container,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -104,7 +124,7 @@ impl Resolve<ExecuteArgs> for RestartContainer {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log = match periphery
.request(api::container::RestartContainer {
@@ -114,9 +134,9 @@ impl Resolve<ExecuteArgs> for RestartContainer {
{
Ok(log) => log,
Err(e) => Log::error(
"restart container",
"Restart Container",
format_serror(
&e.context("failed to restart container").into(),
&e.context("Failed to restart container").into(),
),
),
};
@@ -132,11 +152,21 @@ impl Resolve<ExecuteArgs> for RestartContainer {
}
impl Resolve<ExecuteArgs> for PauseContainer {
#[instrument(name = "PauseContainer", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"PauseContainer",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
container = self.container,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -160,7 +190,7 @@ impl Resolve<ExecuteArgs> for PauseContainer {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log = match periphery
.request(api::container::PauseContainer {
@@ -170,8 +200,8 @@ impl Resolve<ExecuteArgs> for PauseContainer {
{
Ok(log) => log,
Err(e) => Log::error(
"pause container",
format_serror(&e.context("failed to pause container").into()),
"Pause Container",
format_serror(&e.context("Failed to pause container").into()),
),
};
@@ -186,11 +216,21 @@ impl Resolve<ExecuteArgs> for PauseContainer {
}
impl Resolve<ExecuteArgs> for UnpauseContainer {
#[instrument(name = "UnpauseContainer", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"UnpauseContainer",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
container = self.container,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -214,7 +254,7 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log = match periphery
.request(api::container::UnpauseContainer {
@@ -224,9 +264,9 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
{
Ok(log) => log,
Err(e) => Log::error(
"unpause container",
"Unpause Container",
format_serror(
&e.context("failed to unpause container").into(),
&e.context("Failed to unpause container").into(),
),
),
};
@@ -242,11 +282,23 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
}
impl Resolve<ExecuteArgs> for StopContainer {
#[instrument(name = "StopContainer", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"StopContainer",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
container = self.container,
signal = format!("{:?}", self.signal),
time = self.time,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -270,7 +322,7 @@ impl Resolve<ExecuteArgs> for StopContainer {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log = match periphery
.request(api::container::StopContainer {
@@ -282,8 +334,8 @@ impl Resolve<ExecuteArgs> for StopContainer {
{
Ok(log) => log,
Err(e) => Log::error(
"stop container",
format_serror(&e.context("failed to stop container").into()),
"Stop Container",
format_serror(&e.context("Failed to stop container").into()),
),
};
@@ -298,11 +350,23 @@ impl Resolve<ExecuteArgs> for StopContainer {
}
impl Resolve<ExecuteArgs> for DestroyContainer {
#[instrument(name = "DestroyContainer", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"DestroyContainer",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
container = self.container,
signal = format!("{:?}", self.signal),
time = self.time,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let DestroyContainer {
server,
container,
@@ -332,7 +396,7 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log = match periphery
.request(api::container::RemoveContainer {
@@ -344,8 +408,10 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
{
Ok(log) => log,
Err(e) => Log::error(
"stop container",
format_serror(&e.context("failed to stop container").into()),
"Remove Container",
format_serror(
&e.context("Failed to remove container").into(),
),
),
};
@@ -360,11 +426,20 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
}
impl Resolve<ExecuteArgs> for StartAllContainers {
#[instrument(name = "StartAllContainers", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"StartAllContainers",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -387,16 +462,17 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
update_update(update.clone()).await?;
let logs = periphery_client(&server)?
let logs = periphery_client(&server)
.await?
.request(api::container::StartAllContainers {})
.await
.context("failed to start all containers on host")?;
.context("Failed to start all containers on host")?;
update.logs.extend(logs);
if all_logs_success(&update.logs) {
update.push_simple_log(
"start all containers",
"Start All Containers",
String::from("All containers have been started on the host."),
);
}
@@ -410,11 +486,20 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
}
impl Resolve<ExecuteArgs> for RestartAllContainers {
#[instrument(name = "RestartAllContainers", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"RestartAllContainers",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -437,16 +522,17 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
update_update(update.clone()).await?;
let logs = periphery_client(&server)?
let logs = periphery_client(&server)
.await?
.request(api::container::RestartAllContainers {})
.await
.context("failed to restart all containers on host")?;
.context("Failed to restart all containers on host")?;
update.logs.extend(logs);
if all_logs_success(&update.logs) {
update.push_simple_log(
"restart all containers",
"Restart All Containers",
String::from(
"All containers have been restarted on the host.",
),
@@ -462,11 +548,20 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
}
impl Resolve<ExecuteArgs> for PauseAllContainers {
#[instrument(name = "PauseAllContainers", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"PauseAllContainers",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -489,16 +584,17 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
update_update(update.clone()).await?;
let logs = periphery_client(&server)?
let logs = periphery_client(&server)
.await?
.request(api::container::PauseAllContainers {})
.await
.context("failed to pause all containers on host")?;
.context("Failed to pause all containers on host")?;
update.logs.extend(logs);
if all_logs_success(&update.logs) {
update.push_simple_log(
"pause all containers",
"Pause All Containers",
String::from("All containers have been paused on the host."),
);
}
@@ -512,11 +608,20 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
}
impl Resolve<ExecuteArgs> for UnpauseAllContainers {
#[instrument(name = "UnpauseAllContainers", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"UnpauseAllContainers",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -539,16 +644,17 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
update_update(update.clone()).await?;
let logs = periphery_client(&server)?
let logs = periphery_client(&server)
.await?
.request(api::container::UnpauseAllContainers {})
.await
.context("failed to unpause all containers on host")?;
.context("Failed to unpause all containers on host")?;
update.logs.extend(logs);
if all_logs_success(&update.logs) {
update.push_simple_log(
"unpause all containers",
"Unpause All Containers",
String::from(
"All containers have been unpaused on the host.",
),
@@ -564,11 +670,20 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
}
impl Resolve<ExecuteArgs> for StopAllContainers {
#[instrument(name = "StopAllContainers", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"StopAllContainers",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -591,16 +706,17 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
update_update(update.clone()).await?;
let logs = periphery_client(&server)?
let logs = periphery_client(&server)
.await?
.request(api::container::StopAllContainers {})
.await
.context("failed to stop all containers on host")?;
.context("Failed to stop all containers on host")?;
update.logs.extend(logs);
if all_logs_success(&update.logs) {
update.push_simple_log(
"stop all containers",
"Stop All Containers",
String::from("All containers have been stopped on the host."),
);
}
@@ -614,11 +730,20 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
}
impl Resolve<ExecuteArgs> for PruneContainers {
#[instrument(name = "PruneContainers", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"PruneContainers",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -641,20 +766,20 @@ impl Resolve<ExecuteArgs> for PruneContainers {
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log = match periphery
.request(api::container::PruneContainers {})
.await
.context(format!(
"failed to prune containers on server {}",
"Failed to prune containers on server {}",
server.name
)) {
Ok(log) => log,
Err(e) => Log::error(
"prune containers",
"Prune Containers",
format_serror(
&e.context("failed to prune containers").into(),
&e.context("Failed to prune containers").into(),
),
),
};
@@ -670,11 +795,21 @@ impl Resolve<ExecuteArgs> for PruneContainers {
}
impl Resolve<ExecuteArgs> for DeleteNetwork {
#[instrument(name = "DeleteNetwork", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"DeleteNetwork",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
network = self.name
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -686,23 +821,23 @@ impl Resolve<ExecuteArgs> for DeleteNetwork {
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log = match periphery
.request(api::network::DeleteNetwork {
.request(api::docker::DeleteNetwork {
name: self.name.clone(),
})
.await
.context(format!(
"failed to delete network {} on server {}",
"Failed to delete network {} on server {}",
self.name, server.name
)) {
Ok(log) => log,
Err(e) => Log::error(
"delete network",
"Delete Network",
format_serror(
&e.context(format!(
"failed to delete network {}",
"Failed to delete network {}",
self.name
))
.into(),
@@ -721,11 +856,20 @@ impl Resolve<ExecuteArgs> for DeleteNetwork {
}
impl Resolve<ExecuteArgs> for PruneNetworks {
#[instrument(name = "PruneNetworks", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"PruneNetworks",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -748,19 +892,19 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log = match periphery
.request(api::network::PruneNetworks {})
.request(api::docker::PruneNetworks {})
.await
.context(format!(
"failed to prune networks on server {}",
"Failed to prune networks on server {}",
server.name
)) {
Ok(log) => log,
Err(e) => Log::error(
"prune networks",
format_serror(&e.context("failed to prune networks").into()),
"Prune Networks",
format_serror(&e.context("Failed to prune networks").into()),
),
};
@@ -775,11 +919,21 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
}
impl Resolve<ExecuteArgs> for DeleteImage {
#[instrument(name = "DeleteImage", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"DeleteImage",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
image = self.name,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -791,22 +945,22 @@ impl Resolve<ExecuteArgs> for DeleteImage {
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log = match periphery
.request(api::image::DeleteImage {
.request(api::docker::DeleteImage {
name: self.name.clone(),
})
.await
.context(format!(
"failed to delete image {} on server {}",
"Failed to delete image {} on server {}",
self.name, server.name
)) {
Ok(log) => log,
Err(e) => Log::error(
"delete image",
format_serror(
&e.context(format!("failed to delete image {}", self.name))
&e.context(format!("Failed to delete image {}", self.name))
.into(),
),
),
@@ -823,11 +977,20 @@ impl Resolve<ExecuteArgs> for DeleteImage {
}
impl Resolve<ExecuteArgs> for PruneImages {
#[instrument(name = "PruneImages", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"PruneImages",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -850,15 +1013,15 @@ impl Resolve<ExecuteArgs> for PruneImages {
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log =
match periphery.request(api::image::PruneImages {}).await {
match periphery.request(api::docker::PruneImages {}).await {
Ok(log) => log,
Err(e) => Log::error(
"prune images",
"Prune Images",
format!(
"failed to prune images on server {} | {e:#?}",
"Failed to prune images on server {} | {e:#?}",
server.name
),
),
@@ -875,11 +1038,21 @@ impl Resolve<ExecuteArgs> for PruneImages {
}
impl Resolve<ExecuteArgs> for DeleteVolume {
#[instrument(name = "DeleteVolume", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"DeleteVolume",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
volume = self.name,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -891,15 +1064,15 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log = match periphery
.request(api::volume::DeleteVolume {
.request(api::docker::DeleteVolume {
name: self.name.clone(),
})
.await
.context(format!(
"failed to delete volume {} on server {}",
"Failed to delete volume {} on server {}",
self.name, server.name
)) {
Ok(log) => log,
@@ -907,7 +1080,7 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
"delete volume",
format_serror(
&e.context(format!(
"failed to delete volume {}",
"Failed to delete volume {}",
self.name
))
.into(),
@@ -926,11 +1099,20 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
}
impl Resolve<ExecuteArgs> for PruneVolumes {
#[instrument(name = "PruneVolumes", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"PruneVolumes",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -953,15 +1135,15 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log =
match periphery.request(api::volume::PruneVolumes {}).await {
match periphery.request(api::docker::PruneVolumes {}).await {
Ok(log) => log,
Err(e) => Log::error(
"prune volumes",
"Prune Volumes",
format!(
"failed to prune volumes on server {} | {e:#?}",
"Failed to prune volumes on server {} | {e:#?}",
server.name
),
),
@@ -978,11 +1160,20 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
}
impl Resolve<ExecuteArgs> for PruneDockerBuilders {
#[instrument(name = "PruneDockerBuilders", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"PruneDockerBuilders",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -1005,15 +1196,15 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log =
match periphery.request(api::build::PruneBuilders {}).await {
Ok(log) => log,
Err(e) => Log::error(
"prune builders",
"Prune Builders",
format!(
"failed to docker builder prune on server {} | {e:#?}",
"Failed to docker builder prune on server {} | {e:#?}",
server.name
),
),
@@ -1030,11 +1221,20 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
}
impl Resolve<ExecuteArgs> for PruneBuildx {
#[instrument(name = "PruneBuildx", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"PruneBuildx",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -1057,15 +1257,15 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log =
match periphery.request(api::build::PruneBuildx {}).await {
Ok(log) => log,
Err(e) => Log::error(
"prune buildx",
"Prune Buildx",
format!(
"failed to docker buildx prune on server {} | {e:#?}",
"Failed to docker buildx prune on server {} | {e:#?}",
server.name
),
),
@@ -1082,11 +1282,20 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
}
impl Resolve<ExecuteArgs> for PruneSystem {
#[instrument(name = "PruneSystem", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"PruneSystem",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -1109,14 +1318,14 @@ impl Resolve<ExecuteArgs> for PruneSystem {
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let log = match periphery.request(api::PruneSystem {}).await {
Ok(log) => log,
Err(e) => Log::error(
"prune system",
"Prune System",
format!(
"failed to docker system prune on server {} | {e:#?}",
"Failed to docker system prune on server {} | {e:#?}",
server.name
),
),

View File

@@ -1,6 +1,6 @@
use std::{collections::HashSet, str::FromStr};
use anyhow::Context;
use anyhow::{Context, anyhow};
use database::mungos::mongodb::bson::{
doc, oid::ObjectId, to_bson, to_document,
};
@@ -9,7 +9,7 @@ use interpolate::Interpolator;
use komodo_client::{
api::{execute::*, write::RefreshStackCache},
entities::{
FileContents,
FileContents, SwarmOrServer,
permission::PermissionLevel,
repo::Repo,
server::Server,
@@ -20,8 +20,13 @@ use komodo_client::{
user::User,
},
};
use periphery_client::api::compose::*;
use resolver_api::Resolve;
use mogh_error::AddStatusCodeError as _;
use mogh_resolver::Resolve;
use periphery_client::api::{
DeployStackResponse, compose::*, swarm::DeploySwarmStack,
};
use reqwest::StatusCode;
use uuid::Uuid;
use crate::{
api::write::WriteArgs,
@@ -29,14 +34,20 @@ use crate::{
periphery_client,
query::{VariablesAndSecrets, get_variables_and_secrets},
stack_git_token,
swarm::swarm_request,
update::{
add_update_without_send, init_execution_update, update_update,
},
},
monitor::update_cache_for_server,
monitor::{update_cache_for_server, update_cache_for_swarm},
permission::get_check_permissions,
resource,
stack::{execute::execute_compose, get_stack_and_server},
stack::{
execute::{
execute_compose, execute_compose_with_stack_and_server,
},
setup_stack_execution,
},
state::{action_states, db_client},
};
@@ -54,11 +65,19 @@ impl super::BatchExecute for BatchDeployStack {
}
impl Resolve<ExecuteArgs> for BatchDeployStack {
#[instrument(name = "BatchDeployStack", skip(user), fields(user_id = user.id))]
#[instrument(
"BatchDeployStack",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
)
)]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeployStack>(&self.pattern, user)
.await?,
@@ -67,19 +86,31 @@ impl Resolve<ExecuteArgs> for BatchDeployStack {
}
impl Resolve<ExecuteArgs> for DeployStack {
#[instrument(name = "DeployStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"DeployStack",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
services = format!("{:?}", self.services),
stop_time = self.stop_time,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (mut stack, server) = get_stack_and_server(
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (mut stack, swarm_or_server) = setup_stack_execution(
&self.stack,
user,
PermissionLevel::Execute.into(),
true,
)
.await?;
swarm_or_server.verify_has_target()?;
let mut repo = if !stack.config.files_on_host
&& !stack.config.linked_repo.is_empty()
{
@@ -145,26 +176,45 @@ impl Resolve<ExecuteArgs> for DeployStack {
Default::default()
};
let ComposeUpResponse {
let DeployStackResponse {
logs,
deployed,
services,
file_contents,
missing_files,
remote_errors,
compose_config,
merged_config,
commit_hash,
commit_message,
} = periphery_client(&server)?
.request(ComposeUp {
stack: stack.clone(),
services: self.services,
repo,
git_token,
registry_token,
replacers: secret_replacers.into_iter().collect(),
})
.await?;
} = match &swarm_or_server {
SwarmOrServer::None => unreachable!(),
SwarmOrServer::Swarm(swarm) => {
swarm_request(
&swarm.config.server_ids,
DeploySwarmStack {
stack: stack.clone(),
repo,
git_token,
registry_token,
replacers: secret_replacers.into_iter().collect(),
},
)
.await?
}
SwarmOrServer::Server(server) => {
periphery_client(server)
.await?
.request(ComposeUp {
stack: stack.clone(),
services: self.services,
repo,
git_token,
registry_token,
replacers: secret_replacers.into_iter().collect(),
})
.await?
}
};
update.logs.extend(logs);
@@ -198,7 +248,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
})
.collect(),
),
compose_config,
merged_config,
commit_hash.clone(),
commit_message.clone(),
)
@@ -236,7 +286,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
};
let info = to_document(&info)
.context("failed to serialize stack info to bson")?;
.context("Failed to serialize stack info to bson")?;
db_client()
.stacks
@@ -245,22 +295,30 @@ impl Resolve<ExecuteArgs> for DeployStack {
doc! { "$set": { "info": info } },
)
.await
.context("failed to update stack info on db")?;
.context("Failed to update stack info on db")?;
anyhow::Ok(())
};
// This will be weird with single service deploys. Come back to it.
if let Err(e) = update_info.await {
update.push_error_log(
"refresh stack info",
"Refresh Stack Info",
format_serror(
&e.context("failed to refresh stack info on db").into(),
&e.context("Failed to refresh stack info on db").into(),
),
)
}
// Ensure cached stack state up to date by updating server cache
update_cache_for_server(&server, true).await;
match swarm_or_server {
SwarmOrServer::None => unreachable!(),
SwarmOrServer::Swarm(swarm) => {
update_cache_for_swarm(&swarm, true).await;
}
SwarmOrServer::Server(server) => {
update_cache_for_server(&server, true).await;
}
}
update.finalize();
update_update(update.clone()).await?;
@@ -280,11 +338,19 @@ impl super::BatchExecute for BatchDeployStackIfChanged {
}
impl Resolve<ExecuteArgs> for BatchDeployStackIfChanged {
#[instrument(name = "BatchDeployStackIfChanged", skip(user), fields(user_id = user.id))]
#[instrument(
"BatchDeployStackIfChanged",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
)
)]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeployStackIfChanged>(
&self.pattern,
@@ -296,11 +362,21 @@ impl Resolve<ExecuteArgs> for BatchDeployStackIfChanged {
}
impl Resolve<ExecuteArgs> for DeployStackIfChanged {
#[instrument(name = "DeployStackIfChanged", skip(user, update), fields(user_id = user.id))]
#[instrument(
"DeployStackIfChanged",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
stop_time = self.stop_time,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let stack = get_check_permissions::<Stack>(
&self.stack,
user,
@@ -357,6 +433,7 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
.resolve(&ExecuteArgs {
user: user.clone(),
update,
id: *id,
})
.await
}
@@ -466,11 +543,19 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
}
}
#[instrument(
"DeployStackServices",
skip_all,
fields(
stack = stack,
services = format!("{services:?}")
)
)]
async fn deploy_services(
stack: String,
services: Vec<String>,
user: &User,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
// The existing update is initialized to DeployStack,
// but also has not been created on database.
// Setup a new update here.
@@ -487,15 +572,24 @@ async fn deploy_services(
.resolve(&ExecuteArgs {
user: user.clone(),
update,
id: Uuid::new_v4(),
})
.await
}
#[instrument(
"RestartStackServices",
skip_all,
fields(
stack = stack,
services = format!("{services:?}")
)
)]
async fn restart_services(
stack: String,
services: Vec<String>,
user: &User,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
// The existing update is initialized to DeployStack,
// but also has not been created on database.
// Setup a new update here.
@@ -509,6 +603,7 @@ async fn restart_services(
.resolve(&ExecuteArgs {
user: user.clone(),
update,
id: Uuid::new_v4(),
})
.await
}
@@ -525,6 +620,11 @@ async fn restart_services(
/// Changes to config files after restart is applied should
/// be taken as the deployed contents, otherwise next changed check
/// will restart service again for no reason.
#[instrument(
"UpdateStackDeployedContents",
skip_all,
fields(stack = id)
)]
async fn update_deployed_contents_with_latest(
id: &str,
contents: Option<Vec<StackRemoteFileContents>>,
@@ -662,11 +762,19 @@ impl super::BatchExecute for BatchPullStack {
}
impl Resolve<ExecuteArgs> for BatchPullStack {
#[instrument(name = "BatchPullStack", skip(user), fields(user_id = user.id))]
#[instrument(
"BatchPullStack",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
)
)]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchPullStack>(&self.pattern, user)
.await?,
@@ -699,6 +807,14 @@ async fn maybe_pull_stack(
Ok(())
}
#[instrument(
"PullStackInner",
skip_all,
fields(
stack = stack.id,
services = format!("{services:?}"),
)
)]
pub async fn pull_stack_inner(
mut stack: Stack,
services: Vec<String>,
@@ -749,7 +865,8 @@ pub async fn pull_stack_inner(
Default::default()
};
let res = periphery_client(server)?
let res = periphery_client(server)
.await?
.request(ComposePull {
stack,
services,
@@ -767,19 +884,37 @@ pub async fn pull_stack_inner(
}
impl Resolve<ExecuteArgs> for PullStack {
#[instrument(name = "PullStack", skip(user, update), fields(user_id = user.id))]
#[instrument(
"PullStack",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
services = format!("{:?}", self.services),
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (stack, server) = get_stack_and_server(
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (stack, swarm_or_server) = setup_stack_execution(
&self.stack,
user,
PermissionLevel::Execute.into(),
true,
)
.await?;
let SwarmOrServer::Server(server) = swarm_or_server else {
return Err(
anyhow!(
"PullStack should not be called for Stack in Swarm Mode"
)
.status_code(StatusCode::BAD_REQUEST),
);
};
let repo = if !stack.config.files_on_host
&& !stack.config.linked_repo.is_empty()
{
@@ -820,11 +955,21 @@ impl Resolve<ExecuteArgs> for PullStack {
}
impl Resolve<ExecuteArgs> for StartStack {
#[instrument(name = "StartStack", skip(user, update), fields(user_id = user.id))]
#[instrument(
"StartStack",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
services = format!("{:?}", self.services),
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
execute_compose::<StartStack>(
&self.stack,
self.services,
@@ -839,11 +984,21 @@ impl Resolve<ExecuteArgs> for StartStack {
}
impl Resolve<ExecuteArgs> for RestartStack {
#[instrument(name = "RestartStack", skip(user, update), fields(user_id = user.id))]
#[instrument(
"RestartStack",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
services = format!("{:?}", self.services),
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
execute_compose::<RestartStack>(
&self.stack,
self.services,
@@ -860,11 +1015,21 @@ impl Resolve<ExecuteArgs> for RestartStack {
}
impl Resolve<ExecuteArgs> for PauseStack {
#[instrument(name = "PauseStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"PauseStack",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
services = format!("{:?}", self.services),
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
execute_compose::<PauseStack>(
&self.stack,
self.services,
@@ -879,11 +1044,21 @@ impl Resolve<ExecuteArgs> for PauseStack {
}
impl Resolve<ExecuteArgs> for UnpauseStack {
#[instrument(name = "UnpauseStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"UnpauseStack",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
services = format!("{:?}", self.services),
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
execute_compose::<UnpauseStack>(
&self.stack,
self.services,
@@ -898,11 +1073,21 @@ impl Resolve<ExecuteArgs> for UnpauseStack {
}
impl Resolve<ExecuteArgs> for StopStack {
#[instrument(name = "StopStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"StopStack",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
services = format!("{:?}", self.services),
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
execute_compose::<StopStack>(
&self.stack,
self.services,
@@ -929,11 +1114,19 @@ impl super::BatchExecute for BatchDestroyStack {
}
impl Resolve<ExecuteArgs> for BatchDestroyStack {
#[instrument(name = "BatchDestroyStack", skip(user), fields(user_id = user.id))]
#[instrument(
"BatchDestroyStack",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
)
)]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDestroyStack>(&self.pattern, user)
.await
.map_err(Into::into)
@@ -941,38 +1134,133 @@ impl Resolve<ExecuteArgs> for BatchDestroyStack {
}
impl Resolve<ExecuteArgs> for DestroyStack {
#[instrument(name = "DestroyStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"DestroyStack",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
services = format!("{:?}", self.services),
remove_orphans = self.remove_orphans,
stop_time = self.stop_time,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<DestroyStack>(
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (stack, swarm_or_server) = setup_stack_execution(
&self.stack,
self.services,
user,
|state| state.destroying = true,
update.clone(),
(self.stop_time, self.remove_orphans),
PermissionLevel::Execute.into(),
)
.await
.map_err(Into::into)
.await?;
swarm_or_server.verify_has_target()?;
match swarm_or_server {
SwarmOrServer::None => unreachable!(),
SwarmOrServer::Swarm(swarm) => {
if !self.services.is_empty() {
return Err(
anyhow!("Cannot destroy specific Stack services when in Swarm mode.")
.status_code(StatusCode::BAD_REQUEST)
);
}
// get the action state for the stack (or insert default).
let action_state = action_states()
.stack
.get_or_insert_default(&stack.id)
.await;
// Will check to ensure stack not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let _action_guard =
action_state.update(|state| state.destroying = true)?;
let mut update = update.clone();
// Send update here for frontend to recheck action state
update_update(update.clone()).await?;
match swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::RemoveSwarmStacks {
stacks: vec![stack.project_name(false)],
detach: false,
},
)
.await
{
Ok(log) => update.logs.push(log),
Err(e) => update.push_error_log(
"Destroy Stack",
format_serror(
&e.context("Failed to 'docker stack rm' on swarm")
.into(),
),
),
}
// Ensure cached stack state up to date by updating swarm cache
update_cache_for_swarm(&swarm, true).await;
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
SwarmOrServer::Server(server) => {
execute_compose_with_stack_and_server::<DestroyStack>(
stack,
server,
self.services,
|state| state.destroying = true,
update.clone(),
(self.stop_time, self.remove_orphans),
)
.await
.map_err(Into::into)
}
}
}
}
impl Resolve<ExecuteArgs> for RunStackService {
#[instrument(name = "RunStackService", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"RunStackService",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
service = self.service,
request = format!("{self:?}"),
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (mut stack, server) = get_stack_and_server(
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (mut stack, swarm_or_server) = setup_stack_execution(
&self.stack,
user,
PermissionLevel::Execute.into(),
true,
)
.await?;
let SwarmOrServer::Server(server) = swarm_or_server else {
return Err(
anyhow!(
"RunStackService should not be called for Stack in Swarm Mode"
)
.status_code(StatusCode::BAD_REQUEST),
);
};
let mut repo = if !stack.config.files_on_host
&& !stack.config.linked_repo.is_empty()
{
@@ -1022,7 +1310,8 @@ impl Resolve<ExecuteArgs> for RunStackService {
Default::default()
};
let log = periphery_client(&server)?
let log = periphery_client(&server)
.await?
.request(ComposeRun {
stack,
repo,

View File

@@ -0,0 +1,516 @@
use formatting::format_serror;
use komodo_client::{
api::execute::{
CreateSwarmConfig, CreateSwarmSecret, RemoveSwarmConfigs,
RemoveSwarmNodes, RemoveSwarmSecrets, RemoveSwarmServices,
RemoveSwarmStacks, RotateSwarmConfig, RotateSwarmSecret,
},
entities::{permission::PermissionLevel, swarm::Swarm},
};
use mogh_resolver::Resolve;
use crate::{
api::execute::ExecuteArgs,
helpers::{swarm::swarm_request, update::update_update},
monitor::update_cache_for_swarm,
permission::get_check_permissions,
};
impl Resolve<ExecuteArgs> for RemoveSwarmNodes {
#[instrument(
"RemoveSwarmNodes",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
nodes = serde_json::to_string(&self.nodes).unwrap_or_else(|e| e.to_string()),
force = self.force,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Execute.into(),
)
.await?;
update_update(update.clone()).await?;
let mut update = update.clone();
match swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::RemoveSwarmNodes {
nodes: self.nodes,
force: self.force,
},
)
.await
{
Ok(log) => {
update.logs.push(log);
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Remove Swarm Nodes",
format_serror(
&e.context("Failed to remove swarm nodes").into(),
),
),
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for RemoveSwarmStacks {
#[instrument(
"RemoveSwarmStacks",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
stacks = serde_json::to_string(&self.stacks).unwrap_or_else(|e| e.to_string()),
detach = self.detach,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Execute.into(),
)
.await?;
update_update(update.clone()).await?;
let mut update = update.clone();
match swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::RemoveSwarmStacks {
stacks: self.stacks,
detach: self.detach,
},
)
.await
{
Ok(log) => {
update.logs.push(log);
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Remove Swarm Stacks",
format_serror(
&e.context("Failed to remove swarm stacks").into(),
),
),
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for RemoveSwarmServices {
#[instrument(
"RemoveSwarmServices",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
services = serde_json::to_string(&self.services).unwrap_or_else(|e| e.to_string()),
)
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Execute.into(),
)
.await?;
update_update(update.clone()).await?;
let mut update = update.clone();
match swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::RemoveSwarmServices {
services: self.services,
},
)
.await
{
Ok(log) => {
update.logs.push(log);
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Remove Swarm Services",
format_serror(
&e.context("Failed to remove swarm services").into(),
),
),
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for CreateSwarmConfig {
#[instrument(
"CreateSwarmConfig",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
config = self.name,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Execute.into(),
)
.await?;
update_update(update.clone()).await?;
let mut update = update.clone();
match swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::CreateSwarmConfig {
name: self.name,
data: self.data,
labels: self.labels,
template_driver: self.template_driver,
},
)
.await
{
Ok(log) => {
update.logs.push(log);
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Create Swarm Config",
format_serror(
&e.context("Failed to create swarm config").into(),
),
),
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for RotateSwarmConfig {
#[instrument(
"RotateSwarmConfig",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
config = self.config,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Execute.into(),
)
.await?;
update_update(update.clone()).await?;
let mut update = update.clone();
match swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::RotateSwarmConfig {
config: self.config,
data: self.data,
},
)
.await
{
Ok(logs) => {
update.logs.extend(logs);
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Rotate Swarm Config",
format_serror(
&e.context("Failed to rotate swarm config").into(),
),
),
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for RemoveSwarmConfigs {
#[instrument(
"RemoveSwarmConfigs",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
configs = serde_json::to_string(&self.configs).unwrap_or_else(|e| e.to_string()),
)
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Execute.into(),
)
.await?;
update_update(update.clone()).await?;
let mut update = update.clone();
match swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::RemoveSwarmConfigs {
configs: self.configs,
},
)
.await
{
Ok(log) => {
update.logs.push(log);
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Remove Swarm Configs",
format_serror(
&e.context("Failed to remove swarm configs").into(),
),
),
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for CreateSwarmSecret {
#[instrument(
"CreateSwarmSecret",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
secret = self.name,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Execute.into(),
)
.await?;
update_update(update.clone()).await?;
let mut update = update.clone();
match swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::CreateSwarmSecret {
name: self.name,
data: self.data,
driver: self.driver,
labels: self.labels,
template_driver: self.template_driver,
},
)
.await
{
Ok(log) => {
update.logs.push(log);
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Create Swarm Secret",
format_serror(
&e.context("Failed to create swarm secret").into(),
),
),
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for RotateSwarmSecret {
#[instrument(
"RotateSwarmSecret",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
secret = self.secret,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Execute.into(),
)
.await?;
update_update(update.clone()).await?;
let mut update = update.clone();
match swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::RotateSwarmSecret {
secret: self.secret,
data: self.data,
},
)
.await
{
Ok(logs) => {
update.logs.extend(logs);
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Rotate Swarm Secret",
format_serror(
&e.context("Failed to rotate swarm secret").into(),
),
),
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for RemoveSwarmSecrets {
#[instrument(
"RemoveSwarmSecrets",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
secrets = serde_json::to_string(&self.secrets).unwrap_or_else(|e| e.to_string()),
)
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Execute.into(),
)
.await?;
update_update(update.clone()).await?;
let mut update = update.clone();
match swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::RemoveSwarmSecrets {
secrets: self.secrets,
},
)
.await
{
Ok(log) => {
update.logs.push(log);
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Remove Swarm Secrets",
format_serror(
&e.context("Failed to remove swarm secrets").into(),
),
),
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}

View File

@@ -26,7 +26,7 @@ use komodo_client::{
user::sync_user,
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{
api::write::WriteArgs,
@@ -49,11 +49,22 @@ use crate::{
use super::ExecuteArgs;
impl Resolve<ExecuteArgs> for RunSync {
#[instrument(name = "RunSync", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
"RunSync",
skip_all,
fields(
id = id.to_string(),
operator = user.id,
update_id = update.id,
sync = self.sync,
resource_type = format!("{:?}", self.resource_type),
resources = format!("{:?}", self.resources),
)
)]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let RunSync {
sync,
resource_type: match_resource_type,
@@ -125,34 +136,10 @@ impl Resolve<ExecuteArgs> for RunSync {
};
match ObjectId::from_str(&name_or_id) {
Ok(_) => match resource_type {
ResourceTargetVariant::Alerter => all_resources
.alerters
ResourceTargetVariant::Swarm => all_resources
.swarms
.get(&name_or_id)
.map(|a| a.name.clone()),
ResourceTargetVariant::Build => all_resources
.builds
.get(&name_or_id)
.map(|b| b.name.clone()),
ResourceTargetVariant::Builder => all_resources
.builders
.get(&name_or_id)
.map(|b| b.name.clone()),
ResourceTargetVariant::Deployment => all_resources
.deployments
.get(&name_or_id)
.map(|d| d.name.clone()),
ResourceTargetVariant::Procedure => all_resources
.procedures
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::Action => all_resources
.actions
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::Repo => all_resources
.repos
.get(&name_or_id)
.map(|r| r.name.clone()),
.map(|s| s.name.clone()),
ResourceTargetVariant::Server => all_resources
.servers
.get(&name_or_id)
@@ -161,10 +148,38 @@ impl Resolve<ExecuteArgs> for RunSync {
.stacks
.get(&name_or_id)
.map(|s| s.name.clone()),
ResourceTargetVariant::Deployment => all_resources
.deployments
.get(&name_or_id)
.map(|d| d.name.clone()),
ResourceTargetVariant::Build => all_resources
.builds
.get(&name_or_id)
.map(|b| b.name.clone()),
ResourceTargetVariant::Repo => all_resources
.repos
.get(&name_or_id)
.map(|r| r.name.clone()),
ResourceTargetVariant::Procedure => all_resources
.procedures
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::Action => all_resources
.actions
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::ResourceSync => all_resources
.syncs
.get(&name_or_id)
.map(|s| s.name.clone()),
ResourceTargetVariant::Builder => all_resources
.builders
.get(&name_or_id)
.map(|b| b.name.clone()),
ResourceTargetVariant::Alerter => all_resources
.alerters
.get(&name_or_id)
.map(|a| a.name.clone()),
ResourceTargetVariant::System => None,
},
Err(_) => Some(name_or_id),

View File

@@ -5,10 +5,9 @@ use hmac::{Hmac, Mac};
use serde::Deserialize;
use sha2::Sha256;
use crate::{
config::core_config,
listener::{ExtractBranch, VerifySecret},
};
use crate::config::core_config;
use super::{ExtractBranch, VerifySecret};
type HmacSha256 = Hmac<Sha256>;
@@ -18,7 +17,7 @@ pub struct Github;
impl VerifySecret for Github {
#[instrument("VerifyGithubSecret", skip_all)]
fn verify_secret(
headers: HeaderMap,
headers: &HeaderMap,
body: &str,
custom_secret: &str,
) -> anyhow::Result<()> {

View File

@@ -1,10 +1,10 @@
use anyhow::{Context, anyhow};
use axum::http::HeaderMap;
use serde::Deserialize;
use crate::{
config::core_config,
listener::{ExtractBranch, VerifySecret},
};
use crate::config::core_config;
use super::{ExtractBranch, VerifySecret};
/// Listener implementation for Gitlab type API
pub struct Gitlab;
@@ -12,7 +12,7 @@ pub struct Gitlab;
impl VerifySecret for Gitlab {
#[instrument("VerifyGitlabSecret", skip_all)]
fn verify_secret(
headers: axum::http::HeaderMap,
headers: &HeaderMap,
_body: &str,
custom_secret: &str,
) -> anyhow::Result<()> {

View File

@@ -0,0 +1,4 @@
pub mod github;
pub mod gitlab;
use super::{ExtractBranch, VerifySecret};

View File

@@ -3,9 +3,10 @@ use std::sync::Arc;
use anyhow::anyhow;
use axum::{Router, http::HeaderMap};
use komodo_client::entities::resource::Resource;
use mogh_cache::CloneCache;
use tokio::sync::Mutex;
use crate::{helpers::cache::Cache, resource::KomodoResource};
use crate::resource::KomodoResource;
mod integrations;
mod resources;
@@ -19,7 +20,7 @@ pub fn router() -> Router {
.nest("/gitlab", router::router::<gitlab::Gitlab>())
}
type ListenerLockCache = Cache<String, Arc<Mutex<()>>>;
type ListenerLockCache = CloneCache<String, Arc<Mutex<()>>>;
/// Implemented for all resources which can recieve webhook.
trait CustomSecret: KomodoResource {
@@ -31,7 +32,7 @@ trait CustomSecret: KomodoResource {
/// Implemented on the integration struct, eg [integrations::github::Github]
trait VerifySecret {
fn verify_secret(
headers: HeaderMap,
headers: &HeaderMap,
body: &str,
custom_secret: &str,
) -> anyhow::Result<()>;

View File

@@ -11,9 +11,10 @@ use komodo_client::{
stack::Stack, sync::ResourceSync, user::git_webhook_user,
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use serde::Deserialize;
use serde_json::json;
use uuid::Uuid;
use crate::{
api::{
@@ -21,6 +22,7 @@ use crate::{
write::WriteArgs,
},
helpers::update::init_execution_update,
resource,
};
use super::{ANY_BRANCH, ListenerLockCache};
@@ -54,7 +56,18 @@ pub async fn handle_build_webhook<B: super::ExtractBranch>(
let lock = build_locks().get_or_insert_default(&build.id).await;
let _lock = lock.lock().await;
B::verify_branch(&body, &build.config.branch)?;
// Use the correct target branch when using linked repo.
let branch = if build.config.linked_repo.is_empty() {
build.config.branch
} else {
resource::get::<Repo>(&build.config.linked_repo)
.await
.context("Failed to find 'linked_repo'")?
.config
.branch
};
B::verify_branch(&body, &branch)?;
let user = git_webhook_user().to_owned();
let req = ExecuteRequest::RunBuild(RunBuild { build: build.id });
@@ -63,7 +76,11 @@ pub async fn handle_build_webhook<B: super::ExtractBranch>(
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
Ok(())
@@ -101,7 +118,11 @@ impl RepoExecution for CloneRepo {
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
Ok(())
@@ -121,7 +142,11 @@ impl RepoExecution for PullRepo {
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
Ok(())
@@ -141,7 +166,11 @@ impl RepoExecution for BuildRepo {
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
Ok(())
@@ -212,11 +241,11 @@ fn stack_locks() -> &'static ListenerLockCache {
}
pub trait StackExecution {
async fn resolve(stack: Stack) -> serror::Result<()>;
async fn resolve(stack: Stack) -> mogh_error::Result<()>;
}
impl StackExecution for RefreshStackCache {
async fn resolve(stack: Stack) -> serror::Result<()> {
async fn resolve(stack: Stack) -> mogh_error::Result<()> {
RefreshStackCache { stack: stack.id }
.resolve(&WriteArgs {
user: git_webhook_user().to_owned(),
@@ -227,7 +256,7 @@ impl StackExecution for RefreshStackCache {
}
impl StackExecution for DeployStack {
async fn resolve(stack: Stack) -> serror::Result<()> {
async fn resolve(stack: Stack) -> mogh_error::Result<()> {
let user = git_webhook_user().to_owned();
if stack.config.webhook_force_deploy {
let req = ExecuteRequest::DeployStack(DeployStack {
@@ -240,7 +269,11 @@ impl StackExecution for DeployStack {
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
} else {
@@ -254,7 +287,11 @@ impl StackExecution for DeployStack {
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
}
@@ -303,7 +340,18 @@ pub async fn handle_stack_webhook_inner<
let lock = stack_locks().get_or_insert_default(&stack.id).await;
let _lock = lock.lock().await;
B::verify_branch(&body, &stack.config.branch)?;
// Use the correct target branch when using linked repo.
let branch = if stack.config.linked_repo.is_empty() {
stack.config.branch.clone()
} else {
resource::get::<Repo>(&stack.config.linked_repo)
.await
.context("Failed to find 'linked_repo'")?
.config
.branch
};
B::verify_branch(&body, &branch)?;
E::resolve(stack).await.map_err(|e| e.error)
}
@@ -352,7 +400,11 @@ impl SyncExecution for RunSync {
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
Ok(())
@@ -401,7 +453,18 @@ async fn handle_sync_webhook_inner<
let lock = sync_locks().get_or_insert_default(&sync.id).await;
let _lock = lock.lock().await;
B::verify_branch(&body, &sync.config.branch)?;
// Use the correct target branch when using linked repo.
let branch = if sync.config.linked_repo.is_empty() {
sync.config.branch.clone()
} else {
resource::get::<Repo>(&sync.config.linked_repo)
.await
.context("Failed to find 'linked_repo'")?
.config
.branch
};
B::verify_branch(&body, &branch)?;
E::resolve(sync).await
}
@@ -451,7 +514,11 @@ pub async fn handle_procedure_webhook<B: super::ExtractBranch>(
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
Ok(())
@@ -513,7 +580,11 @@ pub async fn handle_action_webhook<B: super::ExtractBranch>(
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
Ok(())

View File

@@ -1,14 +1,18 @@
use std::net::IpAddr;
use axum::{Router, extract::Path, http::HeaderMap, routing::post};
use komodo_client::entities::{
action::Action, build::Build, procedure::Procedure, repo::Repo,
resource::Resource, stack::Stack, sync::ResourceSync,
};
use mogh_auth_server::request_ip::RequestIp;
use mogh_error::AddStatusCode;
use mogh_rate_limit::WithFailureRateLimit;
use reqwest::StatusCode;
use serde::Deserialize;
use serror::AddStatusCode;
use tracing::Instrument;
use crate::resource::KomodoResource;
use crate::{auth::GENERAL_RATE_LIMITER, resource::KomodoResource};
use super::{
CustomSecret, ExtractBranch, VerifySecret,
@@ -47,9 +51,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
.route(
"/build/{id}",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|Path(Id { id }), RequestIp(ip), headers: HeaderMap, body: String| async move {
let build =
auth_webhook::<P, Build>(&id, headers, &body).await?;
auth_webhook::<P, Build>(&id, &headers, ip, &body).await?;
tokio::spawn(async move {
let span = info_span!("BuildWebhook", id);
async {
@@ -66,16 +70,16 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
mogh_error::Result::Ok(())
},
),
)
.route(
"/repo/{id}/{option}",
post(
|Path(IdAndOption::<RepoWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
|Path(IdAndOption::<RepoWebhookOption> { id, option }), RequestIp(ip), headers: HeaderMap, body: String| async move {
let repo =
auth_webhook::<P, Repo>(&id, headers, &body).await?;
auth_webhook::<P, Repo>(&id, &headers, ip, &body).await?;
tokio::spawn(async move {
let span = info_span!("RepoWebhook", id);
async {
@@ -92,16 +96,16 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
mogh_error::Result::Ok(())
},
),
)
.route(
"/stack/{id}/{option}",
post(
|Path(IdAndOption::<StackWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
|Path(IdAndOption::<StackWebhookOption> { id, option }), RequestIp(ip), headers: HeaderMap, body: String| async move {
let stack =
auth_webhook::<P, Stack>(&id, headers, &body).await?;
auth_webhook::<P, Stack>(&id, &headers, ip, &body).await?;
tokio::spawn(async move {
let span = info_span!("StackWebhook", id);
async {
@@ -118,16 +122,16 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
mogh_error::Result::Ok(())
},
),
)
.route(
"/sync/{id}/{option}",
post(
|Path(IdAndOption::<SyncWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
|Path(IdAndOption::<SyncWebhookOption> { id, option }), RequestIp(ip), headers: HeaderMap, body: String| async move {
let sync =
auth_webhook::<P, ResourceSync>(&id, headers, &body).await?;
auth_webhook::<P, ResourceSync>(&id, &headers, ip, &body).await?;
tokio::spawn(async move {
let span = info_span!("ResourceSyncWebhook", id);
async {
@@ -144,16 +148,16 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
mogh_error::Result::Ok(())
},
),
)
.route(
"/procedure/{id}/{branch}",
post(
|Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {
|Path(IdAndBranch { id, branch }), RequestIp(ip), headers: HeaderMap, body: String| async move {
let procedure =
auth_webhook::<P, Procedure>(&id, headers, &body).await?;
auth_webhook::<P, Procedure>(&id, &headers, ip, &body).await?;
tokio::spawn(async move {
let span = info_span!("ProcedureWebhook", id);
async {
@@ -170,16 +174,16 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
mogh_error::Result::Ok(())
},
),
)
.route(
"/action/{id}/{branch}",
post(
|Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {
|Path(IdAndBranch { id, branch }), RequestIp(ip), headers: HeaderMap, body: String| async move {
let action =
auth_webhook::<P, Action>(&id, headers, &body).await?;
auth_webhook::<P, Action>(&id, &headers, ip, &body).await?;
tokio::spawn(async move {
let span = info_span!("ActionWebhook", id);
async {
@@ -196,7 +200,7 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
mogh_error::Result::Ok(())
},
),
)
@@ -204,17 +208,22 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
async fn auth_webhook<P, R>(
id: &str,
headers: HeaderMap,
headers: &HeaderMap,
ip: IpAddr,
body: &str,
) -> serror::Result<Resource<R::Config, R::Info>>
) -> mogh_error::Result<Resource<R::Config, R::Info>>
where
P: VerifySecret,
R: KomodoResource + CustomSecret,
{
let resource = crate::resource::get::<R>(id)
.await
.status_code(StatusCode::BAD_REQUEST)?;
P::verify_secret(headers, body, R::custom_secret(&resource))
.status_code(StatusCode::UNAUTHORIZED)?;
Ok(resource)
async {
let resource = crate::resource::get::<R>(id)
.await
.status_code(StatusCode::BAD_REQUEST)?;
P::verify_secret(headers, body, R::custom_secret(&resource))
.status_code(StatusCode::UNAUTHORIZED)?;
mogh_error::Result::Ok(resource)
}
.with_failure_rate_limit_using_ip(&GENERAL_RATE_LIMITER, &ip)
.await
}

View File

@@ -1,11 +1,57 @@
pub mod auth;
use axum::{Extension, Router, routing::get};
use komodo_client::entities::user::User;
use mogh_auth_server::middleware::authenticate_request;
use mogh_error::Json;
use mogh_server::{
cors::cors_layer, session::memory_session_layer,
ui::serve_static_ui,
};
use crate::{auth::KomodoAuthImpl, config::core_config, ts_client};
pub mod execute;
pub mod read;
pub mod terminal;
pub mod user;
pub mod write;
mod listener;
mod openapi;
mod terminal;
mod ws;
#[derive(serde::Deserialize)]
struct Variant {
variant: String,
}
pub fn app() -> Router {
let config = core_config();
Router::new()
.merge(openapi::serve_docs())
.route("/version", get(|| async { env!("CARGO_PKG_VERSION") }))
.nest("/auth", mogh_auth_server::api::router::<KomodoAuthImpl>())
.nest("/user", user_router())
.nest("/read", read::router())
.nest("/write", write::router())
.nest("/execute", execute::router())
.nest("/terminal", terminal::router())
.nest("/listener", listener::router())
.nest("/ws", ws::router())
.nest("/client", ts_client::router())
.layer(memory_session_layer(config))
.fallback_service(serve_static_ui(
&config.frontend_path,
config.ui_index_force_no_cache,
))
.layer(cors_layer(config))
}
fn user_router() -> Router {
Router::new()
.route(
"/",
get(|Extension(user): Extension<User>| async { Json(user) }),
)
.layer(axum::middleware::from_fn(
authenticate_request::<KomodoAuthImpl, false>,
))
}

View File

@@ -0,0 +1,18 @@
<!doctype html>
<html>
<head>
<title>Komodo API Docs</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<script id="api-reference" type="application/json">
$spec
</script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
use komodo_client::openapi::KomodoApi;
use utoipa::OpenApi as _;
use utoipa_scalar::{Scalar, Servable as _};
pub fn serve_docs() -> Scalar<utoipa::openapi::OpenApi> {
Scalar::with_url("/docs", KomodoApi::openapi())
.custom_html(include_str!("docs.html"))
}

View File

@@ -8,7 +8,7 @@ use komodo_client::{
permission::PermissionLevel,
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{
helpers::query::get_all_tags,
@@ -23,7 +23,7 @@ impl Resolve<ReadArgs> for GetAction {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Action> {
) -> mogh_error::Result<Action> {
Ok(
get_check_permissions::<Action>(
&self.action,
@@ -39,7 +39,7 @@ impl Resolve<ReadArgs> for ListActions {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<ActionListItem>> {
) -> mogh_error::Result<Vec<ActionListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -61,7 +61,7 @@ impl Resolve<ReadArgs> for ListFullActions {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullActionsResponse> {
) -> mogh_error::Result<ListFullActionsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -83,7 +83,7 @@ impl Resolve<ReadArgs> for GetActionActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ActionActionState> {
) -> mogh_error::Result<ActionActionState> {
let action = get_check_permissions::<Action>(
&self.action,
user,
@@ -104,7 +104,7 @@ impl Resolve<ReadArgs> for GetActionsSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetActionsSummaryResponse> {
) -> mogh_error::Result<GetActionsSummaryResponse> {
let actions = resource::list_full_for_user::<Action>(
Default::default(),
user,

View File

@@ -8,15 +8,15 @@ use komodo_client::{
api::read::{
GetAlert, GetAlertResponse, ListAlerts, ListAlertsResponse,
},
entities::{
deployment::Deployment, server::Server, stack::Stack,
sync::ResourceSync,
},
entities::permission::PermissionLevel,
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{
config::core_config, permission::get_resource_ids_for_user,
config::core_config,
permission::{
check_user_target_access, user_resource_target_query,
},
state::db_client,
};
@@ -28,26 +28,11 @@ impl Resolve<ReadArgs> for ListAlerts {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListAlertsResponse> {
let mut query = self.query.unwrap_or_default();
if !user.admin && !core_config().transparent_mode {
let server_ids =
get_resource_ids_for_user::<Server>(user).await?;
let stack_ids =
get_resource_ids_for_user::<Stack>(user).await?;
let deployment_ids =
get_resource_ids_for_user::<Deployment>(user).await?;
let sync_ids =
get_resource_ids_for_user::<ResourceSync>(user).await?;
query.extend(doc! {
"$or": [
{ "target.type": "Server", "target.id": { "$in": &server_ids } },
{ "target.type": "Stack", "target.id": { "$in": &stack_ids } },
{ "target.type": "Deployment", "target.id": { "$in": &deployment_ids } },
{ "target.type": "ResourceSync", "target.id": { "$in": &sync_ids } },
]
});
}
) -> mogh_error::Result<ListAlertsResponse> {
// Alerts
let query = user_resource_target_query(user, self.query)
.await?
.unwrap_or_default();
let alerts = find_collect(
&db_client().alerts,
@@ -76,13 +61,21 @@ impl Resolve<ReadArgs> for ListAlerts {
impl Resolve<ReadArgs> for GetAlert {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<GetAlertResponse> {
Ok(
find_one_by_id(&db_client().alerts, &self.id)
.await
.context("failed to query db for alert")?
.context("no alert found with given id")?,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetAlertResponse> {
let alert = find_one_by_id(&db_client().alerts, &self.id)
.await
.context("failed to query db for alert")?
.context("no alert found with given id")?;
if user.admin || core_config().transparent_mode {
return Ok(alert);
}
check_user_target_access(
&alert.target,
user,
PermissionLevel::Read.into(),
)
.await?;
Ok(alert)
}
}

View File

@@ -8,11 +8,13 @@ use komodo_client::{
permission::PermissionLevel,
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{
helpers::query::get_all_tags, permission::get_check_permissions,
resource, state::db_client,
helpers::query::get_all_tags,
permission::{get_check_permissions, list_resource_ids_for_user},
resource,
state::db_client,
};
use super::ReadArgs;
@@ -21,7 +23,7 @@ impl Resolve<ReadArgs> for GetAlerter {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Alerter> {
) -> mogh_error::Result<Alerter> {
Ok(
get_check_permissions::<Alerter>(
&self.alerter,
@@ -37,7 +39,7 @@ impl Resolve<ReadArgs> for ListAlerters {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<AlerterListItem>> {
) -> mogh_error::Result<Vec<AlerterListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -59,7 +61,7 @@ impl Resolve<ReadArgs> for ListFullAlerters {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullAlertersResponse> {
) -> mogh_error::Result<ListFullAlertersResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -81,10 +83,12 @@ impl Resolve<ReadArgs> for GetAlertersSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetAlertersSummaryResponse> {
let query = match resource::get_resource_object_ids_for_user::<
Alerter,
>(user)
) -> mogh_error::Result<GetAlertersSummaryResponse> {
let query = match list_resource_ids_for_user::<Alerter>(
None,
user,
PermissionLevel::Read.into(),
)
.await?
{
Some(ids) => doc! {

View File

@@ -6,27 +6,23 @@ use database::mungos::{
find::find_collect,
mongodb::{bson::doc, options::FindOptions},
};
use futures::TryStreamExt;
use futures_util::TryStreamExt;
use komodo_client::{
api::read::*,
entities::{
Operation,
build::{Build, BuildActionState, BuildListItem, BuildState},
config::core::CoreConfig,
permission::PermissionLevel,
update::UpdateStatus,
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{
action_states, build_state_cache, db_client, github_client,
},
state::{action_states, build_state_cache, db_client},
};
use super::ReadArgs;
@@ -35,7 +31,7 @@ impl Resolve<ReadArgs> for GetBuild {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Build> {
) -> mogh_error::Result<Build> {
Ok(
get_check_permissions::<Build>(
&self.build,
@@ -51,7 +47,7 @@ impl Resolve<ReadArgs> for ListBuilds {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<BuildListItem>> {
) -> mogh_error::Result<Vec<BuildListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -73,7 +69,7 @@ impl Resolve<ReadArgs> for ListFullBuilds {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullBuildsResponse> {
) -> mogh_error::Result<ListFullBuildsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -95,7 +91,7 @@ impl Resolve<ReadArgs> for GetBuildActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<BuildActionState> {
) -> mogh_error::Result<BuildActionState> {
let build = get_check_permissions::<Build>(
&self.build,
user,
@@ -116,7 +112,7 @@ impl Resolve<ReadArgs> for GetBuildsSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetBuildsSummaryResponse> {
) -> mogh_error::Result<GetBuildsSummaryResponse> {
let builds = resource::list_full_for_user::<Build>(
Default::default(),
user,
@@ -164,7 +160,7 @@ impl Resolve<ReadArgs> for GetBuildMonthlyStats {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<GetBuildMonthlyStatsResponse> {
) -> mogh_error::Result<GetBuildMonthlyStatsResponse> {
let curr_ts = unix_timestamp_ms() as i64;
let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS;
@@ -220,7 +216,7 @@ impl Resolve<ReadArgs> for ListBuildVersions {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<BuildVersionResponseItem>> {
) -> mogh_error::Result<Vec<BuildVersionResponseItem>> {
let ListBuildVersions {
build,
major,
@@ -277,7 +273,7 @@ impl Resolve<ReadArgs> for ListCommonBuildExtraArgs {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListCommonBuildExtraArgsResponse> {
) -> mogh_error::Result<ListCommonBuildExtraArgsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -306,81 +302,3 @@ impl Resolve<ReadArgs> for ListCommonBuildExtraArgs {
Ok(res)
}
}
impl Resolve<ReadArgs> for GetBuildWebhookEnabled {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetBuildWebhookEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetBuildWebhookEnabledResponse {
managed: false,
enabled: false,
});
};
let build = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Read.into(),
)
.await?;
if build.config.git_provider != "github.com"
|| build.config.repo.is_empty()
{
return Ok(GetBuildWebhookEnabledResponse {
managed: false,
enabled: false,
});
}
let mut split = build.config.repo.split('/');
let owner = split.next().context("Build repo has no owner")?;
let Some(github) = github.get(owner) else {
return Ok(GetBuildWebhookEnabledResponse {
managed: false,
enabled: false,
});
};
let repo =
split.next().context("Build repo has no repo after the /")?;
let github_repos = github.repos();
let webhooks = github_repos
.list_all_webhooks(owner, repo)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = format!("{host}/listener/github/build/{}", build.id);
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
return Ok(GetBuildWebhookEnabledResponse {
managed: true,
enabled: true,
});
}
}
Ok(GetBuildWebhookEnabledResponse {
managed: true,
enabled: false,
})
}
}

View File

@@ -8,11 +8,13 @@ use komodo_client::{
permission::PermissionLevel,
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{
helpers::query::get_all_tags, permission::get_check_permissions,
resource, state::db_client,
helpers::query::get_all_tags,
permission::{get_check_permissions, list_resource_ids_for_user},
resource,
state::db_client,
};
use super::ReadArgs;
@@ -21,7 +23,7 @@ impl Resolve<ReadArgs> for GetBuilder {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Builder> {
) -> mogh_error::Result<Builder> {
Ok(
get_check_permissions::<Builder>(
&self.builder,
@@ -37,7 +39,7 @@ impl Resolve<ReadArgs> for ListBuilders {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<BuilderListItem>> {
) -> mogh_error::Result<Vec<BuilderListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -59,7 +61,7 @@ impl Resolve<ReadArgs> for ListFullBuilders {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullBuildersResponse> {
) -> mogh_error::Result<ListFullBuildersResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -81,10 +83,12 @@ impl Resolve<ReadArgs> for GetBuildersSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetBuildersSummaryResponse> {
let query = match resource::get_resource_object_ids_for_user::<
Builder,
>(user)
) -> mogh_error::Result<GetBuildersSummaryResponse> {
let query = match list_resource_ids_for_user::<Builder>(
None,
user,
PermissionLevel::Read.into(),
)
.await?
{
Some(ids) => doc! {

View File

@@ -4,23 +4,31 @@ use anyhow::{Context, anyhow};
use komodo_client::{
api::read::*,
entities::{
SwarmOrServer,
deployment::{
Deployment, DeploymentActionState, DeploymentConfig,
DeploymentListItem, DeploymentState,
},
docker::container::{Container, ContainerStats},
docker::{
container::{Container, ContainerStats},
service::SwarmService,
},
permission::PermissionLevel,
server::{Server, ServerState},
update::Log,
},
};
use mogh_error::AddStatusCodeError as _;
use mogh_resolver::Resolve;
use periphery_client::api::{self, container::InspectContainer};
use resolver_api::Resolve;
use reqwest::StatusCode;
use crate::{
helpers::{periphery_client, query::get_all_tags},
helpers::{
periphery_client, query::get_all_tags, swarm::swarm_request,
},
permission::get_check_permissions,
resource,
resource::{self, setup_deployment_execution},
state::{
action_states, deployment_status_cache, server_status_cache,
},
@@ -32,7 +40,7 @@ impl Resolve<ReadArgs> for GetDeployment {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Deployment> {
) -> mogh_error::Result<Deployment> {
Ok(
get_check_permissions::<Deployment>(
&self.deployment,
@@ -48,7 +56,7 @@ impl Resolve<ReadArgs> for ListDeployments {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<DeploymentListItem>> {
) -> mogh_error::Result<Vec<DeploymentListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -78,7 +86,7 @@ impl Resolve<ReadArgs> for ListFullDeployments {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullDeploymentsResponse> {
) -> mogh_error::Result<ListFullDeploymentsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -100,7 +108,7 @@ impl Resolve<ReadArgs> for GetDeploymentContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDeploymentContainerResponse> {
) -> mogh_error::Result<GetDeploymentContainerResponse> {
let deployment = get_check_permissions::<Deployment>(
&self.deployment,
user,
@@ -125,35 +133,49 @@ impl Resolve<ReadArgs> for GetDeploymentLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
) -> mogh_error::Result<Log> {
let GetDeploymentLog {
deployment,
tail,
timestamps,
} = self;
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = get_check_permissions::<Deployment>(
let (deployment, swarm_or_server) = setup_deployment_execution(
&deployment,
user,
PermissionLevel::Read.logs(),
)
.await?;
if server_id.is_empty() {
return Ok(Log::default());
}
let server = resource::get::<Server>(&server_id).await?;
let res = periphery_client(&server)?
.request(api::container::GetContainerLog {
name,
tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps,
})
swarm_or_server.verify_has_target()?;
let log = match swarm_or_server {
SwarmOrServer::None => unreachable!(),
SwarmOrServer::Swarm(swarm) => swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::GetSwarmServiceLog {
service: deployment.name,
tail,
timestamps,
no_task_ids: false,
no_resolve: false,
details: false,
},
)
.await
.context("failed at call to periphery")?;
Ok(res)
.context("Failed to get service log from swarm")?,
SwarmOrServer::Server(server) => periphery_client(&server)
.await?
.request(api::container::GetContainerLog {
name: deployment.name,
tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps,
})
.await
.context("failed at call to periphery")?,
};
Ok(log)
}
}
@@ -161,7 +183,7 @@ impl Resolve<ReadArgs> for SearchDeploymentLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
) -> mogh_error::Result<Log> {
let SearchDeploymentLog {
deployment,
terms,
@@ -169,31 +191,47 @@ impl Resolve<ReadArgs> for SearchDeploymentLog {
invert,
timestamps,
} = self;
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = get_check_permissions::<Deployment>(
let (deployment, swarm_or_server) = setup_deployment_execution(
&deployment,
user,
PermissionLevel::Read.logs(),
)
.await?;
if server_id.is_empty() {
return Ok(Log::default());
}
let server = resource::get::<Server>(&server_id).await?;
let res = periphery_client(&server)?
.request(api::container::GetContainerLogSearch {
name,
terms,
combinator,
invert,
timestamps,
})
swarm_or_server.verify_has_target()?;
let log = match swarm_or_server {
SwarmOrServer::None => unreachable!(),
SwarmOrServer::Swarm(swarm) => swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::GetSwarmServiceLogSearch {
service: deployment.name,
terms,
combinator,
invert,
timestamps,
no_task_ids: false,
no_resolve: false,
details: false,
},
)
.await
.context("failed at call to periphery")?;
Ok(res)
.context("Failed to search service log from swarm")?,
SwarmOrServer::Server(server) => periphery_client(&server)
.await?
.request(api::container::GetContainerLogSearch {
name: deployment.name,
terms,
combinator,
invert,
timestamps,
})
.await
.context("Failed to search container log from server")?,
};
Ok(log)
}
}
@@ -201,43 +239,80 @@ impl Resolve<ReadArgs> for InspectDeploymentContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Container> {
) -> mogh_error::Result<Container> {
let InspectDeploymentContainer { deployment } = self;
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = get_check_permissions::<Deployment>(
let (deployment, swarm_or_server) = setup_deployment_execution(
&deployment,
user,
PermissionLevel::Read.inspect(),
)
.await?;
if server_id.is_empty() {
let SwarmOrServer::Server(server) = swarm_or_server else {
return Err(
anyhow!(
"Cannot inspect deployment, not attached to any server"
"InspectDeploymentContainer should not be called for Deployment in Swarm Mode"
)
.into(),
.status_code(StatusCode::BAD_REQUEST),
);
}
let server = resource::get::<Server>(&server_id).await?;
};
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
"Cannot inspect container: Server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)?
.request(InspectContainer { name })
.await?;
Ok(res)
periphery_client(&server)
.await?
.request(InspectContainer {
name: deployment.name,
})
.await
.context("Failed to inspect container on server")
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for InspectDeploymentSwarmService {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<SwarmService> {
let InspectDeploymentSwarmService { deployment } = self;
let (deployment, swarm_or_server) = setup_deployment_execution(
&deployment,
user,
PermissionLevel::Read.logs(),
)
.await?;
let SwarmOrServer::Swarm(swarm) = swarm_or_server else {
return Err(
anyhow!(
"InspectDeploymentSwarmService should only be called for Deployment in Swarm Mode"
)
.status_code(StatusCode::BAD_REQUEST),
);
};
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmService {
service: deployment.name,
},
)
.await
.context("Failed to inspect service on swarm")
.map_err(Into::into)
}
}
@@ -245,7 +320,7 @@ impl Resolve<ReadArgs> for GetDeploymentStats {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ContainerStats> {
) -> mogh_error::Result<ContainerStats> {
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
@@ -262,7 +337,8 @@ impl Resolve<ReadArgs> for GetDeploymentStats {
);
}
let server = resource::get::<Server>(&server_id).await?;
let res = periphery_client(&server)?
let res = periphery_client(&server)
.await?
.request(api::container::GetContainerStats { name })
.await
.context("failed to get stats from periphery")?;
@@ -274,7 +350,7 @@ impl Resolve<ReadArgs> for GetDeploymentActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<DeploymentActionState> {
) -> mogh_error::Result<DeploymentActionState> {
let deployment = get_check_permissions::<Deployment>(
&self.deployment,
user,
@@ -295,7 +371,7 @@ impl Resolve<ReadArgs> for GetDeploymentsSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDeploymentsSummaryResponse> {
) -> mogh_error::Result<GetDeploymentsSummaryResponse> {
let deployments = resource::list_full_for_user::<Deployment>(
Default::default(),
user,
@@ -321,7 +397,9 @@ impl Resolve<ReadArgs> for GetDeploymentsSummary {
res.not_deployed += 1;
}
DeploymentState::Unknown => {
res.unknown += 1;
if !deployment.template {
res.unknown += 1;
}
}
_ => {
res.unhealthy += 1;
@@ -336,7 +414,7 @@ impl Resolve<ReadArgs> for ListCommonDeploymentExtraArgs {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListCommonDeploymentExtraArgsResponse> {
) -> mogh_error::Result<ListCommonDeploymentExtraArgsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {

View File

@@ -1,4 +1,4 @@
use std::{collections::HashSet, sync::OnceLock, time::Instant};
use std::collections::HashSet;
use anyhow::{Context, anyhow};
use axum::{
@@ -18,16 +18,21 @@ use komodo_client::{
user::User,
},
};
use resolver_api::Resolve;
use response::Response;
use mogh_auth_server::middleware::authenticate_request;
use mogh_error::Response;
use mogh_error::{AddStatusCodeError, Json};
use mogh_resolver::Resolve;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serror::Json;
use strum::{Display, EnumDiscriminants};
use typeshare::typeshare;
use uuid::Uuid;
use crate::{
auth::auth_request, config::core_config, helpers::periphery_client,
auth::KomodoAuthImpl,
config::{core_config, core_keys},
helpers::periphery_client,
resource,
};
@@ -39,6 +44,7 @@ mod alerter;
mod build;
mod builder;
mod deployment;
mod onboarding_key;
mod permission;
mod procedure;
mod provider;
@@ -46,8 +52,10 @@ mod repo;
mod schedule;
mod server;
mod stack;
mod swarm;
mod sync;
mod tag;
mod terminal;
mod toml;
mod update;
mod user;
@@ -59,10 +67,13 @@ pub struct ReadArgs {
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants,
)]
#[strum_discriminants(name(ReadRequestVariant), derive(Display))]
#[args(ReadArgs)]
#[response(Response)]
#[error(serror::Error)]
#[error(mogh_error::Error)]
#[serde(tag = "type", content = "params")]
enum ReadRequest {
GetVersion(GetVersion),
@@ -71,19 +82,108 @@ enum ReadRequest {
ListGitProvidersFromConfig(ListGitProvidersFromConfig),
ListDockerRegistriesFromConfig(ListDockerRegistriesFromConfig),
// ==== USER ====
GetUsername(GetUsername),
GetPermission(GetPermission),
FindUser(FindUser),
ListUsers(ListUsers),
ListApiKeys(ListApiKeys),
ListApiKeysForServiceUser(ListApiKeysForServiceUser),
ListPermissions(ListPermissions),
ListUserTargetPermissions(ListUserTargetPermissions),
// ==== SWARM ====
GetSwarmsSummary(GetSwarmsSummary),
GetSwarm(GetSwarm),
GetSwarmActionState(GetSwarmActionState),
ListSwarms(ListSwarms),
InspectSwarm(InspectSwarm),
ListFullSwarms(ListFullSwarms),
ListSwarmNodes(ListSwarmNodes),
InspectSwarmNode(InspectSwarmNode),
ListSwarmConfigs(ListSwarmConfigs),
InspectSwarmConfig(InspectSwarmConfig),
ListSwarmSecrets(ListSwarmSecrets),
InspectSwarmSecret(InspectSwarmSecret),
ListSwarmStacks(ListSwarmStacks),
InspectSwarmStack(InspectSwarmStack),
ListSwarmTasks(ListSwarmTasks),
InspectSwarmTask(InspectSwarmTask),
ListSwarmServices(ListSwarmServices),
InspectSwarmService(InspectSwarmService),
GetSwarmServiceLog(GetSwarmServiceLog),
SearchSwarmServiceLog(SearchSwarmServiceLog),
ListSwarmNetworks(ListSwarmNetworks),
// ==== USER GROUP ====
GetUserGroup(GetUserGroup),
ListUserGroups(ListUserGroups),
// ==== SERVER ====
GetServersSummary(GetServersSummary),
GetServer(GetServer),
GetServerState(GetServerState),
GetPeripheryInformation(GetPeripheryInformation),
GetServerActionState(GetServerActionState),
ListServers(ListServers),
ListFullServers(ListFullServers),
// ==== TERMINAL ====
ListTerminals(ListTerminals),
// ==== DOCKER ====
GetDockerContainersSummary(GetDockerContainersSummary),
ListAllDockerContainers(ListAllDockerContainers),
ListDockerContainers(ListDockerContainers),
InspectDockerContainer(InspectDockerContainer),
GetResourceMatchingContainer(GetResourceMatchingContainer),
GetContainerLog(GetContainerLog),
SearchContainerLog(SearchContainerLog),
ListComposeProjects(ListComposeProjects),
ListDockerNetworks(ListDockerNetworks),
InspectDockerNetwork(InspectDockerNetwork),
ListDockerImages(ListDockerImages),
InspectDockerImage(InspectDockerImage),
ListDockerImageHistory(ListDockerImageHistory),
ListDockerVolumes(ListDockerVolumes),
InspectDockerVolume(InspectDockerVolume),
// ==== SERVER STATS ====
GetSystemInformation(GetSystemInformation),
GetSystemStats(GetSystemStats),
GetHistoricalServerStats(GetHistoricalServerStats),
ListSystemProcesses(ListSystemProcesses),
// ==== STACK ====
GetStacksSummary(GetStacksSummary),
GetStack(GetStack),
GetStackActionState(GetStackActionState),
GetStackLog(GetStackLog),
SearchStackLog(SearchStackLog),
InspectStackContainer(InspectStackContainer),
InspectStackSwarmService(InspectStackSwarmService),
ListStacks(ListStacks),
ListFullStacks(ListFullStacks),
ListStackServices(ListStackServices),
ListCommonStackExtraArgs(ListCommonStackExtraArgs),
ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs),
// ==== DEPLOYMENT ====
GetDeploymentsSummary(GetDeploymentsSummary),
GetDeployment(GetDeployment),
GetDeploymentContainer(GetDeploymentContainer),
GetDeploymentActionState(GetDeploymentActionState),
GetDeploymentStats(GetDeploymentStats),
GetDeploymentLog(GetDeploymentLog),
SearchDeploymentLog(SearchDeploymentLog),
InspectDeploymentContainer(InspectDeploymentContainer),
InspectDeploymentSwarmService(InspectDeploymentSwarmService),
ListDeployments(ListDeployments),
ListFullDeployments(ListFullDeployments),
ListCommonDeploymentExtraArgs(ListCommonDeploymentExtraArgs),
// ==== BUILD ====
GetBuildsSummary(GetBuildsSummary),
GetBuild(GetBuild),
GetBuildActionState(GetBuildActionState),
GetBuildMonthlyStats(GetBuildMonthlyStats),
ListBuildVersions(ListBuildVersions),
ListBuilds(ListBuilds),
ListFullBuilds(ListFullBuilds),
ListCommonBuildExtraArgs(ListCommonBuildExtraArgs),
// ==== REPO ====
GetReposSummary(GetReposSummary),
GetRepo(GetRepo),
GetRepoActionState(GetRepoActionState),
ListRepos(ListRepos),
ListFullRepos(ListFullRepos),
// ==== PROCEDURE ====
GetProceduresSummary(GetProceduresSummary),
@@ -102,88 +202,10 @@ enum ReadRequest {
// ==== SCHEDULE ====
ListSchedules(ListSchedules),
// ==== SERVER ====
GetServersSummary(GetServersSummary),
GetServer(GetServer),
GetServerState(GetServerState),
GetPeripheryVersion(GetPeripheryVersion),
GetServerActionState(GetServerActionState),
GetHistoricalServerStats(GetHistoricalServerStats),
ListServers(ListServers),
ListFullServers(ListFullServers),
InspectDockerContainer(InspectDockerContainer),
GetResourceMatchingContainer(GetResourceMatchingContainer),
GetContainerLog(GetContainerLog),
SearchContainerLog(SearchContainerLog),
InspectDockerNetwork(InspectDockerNetwork),
InspectDockerImage(InspectDockerImage),
ListDockerImageHistory(ListDockerImageHistory),
InspectDockerVolume(InspectDockerVolume),
GetDockerContainersSummary(GetDockerContainersSummary),
ListAllDockerContainers(ListAllDockerContainers),
ListDockerContainers(ListDockerContainers),
ListDockerNetworks(ListDockerNetworks),
ListDockerImages(ListDockerImages),
ListDockerVolumes(ListDockerVolumes),
ListComposeProjects(ListComposeProjects),
ListTerminals(ListTerminals),
// ==== SERVER STATS ====
GetSystemInformation(GetSystemInformation),
GetSystemStats(GetSystemStats),
ListSystemProcesses(ListSystemProcesses),
// ==== STACK ====
GetStacksSummary(GetStacksSummary),
GetStack(GetStack),
GetStackActionState(GetStackActionState),
GetStackWebhooksEnabled(GetStackWebhooksEnabled),
GetStackLog(GetStackLog),
SearchStackLog(SearchStackLog),
InspectStackContainer(InspectStackContainer),
ListStacks(ListStacks),
ListFullStacks(ListFullStacks),
ListStackServices(ListStackServices),
ListCommonStackExtraArgs(ListCommonStackExtraArgs),
ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs),
// ==== DEPLOYMENT ====
GetDeploymentsSummary(GetDeploymentsSummary),
GetDeployment(GetDeployment),
GetDeploymentContainer(GetDeploymentContainer),
GetDeploymentActionState(GetDeploymentActionState),
GetDeploymentStats(GetDeploymentStats),
GetDeploymentLog(GetDeploymentLog),
SearchDeploymentLog(SearchDeploymentLog),
InspectDeploymentContainer(InspectDeploymentContainer),
ListDeployments(ListDeployments),
ListFullDeployments(ListFullDeployments),
ListCommonDeploymentExtraArgs(ListCommonDeploymentExtraArgs),
// ==== BUILD ====
GetBuildsSummary(GetBuildsSummary),
GetBuild(GetBuild),
GetBuildActionState(GetBuildActionState),
GetBuildMonthlyStats(GetBuildMonthlyStats),
ListBuildVersions(ListBuildVersions),
GetBuildWebhookEnabled(GetBuildWebhookEnabled),
ListBuilds(ListBuilds),
ListFullBuilds(ListFullBuilds),
ListCommonBuildExtraArgs(ListCommonBuildExtraArgs),
// ==== REPO ====
GetReposSummary(GetReposSummary),
GetRepo(GetRepo),
GetRepoActionState(GetRepoActionState),
GetRepoWebhooksEnabled(GetRepoWebhooksEnabled),
ListRepos(ListRepos),
ListFullRepos(ListFullRepos),
// ==== SYNC ====
GetResourceSyncsSummary(GetResourceSyncsSummary),
GetResourceSync(GetResourceSync),
GetResourceSyncActionState(GetResourceSyncActionState),
GetSyncWebhooksEnabled(GetSyncWebhooksEnabled),
ListResourceSyncs(ListResourceSyncs),
ListFullResourceSyncs(ListFullResourceSyncs),
@@ -207,6 +229,20 @@ enum ReadRequest {
GetTag(GetTag),
ListTags(ListTags),
// ==== USER ====
GetUsername(GetUsername),
GetPermission(GetPermission),
FindUser(FindUser),
ListUsers(ListUsers),
ListApiKeys(ListApiKeys),
ListApiKeysForServiceUser(ListApiKeysForServiceUser),
ListPermissions(ListPermissions),
ListUserTargetPermissions(ListUserTargetPermissions),
// ==== USER GROUP ====
GetUserGroup(GetUserGroup),
ListUserGroups(ListUserGroups),
// ==== UPDATE ====
GetUpdate(GetUpdate),
ListUpdates(ListUpdates),
@@ -224,20 +260,25 @@ enum ReadRequest {
ListGitProviderAccounts(ListGitProviderAccounts),
GetDockerRegistryAccount(GetDockerRegistryAccount),
ListDockerRegistryAccounts(ListDockerRegistryAccounts),
// ==== ONBOARDING KEY ====
ListOnboardingKeys(ListOnboardingKeys),
}
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler))
.layer(middleware::from_fn(auth_request))
.layer(middleware::from_fn(
authenticate_request::<KomodoAuthImpl, true>,
))
}
async fn variant_handler(
user: Extension<User>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<axum::response::Response> {
) -> mogh_error::Result<axum::response::Response> {
let req: ReadRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
@@ -245,20 +286,27 @@ async fn variant_handler(
handler(user, Json(req)).await
}
#[instrument(name = "ReadHandler", level = "debug", skip(user), fields(user_id = user.id))]
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<ReadRequest>,
) -> serror::Result<axum::response::Response> {
let timer = Instant::now();
) -> mogh_error::Result<axum::response::Response> {
let req_id = Uuid::new_v4();
debug!("/read request | user: {}", user.username);
let variant: ReadRequestVariant = (&request).into();
debug!(
"READ REQUEST {req_id} | METHOD: {variant} | USER: {} ({})",
user.username, user.id
);
let res = request.resolve(&ReadArgs { user }).await;
if let Err(e) = &res {
debug!("/read request {req_id} error: {:#}", e.error);
debug!(
"READ REQUEST {req_id} | METHOD: {variant} | ERROR: {:#}",
e.error
);
}
let elapsed = timer.elapsed();
debug!("/read request {req_id} | resolve time: {elapsed:?}");
res.map(|res| res.0)
}
@@ -266,18 +314,22 @@ impl Resolve<ReadArgs> for GetVersion {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<GetVersionResponse> {
) -> mogh_error::Result<GetVersionResponse> {
Ok(GetVersionResponse {
version: env!("CARGO_PKG_VERSION").to_string(),
})
}
}
fn core_info() -> &'static GetCoreInfoResponse {
static CORE_INFO: OnceLock<GetCoreInfoResponse> = OnceLock::new();
CORE_INFO.get_or_init(|| {
//
impl Resolve<ReadArgs> for GetCoreInfo {
async fn resolve(
self,
_: &ReadArgs,
) -> mogh_error::Result<GetCoreInfoResponse> {
let config = core_config();
GetCoreInfoResponse {
let info = GetCoreInfoResponse {
title: config.title.clone(),
monitoring_interval: config.monitoring_interval,
webhook_base_url: if config.webhook_base_url.is_empty() {
@@ -291,31 +343,20 @@ fn core_info() -> &'static GetCoreInfoResponse {
disable_non_admin_create: config.disable_non_admin_create,
disable_websocket_reconnect: config.disable_websocket_reconnect,
enable_fancy_toml: config.enable_fancy_toml,
github_webhook_owners: config
.github_webhook_app
.installations
.iter()
.map(|i| i.namespace.to_string())
.collect(),
timezone: config.timezone.clone(),
}
})
}
impl Resolve<ReadArgs> for GetCoreInfo {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<GetCoreInfoResponse> {
Ok(core_info().clone())
public_key: core_keys().load().public.to_string(),
};
Ok(info)
}
}
//
impl Resolve<ReadArgs> for ListSecrets {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<ListSecretsResponse> {
) -> mogh_error::Result<ListSecretsResponse> {
let mut secrets = core_config()
.secrets
.keys()
@@ -337,13 +378,15 @@ impl Resolve<ReadArgs> for ListSecrets {
}
_ => {
return Err(
anyhow!("target must be `Server` or `Builder`").into(),
anyhow!("target must be `Server` or `Builder`")
.status_code(StatusCode::BAD_REQUEST),
);
}
};
if let Some(id) = server_id {
let server = resource::get::<Server>(&id).await?;
let more = periphery_client(&server)?
let more = periphery_client(&server)
.await?
.request(periphery_client::api::ListSecrets {})
.await
.with_context(|| {
@@ -363,11 +406,13 @@ impl Resolve<ReadArgs> for ListSecrets {
}
}
//
impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListGitProvidersFromConfigResponse> {
) -> mogh_error::Result<ListGitProvidersFromConfigResponse> {
let mut providers = core_config().git_providers.clone();
if let Some(target) = self.target {
@@ -395,7 +440,8 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
}
_ => {
return Err(
anyhow!("target must be `Server` or `Builder`").into(),
anyhow!("target must be `Server` or `Builder`")
.status_code(StatusCode::BAD_REQUEST),
);
}
}
@@ -465,11 +511,13 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
}
}
//
impl Resolve<ReadArgs> for ListDockerRegistriesFromConfig {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<ListDockerRegistriesFromConfigResponse> {
) -> mogh_error::Result<ListDockerRegistriesFromConfigResponse> {
let mut registries = core_config().docker_registries.clone();
if let Some(target) = self.target {
@@ -513,9 +561,10 @@ impl Resolve<ReadArgs> for ListDockerRegistriesFromConfig {
async fn merge_git_providers_for_server(
providers: &mut Vec<GitProvider>,
server_id: &str,
) -> serror::Result<()> {
) -> mogh_error::Result<()> {
let server = resource::get::<Server>(server_id).await?;
let more = periphery_client(&server)?
let more = periphery_client(&server)
.await?
.request(periphery_client::api::ListGitProviders {})
.await
.with_context(|| {
@@ -551,9 +600,10 @@ fn merge_git_providers(
async fn merge_docker_registries_for_server(
registries: &mut Vec<DockerRegistry>,
server_id: &str,
) -> serror::Result<()> {
) -> mogh_error::Result<()> {
let server = resource::get::<Server>(server_id).await?;
let more = periphery_client(&server)?
let more = periphery_client(&server)
.await?
.request(periphery_client::api::ListDockerRegistries {})
.await
.with_context(|| {

View File

@@ -0,0 +1,51 @@
use std::cmp::Ordering;
use anyhow::{Context, anyhow};
use database::mungos::find::find_collect;
use komodo_client::api::read::{
ListOnboardingKeys, ListOnboardingKeysResponse,
};
use mogh_error::AddStatusCodeError;
use mogh_resolver::Resolve;
use reqwest::StatusCode;
use crate::{api::read::ReadArgs, state::db_client};
//
impl Resolve<ReadArgs> for ListOnboardingKeys {
async fn resolve(
self,
ReadArgs { user: admin }: &ReadArgs,
) -> mogh_error::Result<ListOnboardingKeysResponse> {
if !admin.admin {
return Err(
anyhow!("This call is admin only")
.status_code(StatusCode::FORBIDDEN),
);
}
let mut keys =
find_collect(&db_client().onboarding_keys, None, None)
.await
.context(
"Failed to query database for Server onboarding keys",
)?;
// No expiry keys first, followed
keys.sort_by(|a, b| {
if a.expires == b.expires {
Ordering::Equal
} else if a.expires == 0 {
Ordering::Less
} else if b.expires == 0 {
Ordering::Greater
} else {
// Descending
b.expires.cmp(&a.expires)
}
});
Ok(keys)
}
}

View File

@@ -8,7 +8,7 @@ use komodo_client::{
},
entities::permission::PermissionLevel,
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{
helpers::query::get_user_permission_on_target, state::db_client,
@@ -20,7 +20,7 @@ impl Resolve<ReadArgs> for ListPermissions {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListPermissionsResponse> {
) -> mogh_error::Result<ListPermissionsResponse> {
let res = find_collect(
&db_client().permissions,
doc! {
@@ -39,7 +39,7 @@ impl Resolve<ReadArgs> for GetPermission {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetPermissionResponse> {
) -> mogh_error::Result<GetPermissionResponse> {
if user.admin {
return Ok(PermissionLevel::Write.all());
}
@@ -51,7 +51,7 @@ impl Resolve<ReadArgs> for ListUserTargetPermissions {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListUserTargetPermissionsResponse> {
) -> mogh_error::Result<ListUserTargetPermissionsResponse> {
if !user.admin {
return Err(anyhow!("this method is admin only").into());
}

View File

@@ -6,7 +6,7 @@ use komodo_client::{
procedure::{Procedure, ProcedureState},
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{
helpers::query::get_all_tags,
@@ -21,7 +21,7 @@ impl Resolve<ReadArgs> for GetProcedure {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetProcedureResponse> {
) -> mogh_error::Result<GetProcedureResponse> {
Ok(
get_check_permissions::<Procedure>(
&self.procedure,
@@ -37,7 +37,7 @@ impl Resolve<ReadArgs> for ListProcedures {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListProceduresResponse> {
) -> mogh_error::Result<ListProceduresResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -59,7 +59,7 @@ impl Resolve<ReadArgs> for ListFullProcedures {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullProceduresResponse> {
) -> mogh_error::Result<ListFullProceduresResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -81,7 +81,7 @@ impl Resolve<ReadArgs> for GetProceduresSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetProceduresSummaryResponse> {
) -> mogh_error::Result<GetProceduresSummaryResponse> {
let procedures = resource::list_full_for_user::<Procedure>(
Default::default(),
user,
@@ -127,7 +127,7 @@ impl Resolve<ReadArgs> for GetProcedureActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetProcedureActionStateResponse> {
) -> mogh_error::Result<GetProcedureActionStateResponse> {
let procedure = get_check_permissions::<Procedure>(
&self.procedure,
user,

View File

@@ -5,7 +5,7 @@ use database::mungos::{
mongodb::options::FindOptions,
};
use komodo_client::api::read::*;
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::state::db_client;
@@ -15,7 +15,7 @@ impl Resolve<ReadArgs> for GetGitProviderAccount {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetGitProviderAccountResponse> {
) -> mogh_error::Result<GetGitProviderAccountResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can read git provider accounts").into(),
@@ -35,7 +35,7 @@ impl Resolve<ReadArgs> for ListGitProviderAccounts {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListGitProviderAccountsResponse> {
) -> mogh_error::Result<ListGitProviderAccountsResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can read git provider accounts").into(),
@@ -65,7 +65,7 @@ impl Resolve<ReadArgs> for GetDockerRegistryAccount {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDockerRegistryAccountResponse> {
) -> mogh_error::Result<GetDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can read docker registry accounts")
@@ -87,7 +87,7 @@ impl Resolve<ReadArgs> for ListDockerRegistryAccounts {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerRegistryAccountsResponse> {
) -> mogh_error::Result<ListDockerRegistryAccountsResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can read docker registry accounts")

View File

@@ -2,19 +2,17 @@ use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
config::core::CoreConfig,
permission::PermissionLevel,
repo::{Repo, RepoActionState, RepoListItem, RepoState},
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{action_states, github_client, repo_state_cache},
state::{action_states, repo_state_cache},
};
use super::ReadArgs;
@@ -23,7 +21,7 @@ impl Resolve<ReadArgs> for GetRepo {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Repo> {
) -> mogh_error::Result<Repo> {
Ok(
get_check_permissions::<Repo>(
&self.repo,
@@ -39,7 +37,7 @@ impl Resolve<ReadArgs> for ListRepos {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<RepoListItem>> {
) -> mogh_error::Result<Vec<RepoListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -61,7 +59,7 @@ impl Resolve<ReadArgs> for ListFullRepos {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullReposResponse> {
) -> mogh_error::Result<ListFullReposResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -83,7 +81,7 @@ impl Resolve<ReadArgs> for GetRepoActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<RepoActionState> {
) -> mogh_error::Result<RepoActionState> {
let repo = get_check_permissions::<Repo>(
&self.repo,
user,
@@ -104,7 +102,7 @@ impl Resolve<ReadArgs> for GetReposSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetReposSummaryResponse> {
) -> mogh_error::Result<GetReposSummaryResponse> {
let repos = resource::list_full_for_user::<Repo>(
Default::default(),
user,
@@ -142,7 +140,11 @@ impl Resolve<ReadArgs> for GetReposSummary {
}
(RepoState::Ok, _) => res.ok += 1,
(RepoState::Failed, _) => res.failed += 1,
(RepoState::Unknown, _) => res.unknown += 1,
(RepoState::Unknown, _) => {
if !repo.template {
res.unknown += 1
}
}
// will never come off the cache in the building state, since that comes from action states
(RepoState::Cloning, _)
| (RepoState::Pulling, _)
@@ -155,104 +157,3 @@ impl Resolve<ReadArgs> for GetReposSummary {
Ok(res)
}
}
impl Resolve<ReadArgs> for GetRepoWebhooksEnabled {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetRepoWebhooksEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetRepoWebhooksEnabledResponse {
managed: false,
clone_enabled: false,
pull_enabled: false,
build_enabled: false,
});
};
let repo = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Read.into(),
)
.await?;
if repo.config.git_provider != "github.com"
|| repo.config.repo.is_empty()
{
return Ok(GetRepoWebhooksEnabledResponse {
managed: false,
clone_enabled: false,
pull_enabled: false,
build_enabled: false,
});
}
let mut split = repo.config.repo.split('/');
let owner = split.next().context("Repo repo has no owner")?;
let Some(github) = github.get(owner) else {
return Ok(GetRepoWebhooksEnabledResponse {
managed: false,
clone_enabled: false,
pull_enabled: false,
build_enabled: false,
});
};
let repo_name =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
let webhooks = github_repos
.list_all_webhooks(owner, repo_name)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let clone_url =
format!("{host}/listener/github/repo/{}/clone", repo.id);
let pull_url =
format!("{host}/listener/github/repo/{}/pull", repo.id);
let build_url =
format!("{host}/listener/github/repo/{}/build", repo.id);
let mut clone_enabled = false;
let mut pull_enabled = false;
let mut build_enabled = false;
for webhook in webhooks {
if !webhook.active {
continue;
}
if webhook.config.url == clone_url {
clone_enabled = true
}
if webhook.config.url == pull_url {
pull_enabled = true
}
if webhook.config.url == build_url {
build_enabled = true
}
}
Ok(GetRepoWebhooksEnabledResponse {
managed: true,
clone_enabled,
pull_enabled,
build_enabled,
})
}
}

View File

@@ -1,4 +1,4 @@
use futures::future::join_all;
use futures_util::future::join_all;
use komodo_client::{
api::read::*,
entities::{
@@ -10,7 +10,7 @@ use komodo_client::{
schedule::Schedule,
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{
helpers::query::{get_all_tags, get_last_run_at},
@@ -24,7 +24,7 @@ impl Resolve<ReadArgs> for ListSchedules {
async fn resolve(
self,
args: &ReadArgs,
) -> serror::Result<Vec<Schedule>> {
) -> mogh_error::Result<Vec<Schedule>> {
let all_tags = get_all_tags(None).await?;
let (actions, procedures) = tokio::try_join!(
list_full_for_user::<Action>(

View File

@@ -25,33 +25,31 @@ use komodo_client::{
network::Network,
volume::Volume,
},
komodo_timestamp,
permission::PermissionLevel,
server::{
Server, ServerActionState, ServerListItem, ServerState,
TerminalInfo,
Server, ServerActionState, ServerListItem, ServerQuery,
ServerState,
},
stack::{Stack, StackServiceNames},
stats::{SystemInformation, SystemProcess},
update::Log,
},
};
use mogh_error::AddStatusCode;
use mogh_resolver::Resolve;
use periphery_client::api::{
self as periphery,
container::InspectContainer,
image::{ImageHistory, InspectImage},
network::InspectNetwork,
volume::InspectVolume,
docker::{
ImageHistory, InspectImage, InspectNetwork, InspectVolume,
},
};
use resolver_api::Resolve;
use reqwest::StatusCode;
use tokio::sync::Mutex;
use crate::{
helpers::{
periphery_client,
query::{get_all_tags, get_system_info},
},
permission::get_check_permissions,
helpers::{periphery_client, query::get_all_tags},
permission::{get_check_permissions, list_resources_for_user},
resource,
stack::compose_container_match_regex,
state::{action_states, db_client, server_status_cache},
@@ -63,7 +61,7 @@ impl Resolve<ReadArgs> for GetServersSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetServersSummaryResponse> {
) -> mogh_error::Result<GetServersSummaryResponse> {
let servers = resource::list_for_user::<Server>(
Default::default(),
user,
@@ -80,11 +78,8 @@ impl Resolve<ReadArgs> for GetServersSummary {
match server.info.state {
ServerState::Ok => {
// Check for version mismatch
let has_version_mismatch = !server.info.version.is_empty()
&& server.info.version != "Unknown"
&& server.info.version != core_version;
if has_version_mismatch {
if matches!(&server.info.version, Some(version) if version != core_version)
{
res.warning += 1;
} else {
res.healthy += 1;
@@ -94,7 +89,9 @@ impl Resolve<ReadArgs> for GetServersSummary {
res.unhealthy += 1;
}
ServerState::Disabled => {
res.disabled += 1;
if !server.template {
res.disabled += 1;
}
}
}
}
@@ -102,31 +99,11 @@ impl Resolve<ReadArgs> for GetServersSummary {
}
}
impl Resolve<ReadArgs> for GetPeripheryVersion {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetPeripheryVersionResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let version = server_status_cache()
.get(&server.id)
.await
.map(|s| s.version.clone())
.unwrap_or(String::from("unknown"));
Ok(GetPeripheryVersionResponse { version })
}
}
impl Resolve<ReadArgs> for GetServer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Server> {
) -> mogh_error::Result<Server> {
Ok(
get_check_permissions::<Server>(
&self.server,
@@ -142,7 +119,7 @@ impl Resolve<ReadArgs> for ListServers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<ServerListItem>> {
) -> mogh_error::Result<Vec<ServerListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -164,7 +141,7 @@ impl Resolve<ReadArgs> for ListFullServers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullServersResponse> {
) -> mogh_error::Result<ListFullServersResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -186,7 +163,7 @@ impl Resolve<ReadArgs> for GetServerState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetServerStateResponse> {
) -> mogh_error::Result<GetServerStateResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -208,7 +185,7 @@ impl Resolve<ReadArgs> for GetServerActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ServerActionState> {
) -> mogh_error::Result<ServerActionState> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -225,18 +202,50 @@ impl Resolve<ReadArgs> for GetServerActionState {
}
}
impl Resolve<ReadArgs> for GetSystemInformation {
impl Resolve<ReadArgs> for GetPeripheryInformation {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<SystemInformation> {
) -> mogh_error::Result<GetPeripheryInformationResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
get_system_info(&server).await.map_err(Into::into)
server_status_cache()
.get(&server.id)
.await
.context("Missing server status")?
.periphery_info
.as_ref()
.cloned()
.context("Server status missing Periphery Info. The Server may be disconnected.")
.status_code(StatusCode::INTERNAL_SERVER_ERROR)
}
}
impl Resolve<ReadArgs> for GetSystemInformation {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<SystemInformation> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await
.status_code(StatusCode::BAD_REQUEST)?;
server_status_cache()
.get(&server.id)
.await
.context("Missing server status")?
.system_info
.as_ref()
.cloned()
.context("Server status missing system Info. The Server may be disconnected.")
.status_code(StatusCode::INTERNAL_SERVER_ERROR)
}
}
@@ -244,22 +253,22 @@ impl Resolve<ReadArgs> for GetSystemStats {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetSystemStatsResponse> {
) -> mogh_error::Result<GetSystemStatsResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.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
server_status_cache()
.get(&server.id)
.await
.context("Missing server status")?
.system_stats
.as_ref()
.context("server stats not available")?;
Ok(stats.clone())
.cloned()
.context("Server status missing system stats. The Server may be disconnected.")
.status_code(StatusCode::INTERNAL_SERVER_ERROR)
}
}
@@ -276,7 +285,7 @@ impl Resolve<ReadArgs> for ListSystemProcesses {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListSystemProcessesResponse> {
) -> mogh_error::Result<ListSystemProcessesResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -289,7 +298,8 @@ impl Resolve<ReadArgs> for ListSystemProcesses {
cached.0.clone()
}
_ => {
let stats = periphery_client(&server)?
let stats = periphery_client(&server)
.await?
.request(periphery::stats::GetSystemProcesses {})
.await?;
lock.insert(
@@ -310,7 +320,7 @@ impl Resolve<ReadArgs> for GetHistoricalServerStats {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetHistoricalServerStatsResponse> {
) -> mogh_error::Result<GetHistoricalServerStatsResponse> {
let GetHistoricalServerStats {
server,
granularity,
@@ -363,7 +373,7 @@ impl Resolve<ReadArgs> for ListDockerContainers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerContainersResponse> {
) -> mogh_error::Result<ListDockerContainersResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -373,8 +383,8 @@ impl Resolve<ReadArgs> for ListDockerContainers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(containers) = &cache.containers {
Ok(containers.clone())
if let Some(docker) = &cache.docker {
Ok(docker.containers.clone())
} else {
Ok(Vec::new())
}
@@ -385,20 +395,14 @@ impl Resolve<ReadArgs> for ListAllDockerContainers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListAllDockerContainersResponse> {
) -> mogh_error::Result<ListAllDockerContainersResponse> {
let servers = resource::list_for_user::<Server>(
Default::default(),
ServerQuery::builder().names(self.servers.clone()).build(),
user,
PermissionLevel::Read.into(),
&[],
)
.await?
.into_iter()
.filter(|server| {
self.servers.is_empty()
|| self.servers.contains(&server.id)
|| self.servers.contains(&server.name)
});
.await?;
let mut containers = Vec::<ContainerListItem>::new();
@@ -406,9 +410,18 @@ impl Resolve<ReadArgs> for ListAllDockerContainers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(more_containers) = &cache.containers {
containers.extend(more_containers.clone());
}
let Some(docker) = &cache.docker else {
continue;
};
let more = docker
.containers
.iter()
.filter(|container| {
self.containers.is_empty()
|| self.containers.contains(&container.name)
})
.cloned();
containers.extend(more);
}
Ok(containers)
@@ -419,7 +432,7 @@ impl Resolve<ReadArgs> for GetDockerContainersSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDockerContainersSummaryResponse> {
) -> mogh_error::Result<GetDockerContainersSummaryResponse> {
let servers = resource::list_full_for_user::<Server>(
Default::default(),
user,
@@ -436,8 +449,8 @@ impl Resolve<ReadArgs> for GetDockerContainersSummary {
.get_or_insert_default(&server.id)
.await;
if let Some(containers) = &cache.containers {
for container in containers {
if let Some(docker) = &cache.docker {
for container in &docker.containers {
res.total += 1;
match container.state {
ContainerStateStatusEnum::Created
@@ -459,7 +472,7 @@ impl Resolve<ReadArgs> for InspectDockerContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Container> {
) -> mogh_error::Result<Container> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -478,7 +491,8 @@ impl Resolve<ReadArgs> for InspectDockerContainer {
.into(),
);
}
let res = periphery_client(&server)?
let res = periphery_client(&server)
.await?
.request(InspectContainer {
name: self.container,
})
@@ -493,7 +507,7 @@ impl Resolve<ReadArgs> for GetContainerLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
) -> mogh_error::Result<Log> {
let GetContainerLog {
server,
container,
@@ -506,7 +520,8 @@ impl Resolve<ReadArgs> for GetContainerLog {
PermissionLevel::Read.logs(),
)
.await?;
let res = periphery_client(&server)?
let res = periphery_client(&server)
.await?
.request(periphery::container::GetContainerLog {
name: container,
tail: cmp::min(tail, MAX_LOG_LENGTH),
@@ -522,7 +537,7 @@ impl Resolve<ReadArgs> for SearchContainerLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
) -> mogh_error::Result<Log> {
let SearchContainerLog {
server,
container,
@@ -537,7 +552,8 @@ impl Resolve<ReadArgs> for SearchContainerLog {
PermissionLevel::Read.logs(),
)
.await?;
let res = periphery_client(&server)?
let res = periphery_client(&server)
.await?
.request(periphery::container::GetContainerLogSearch {
name: container,
terms,
@@ -555,7 +571,7 @@ impl Resolve<ReadArgs> for GetResourceMatchingContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetResourceMatchingContainerResponse> {
) -> mogh_error::Result<GetResourceMatchingContainerResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -572,12 +588,12 @@ impl Resolve<ReadArgs> for GetResourceMatchingContainer {
}
// then check stacks
let stacks =
resource::list_full_for_user_using_document::<Stack>(
doc! { "config.server_id": &server.id },
user,
)
.await?;
let stacks = list_resources_for_user::<Stack>(
doc! { "config.server_id": &server.id },
user,
PermissionLevel::Read.into(),
)
.await?;
// check matching stack
for stack in stacks {
@@ -616,7 +632,7 @@ impl Resolve<ReadArgs> for ListDockerNetworks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerNetworksResponse> {
) -> mogh_error::Result<ListDockerNetworksResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -626,8 +642,8 @@ impl Resolve<ReadArgs> for ListDockerNetworks {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(networks) = &cache.networks {
Ok(networks.clone())
if let Some(docker) = &cache.docker {
Ok(docker.networks.clone())
} else {
Ok(Vec::new())
}
@@ -638,7 +654,7 @@ impl Resolve<ReadArgs> for InspectDockerNetwork {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Network> {
) -> mogh_error::Result<Network> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -657,7 +673,8 @@ impl Resolve<ReadArgs> for InspectDockerNetwork {
.into(),
);
}
let res = periphery_client(&server)?
let res = periphery_client(&server)
.await?
.request(InspectNetwork { name: self.network })
.await?;
Ok(res)
@@ -668,7 +685,7 @@ impl Resolve<ReadArgs> for ListDockerImages {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerImagesResponse> {
) -> mogh_error::Result<ListDockerImagesResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -678,8 +695,8 @@ impl Resolve<ReadArgs> for ListDockerImages {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(images) = &cache.images {
Ok(images.clone())
if let Some(docker) = &cache.docker {
Ok(docker.images.clone())
} else {
Ok(Vec::new())
}
@@ -690,7 +707,7 @@ impl Resolve<ReadArgs> for InspectDockerImage {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Image> {
) -> mogh_error::Result<Image> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -706,7 +723,8 @@ impl Resolve<ReadArgs> for InspectDockerImage {
.into(),
);
}
let res = periphery_client(&server)?
let res = periphery_client(&server)
.await?
.request(InspectImage { name: self.image })
.await?;
Ok(res)
@@ -717,7 +735,7 @@ impl Resolve<ReadArgs> for ListDockerImageHistory {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<ImageHistoryResponseItem>> {
) -> mogh_error::Result<Vec<ImageHistoryResponseItem>> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -736,7 +754,8 @@ impl Resolve<ReadArgs> for ListDockerImageHistory {
.into(),
);
}
let res = periphery_client(&server)?
let res = periphery_client(&server)
.await?
.request(ImageHistory { name: self.image })
.await?;
Ok(res)
@@ -747,7 +766,7 @@ impl Resolve<ReadArgs> for ListDockerVolumes {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerVolumesResponse> {
) -> mogh_error::Result<ListDockerVolumesResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -757,8 +776,8 @@ impl Resolve<ReadArgs> for ListDockerVolumes {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(volumes) = &cache.volumes {
Ok(volumes.clone())
if let Some(docker) = &cache.docker {
Ok(docker.volumes.clone())
} else {
Ok(Vec::new())
}
@@ -769,7 +788,7 @@ impl Resolve<ReadArgs> for InspectDockerVolume {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Volume> {
) -> mogh_error::Result<Volume> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -785,7 +804,8 @@ impl Resolve<ReadArgs> for InspectDockerVolume {
.into(),
);
}
let res = periphery_client(&server)?
let res = periphery_client(&server)
.await?
.request(InspectVolume { name: self.volume })
.await?;
Ok(res)
@@ -796,7 +816,7 @@ impl Resolve<ReadArgs> for ListComposeProjects {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListComposeProjectsResponse> {
) -> mogh_error::Result<ListComposeProjectsResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -806,73 +826,54 @@ impl Resolve<ReadArgs> for ListComposeProjects {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(projects) = &cache.projects {
Ok(projects.clone())
if let Some(docker) = &cache.docker {
Ok(docker.projects.clone())
} else {
Ok(Vec::new())
}
}
}
#[derive(Default)]
struct TerminalCacheItem {
list: Vec<TerminalInfo>,
ttl: i64,
}
// impl Resolve<ReadArgs> for ListAllTerminals {
// async fn resolve(
// self,
// args: &ReadArgs,
// ) -> Result<Self::Response, Self::Error> {
// // match self.tar
// let mut terminals = resource::list_full_for_user::<Server>(
// self.query, &args.user, &all_tags,
// )
// .await?
// .into_iter()
// .map(|server| async move {
// (
// list_terminals_inner(&server, self.fresh).await,
// (server.id, server.name),
// )
// })
// .collect::<FuturesUnordered<_>>()
// .collect::<Vec<_>>()
// .await
// .into_iter()
// .flat_map(|(terminals, server)| {
// let terminals = terminals.ok()?;
// Some((terminals, server))
// })
// .flat_map(|(terminals, (server_id, server_name))| {
// terminals.into_iter().map(move |info| {
// TerminalInfoWithServer::from_terminal_info(
// &server_id,
// &server_name,
// info,
// )
// })
// })
// .collect::<Vec<_>>();
const TERMINAL_CACHE_TIMEOUT: i64 = 30_000;
// terminals.sort_by(|a, b| {
// a.server_name.cmp(&b.server_name).then(a.name.cmp(&b.name))
// });
#[derive(Default)]
struct TerminalCache(
std::sync::Mutex<
HashMap<String, Arc<tokio::sync::Mutex<TerminalCacheItem>>>,
>,
);
impl TerminalCache {
fn get_or_insert(
&self,
server_id: String,
) -> Arc<tokio::sync::Mutex<TerminalCacheItem>> {
if let Some(cached) =
self.0.lock().unwrap().get(&server_id).cloned()
{
return cached;
}
let to_cache =
Arc::new(tokio::sync::Mutex::new(TerminalCacheItem::default()));
self.0.lock().unwrap().insert(server_id, to_cache.clone());
to_cache
}
}
fn terminals_cache() -> &'static TerminalCache {
static TERMINALS: OnceLock<TerminalCache> = OnceLock::new();
TERMINALS.get_or_init(Default::default)
}
impl Resolve<ReadArgs> for ListTerminals {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListTerminalsResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.terminal(),
)
.await?;
let cache = terminals_cache().get_or_insert(server.id.clone());
let mut cache = cache.lock().await;
if self.fresh || komodo_timestamp() > cache.ttl {
cache.list = periphery_client(&server)?
.request(periphery_client::api::terminal::ListTerminals {})
.await
.context("Failed to get fresh terminal list")?;
cache.ttl = komodo_timestamp() + TERMINAL_CACHE_TIMEOUT;
Ok(cache.list.clone())
} else {
Ok(cache.list.clone())
}
}
}
// Ok(terminals)
// }
// }

View File

@@ -4,29 +4,30 @@ use anyhow::{Context, anyhow};
use komodo_client::{
api::read::*,
entities::{
config::core::CoreConfig,
docker::container::Container,
SwarmOrServer,
docker::{
container::Container, service::SwarmService, stack::SwarmStack,
},
permission::PermissionLevel,
server::{Server, ServerState},
stack::{Stack, StackActionState, StackListItem, StackState},
},
};
use mogh_error::AddStatusCodeError as _;
use mogh_resolver::Resolve;
use periphery_client::api::{
compose::{GetComposeLog, GetComposeLogSearch},
container::InspectContainer,
};
use resolver_api::Resolve;
use reqwest::StatusCode;
use crate::{
config::core_config,
helpers::{periphery_client, query::get_all_tags},
helpers::{
periphery_client, query::get_all_tags, swarm::swarm_request,
},
permission::get_check_permissions,
resource,
stack::get_stack_and_server,
state::{
action_states, github_client, server_status_cache,
stack_status_cache,
},
stack::setup_stack_execution,
state::{action_states, stack_status_cache},
};
use super::ReadArgs;
@@ -35,7 +36,7 @@ impl Resolve<ReadArgs> for GetStack {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Stack> {
) -> mogh_error::Result<Stack> {
Ok(
get_check_permissions::<Stack>(
&self.stack,
@@ -51,7 +52,7 @@ impl Resolve<ReadArgs> for ListStackServices {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListStackServicesResponse> {
) -> mogh_error::Result<ListStackServicesResponse> {
let stack = get_check_permissions::<Stack>(
&self.stack,
user,
@@ -75,30 +76,59 @@ impl Resolve<ReadArgs> for GetStackLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetStackLogResponse> {
) -> mogh_error::Result<GetStackLogResponse> {
let GetStackLog {
stack,
services,
mut services,
tail,
timestamps,
} = self;
let (stack, server) = get_stack_and_server(
let (stack, swarm_or_server) = setup_stack_execution(
&stack,
user,
PermissionLevel::Read.logs(),
true,
)
.await?;
let res = periphery_client(&server)?
.request(GetComposeLog {
project: stack.project_name(false),
services,
tail,
timestamps,
})
.await
.context("Failed to get stack log from periphery")?;
Ok(res)
swarm_or_server.verify_has_target()?;
let log = match swarm_or_server {
SwarmOrServer::None => unreachable!(),
SwarmOrServer::Swarm(swarm) => {
let service = services.pop().context(
"Must pass single service for Swarm mode Stack logs",
)?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::GetSwarmServiceLog {
// The actual service name on swarm will be stackname_servicename
service: format!(
"{}_{service}",
stack.project_name(false)
),
tail,
timestamps,
no_task_ids: false,
no_resolve: false,
details: false,
},
)
.await
.context("Failed to get stack service log from swarm")?
}
SwarmOrServer::Server(server) => periphery_client(&server)
.await?
.request(GetComposeLog {
project: stack.project_name(false),
services,
tail,
timestamps,
})
.await
.context("Failed to get stack log from periphery")?,
};
Ok(log)
}
}
@@ -106,34 +136,61 @@ impl Resolve<ReadArgs> for SearchStackLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<SearchStackLogResponse> {
) -> mogh_error::Result<SearchStackLogResponse> {
let SearchStackLog {
stack,
services,
mut services,
terms,
combinator,
invert,
timestamps,
} = self;
let (stack, server) = get_stack_and_server(
let (stack, swarm_or_server) = setup_stack_execution(
&stack,
user,
PermissionLevel::Read.logs(),
true,
)
.await?;
let res = periphery_client(&server)?
.request(GetComposeLogSearch {
project: stack.project_name(false),
services,
terms,
combinator,
invert,
timestamps,
})
.await
.context("Failed to search stack log from periphery")?;
Ok(res)
swarm_or_server.verify_has_target()?;
let log = match swarm_or_server {
SwarmOrServer::None => unreachable!(),
SwarmOrServer::Swarm(swarm) => {
let service = services.pop().context(
"Must pass single service for Swarm mode Stack logs",
)?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::GetSwarmServiceLogSearch {
service,
terms,
combinator,
invert,
timestamps,
no_task_ids: false,
no_resolve: false,
details: false,
},
)
.await
.context("Failed to get stack service log from swarm")?
}
SwarmOrServer::Server(server) => periphery_client(&server)
.await?
.request(GetComposeLogSearch {
project: stack.project_name(false),
services,
terms,
combinator,
invert,
timestamps,
})
.await
.context("Failed to search stack log from periphery")?,
};
Ok(log)
}
}
@@ -141,40 +198,31 @@ impl Resolve<ReadArgs> for InspectStackContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Container> {
) -> mogh_error::Result<Container> {
let InspectStackContainer { stack, service } = self;
let stack = get_check_permissions::<Stack>(
let (stack, swarm_or_server) = setup_stack_execution(
&stack,
user,
PermissionLevel::Read.inspect(),
)
.await?;
if stack.config.server_id.is_empty() {
return Err(
anyhow!("Cannot inspect stack, not attached to any server")
.into(),
);
}
let server =
resource::get::<Server>(&stack.config.server_id).await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
let SwarmOrServer::Server(server) = swarm_or_server else {
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
"InspectStackContainer should not be called for Stack in Swarm Mode"
)
.into(),
.status_code(StatusCode::BAD_REQUEST),
);
}
};
let services = &stack_status_cache()
.get(&stack.id)
.await
.unwrap_or_default()
.curr
.services;
let Some(name) = services
.iter()
.find(|s| s.service == service)
@@ -184,18 +232,106 @@ impl Resolve<ReadArgs> for InspectStackContainer {
"No service found matching '{service}'. Was the stack last deployed manually?"
).into());
};
let res = periphery_client(&server)?
let res = periphery_client(&server)
.await?
.request(InspectContainer { name })
.await?;
.await
.context("Failed to inspect container on server")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for InspectStackSwarmService {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<SwarmService> {
let InspectStackSwarmService { stack, service } = self;
let (stack, swarm_or_server) = setup_stack_execution(
&stack,
user,
PermissionLevel::Read.inspect(),
)
.await?;
let SwarmOrServer::Swarm(swarm) = swarm_or_server else {
return Err(
anyhow!(
"InspectStackSwarmService should only be called for Stack in Swarm Mode"
)
.status_code(StatusCode::BAD_REQUEST),
);
};
let services = &stack_status_cache()
.get(&stack.id)
.await
.unwrap_or_default()
.curr
.services;
let Some(service) = services
.iter()
.find(|s| s.service == service)
.and_then(|s| {
s.swarm_service.as_ref().and_then(|c| c.name.clone())
})
else {
return Err(anyhow!(
"No service found matching '{service}'. Was the stack last deployed manually?"
).into());
};
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmService { service },
)
.await
.context("Failed to inspect service on swarm")
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for InspectStackSwarmInfo {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<SwarmStack> {
let (stack, swarm_or_server) = setup_stack_execution(
&self.stack,
user,
PermissionLevel::Read.inspect(),
)
.await?;
let SwarmOrServer::Swarm(swarm) = swarm_or_server else {
return Err(
anyhow!(
"InspectStackSwarmInfo should only be called for Stack in Swarm Mode"
)
.status_code(StatusCode::BAD_REQUEST),
);
};
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmStack {
stack: stack.project_name(false),
},
)
.await
.context("Failed to inspect stack info on swarm")
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListCommonStackExtraArgsResponse> {
) -> mogh_error::Result<ListCommonStackExtraArgsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -208,7 +344,7 @@ impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
&all_tags,
)
.await
.context("failed to get resources matching query")?;
.context("Failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();
@@ -229,7 +365,7 @@ impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListCommonStackBuildExtraArgsResponse> {
) -> mogh_error::Result<ListCommonStackBuildExtraArgsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -242,7 +378,7 @@ impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
&all_tags,
)
.await
.context("failed to get resources matching query")?;
.context("Failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();
@@ -263,7 +399,7 @@ impl Resolve<ReadArgs> for ListStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<StackListItem>> {
) -> mogh_error::Result<Vec<StackListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -299,7 +435,7 @@ impl Resolve<ReadArgs> for ListFullStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullStacksResponse> {
) -> mogh_error::Result<ListFullStacksResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -321,7 +457,7 @@ impl Resolve<ReadArgs> for GetStackActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<StackActionState> {
) -> mogh_error::Result<StackActionState> {
let stack = get_check_permissions::<Stack>(
&self.stack,
user,
@@ -342,7 +478,7 @@ impl Resolve<ReadArgs> for GetStacksSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetStacksSummaryResponse> {
) -> mogh_error::Result<GetStacksSummaryResponse> {
let stacks = resource::list_full_for_user::<Stack>(
Default::default(),
user,
@@ -350,7 +486,7 @@ impl Resolve<ReadArgs> for GetStacksSummary {
&[],
)
.await
.context("failed to get stacks from db")?;
.context("Failed to get stacks from database")?;
let mut res = GetStacksSummaryResponse::default();
@@ -363,7 +499,11 @@ impl Resolve<ReadArgs> for GetStacksSummary {
StackState::Running => res.running += 1,
StackState::Stopped | StackState::Paused => res.stopped += 1,
StackState::Down => res.down += 1,
StackState::Unknown => res.unknown += 1,
StackState::Unknown => {
if !stack.template {
res.unknown += 1
}
}
_ => res.unhealthy += 1,
}
}
@@ -371,91 +511,3 @@ impl Resolve<ReadArgs> for GetStacksSummary {
Ok(res)
}
}
impl Resolve<ReadArgs> for GetStackWebhooksEnabled {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetStackWebhooksEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetStackWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
deploy_enabled: false,
});
};
let stack = get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read.into(),
)
.await?;
if stack.config.git_provider != "github.com"
|| stack.config.repo.is_empty()
{
return Ok(GetStackWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
deploy_enabled: false,
});
}
let mut split = stack.config.repo.split('/');
let owner = split.next().context("Sync repo has no owner")?;
let Some(github) = github.get(owner) else {
return Ok(GetStackWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
deploy_enabled: false,
});
};
let repo_name =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
let webhooks = github_repos
.list_all_webhooks(owner, repo_name)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let refresh_url =
format!("{host}/listener/github/stack/{}/refresh", stack.id);
let deploy_url =
format!("{host}/listener/github/stack/{}/deploy", stack.id);
let mut refresh_enabled = false;
let mut deploy_enabled = false;
for webhook in webhooks {
if webhook.active && webhook.config.url == refresh_url {
refresh_enabled = true
}
if webhook.active && webhook.config.url == deploy_url {
deploy_enabled = true
}
}
Ok(GetStackWebhooksEnabledResponse {
managed: true,
refresh_enabled,
deploy_enabled,
})
}
}

View File

@@ -0,0 +1,522 @@
use anyhow::{Context, anyhow};
use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel,
swarm::{Swarm, SwarmActionState, SwarmListItem, SwarmState},
},
};
use mogh_resolver::Resolve;
use crate::{
helpers::{query::get_all_tags, swarm::swarm_request},
permission::get_check_permissions,
resource,
state::{action_states, server_status_cache, swarm_status_cache},
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetSwarm {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Swarm> {
Ok(
get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListSwarms {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Vec<SwarmListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Swarm>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListFullSwarms {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListFullSwarmsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Swarm>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetSwarmActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<SwarmActionState> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
.swarm
.get(&swarm.id)
.await
.unwrap_or_default()
.get()?;
Ok(action_state)
}
}
impl Resolve<ReadArgs> for GetSwarmsSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetSwarmsSummaryResponse> {
let swarms = resource::list_full_for_user::<Swarm>(
Default::default(),
user,
PermissionLevel::Read.into(),
&[],
)
.await
.context("failed to get swarms from db")?;
let mut res = GetSwarmsSummaryResponse::default();
let cache = swarm_status_cache();
for swarm in swarms {
res.total += 1;
match cache
.get(&swarm.id)
.await
.map(|status| status.state)
.unwrap_or_default()
{
SwarmState::Unknown => {
res.unknown += 1;
}
SwarmState::Healthy => {
res.healthy += 1;
}
SwarmState::Unhealthy => {
res.unhealthy += 1;
}
SwarmState::Down => {
res.down += 1;
}
}
}
Ok(res)
}
}
impl Resolve<ReadArgs> for InspectSwarm {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<InspectSwarmResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.inspect(),
)
.await?;
let cache =
swarm_status_cache().get_or_insert_default(&swarm.id).await;
let inspect = cache
.inspect
.as_ref()
.cloned()
.context("SwarmInspectInfo not available")?;
Ok(inspect)
}
}
impl Resolve<ReadArgs> for ListSwarmNodes {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListSwarmNodesResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache =
swarm_status_cache().get_or_insert_default(&swarm.id).await;
if let Some(lists) = &cache.lists {
Ok(lists.nodes.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectSwarmNode {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<InspectSwarmNodeResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.inspect(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmNode {
node: self.node,
},
)
.await
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for ListSwarmServices {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListSwarmServicesResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache =
swarm_status_cache().get_or_insert_default(&swarm.id).await;
if let Some(lists) = &cache.lists {
Ok(lists.services.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectSwarmService {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<InspectSwarmServiceResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.inspect(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmService {
service: self.service,
},
)
.await
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for GetSwarmServiceLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetSwarmServiceLogResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.logs(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::GetSwarmServiceLog {
service: self.service,
tail: self.tail,
timestamps: self.timestamps,
no_task_ids: self.no_task_ids,
no_resolve: self.no_resolve,
details: self.details,
},
)
.await
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for SearchSwarmServiceLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<SearchSwarmServiceLogResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.logs(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::GetSwarmServiceLogSearch {
service: self.service,
terms: self.terms,
combinator: self.combinator,
invert: self.invert,
timestamps: self.timestamps,
no_task_ids: self.no_task_ids,
no_resolve: self.no_resolve,
details: self.details,
},
)
.await
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for ListSwarmTasks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListSwarmTasksResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache =
swarm_status_cache().get_or_insert_default(&swarm.id).await;
if let Some(lists) = &cache.lists {
Ok(lists.tasks.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectSwarmTask {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<InspectSwarmTaskResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.inspect(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmTask {
task: self.task,
},
)
.await
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for ListSwarmSecrets {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListSwarmSecretsResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache =
swarm_status_cache().get_or_insert_default(&swarm.id).await;
if let Some(lists) = &cache.lists {
Ok(lists.secrets.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectSwarmSecret {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<InspectSwarmSecretResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.inspect(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmSecret {
secret: self.secret,
},
)
.await
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for ListSwarmConfigs {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListSwarmConfigsResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache =
swarm_status_cache().get_or_insert_default(&swarm.id).await;
if let Some(lists) = &cache.lists {
Ok(lists.configs.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectSwarmConfig {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<InspectSwarmConfigResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.inspect(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmConfig {
config: self.config,
},
)
.await
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for ListSwarmStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListSwarmStacksResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache =
swarm_status_cache().get_or_insert_default(&swarm.id).await;
if let Some(lists) = &cache.lists {
Ok(lists.stacks.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectSwarmStack {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<InspectSwarmStackResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.inspect(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmStack {
stack: self.stack,
},
)
.await
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for ListSwarmNetworks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache();
for server_id in swarm.config.server_ids {
let Some(status) = cache.get(&server_id).await else {
continue;
};
let Some(docker) = &status.docker else {
continue;
};
let networks = docker
.networks
.iter()
.filter(|network| {
network.driver.as_deref() == Some("overlay")
})
.cloned()
.collect::<Vec<_>>();
return Ok(networks);
}
Err(
anyhow!(
"Failed to retrieve swarm networks from any manager node."
)
.into(),
)
}
}

View File

@@ -2,21 +2,17 @@ use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
config::core::CoreConfig,
permission::PermissionLevel,
sync::{
ResourceSync, ResourceSyncActionState, ResourceSyncListItem,
},
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{action_states, github_client},
helpers::query::get_all_tags, permission::get_check_permissions,
resource, state::action_states,
};
use super::ReadArgs;
@@ -25,7 +21,7 @@ impl Resolve<ReadArgs> for GetResourceSync {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ResourceSync> {
) -> mogh_error::Result<ResourceSync> {
Ok(
get_check_permissions::<ResourceSync>(
&self.sync,
@@ -41,7 +37,7 @@ impl Resolve<ReadArgs> for ListResourceSyncs {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<ResourceSyncListItem>> {
) -> mogh_error::Result<Vec<ResourceSyncListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -63,7 +59,7 @@ impl Resolve<ReadArgs> for ListFullResourceSyncs {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullResourceSyncsResponse> {
) -> mogh_error::Result<ListFullResourceSyncsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
@@ -85,7 +81,7 @@ impl Resolve<ReadArgs> for GetResourceSyncActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ResourceSyncActionState> {
) -> mogh_error::Result<ResourceSyncActionState> {
let sync = get_check_permissions::<ResourceSync>(
&self.sync,
user,
@@ -106,7 +102,7 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetResourceSyncsSummaryResponse> {
) -> mogh_error::Result<GetResourceSyncsSummaryResponse> {
let resource_syncs =
resource::list_full_for_user::<ResourceSync>(
Default::default(),
@@ -154,91 +150,3 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
Ok(res)
}
}
impl Resolve<ReadArgs> for GetSyncWebhooksEnabled {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetSyncWebhooksEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetSyncWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
sync_enabled: false,
});
};
let sync = get_check_permissions::<ResourceSync>(
&self.sync,
user,
PermissionLevel::Read.into(),
)
.await?;
if sync.config.git_provider != "github.com"
|| sync.config.repo.is_empty()
{
return Ok(GetSyncWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
sync_enabled: false,
});
}
let mut split = sync.config.repo.split('/');
let owner = split.next().context("Sync repo has no owner")?;
let Some(github) = github.get(owner) else {
return Ok(GetSyncWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
sync_enabled: false,
});
};
let repo_name =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
let webhooks = github_repos
.list_all_webhooks(owner, repo_name)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let refresh_url =
format!("{host}/listener/github/sync/{}/refresh", sync.id);
let sync_url =
format!("{host}/listener/github/sync/{}/sync", sync.id);
let mut refresh_enabled = false;
let mut sync_enabled = false;
for webhook in webhooks {
if webhook.active && webhook.config.url == refresh_url {
refresh_enabled = true
}
if webhook.active && webhook.config.url == sync_url {
sync_enabled = true
}
}
Ok(GetSyncWebhooksEnabledResponse {
managed: true,
refresh_enabled,
sync_enabled,
})
}
}

View File

@@ -7,20 +7,23 @@ use komodo_client::{
api::read::{GetTag, ListTags},
entities::tag::Tag,
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{helpers::query::get_tag, state::db_client};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetTag {
async fn resolve(self, _: &ReadArgs) -> serror::Result<Tag> {
async fn resolve(self, _: &ReadArgs) -> mogh_error::Result<Tag> {
Ok(get_tag(&self.tag).await?)
}
}
impl Resolve<ReadArgs> for ListTags {
async fn resolve(self, _: &ReadArgs) -> serror::Result<Vec<Tag>> {
async fn resolve(
self,
_: &ReadArgs,
) -> mogh_error::Result<Vec<Tag>> {
let res = find_collect(
&db_client().tags,
self.query,

View File

@@ -0,0 +1,247 @@
use anyhow::Context as _;
use futures_util::{
FutureExt, StreamExt as _, stream::FuturesUnordered,
};
use komodo_client::{
api::read::{ListTerminals, ListTerminalsResponse},
entities::{
deployment::Deployment,
permission::PermissionLevel,
server::Server,
stack::Stack,
terminal::{Terminal, TerminalTarget},
user::User,
},
};
use mogh_error::AddStatusCode;
use mogh_resolver::Resolve;
use reqwest::StatusCode;
use crate::{
helpers::periphery_client, permission::get_check_permissions,
resource,
};
use super::ReadArgs;
//
impl Resolve<ReadArgs> for ListTerminals {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListTerminalsResponse> {
let Some(target) = self.target else {
return list_all_terminals_for_user(user, self.use_names).await;
};
match &target {
TerminalTarget::Server { server } => {
let server = server
.as_ref()
.context("Must provide 'target.params.server'")
.status_code(StatusCode::BAD_REQUEST)?;
let server = get_check_permissions::<Server>(
server,
user,
PermissionLevel::Read.terminal(),
)
.await?;
list_terminals_on_server(&server, Some(target)).await
}
TerminalTarget::Container { server, .. } => {
let server = get_check_permissions::<Server>(
server,
user,
PermissionLevel::Read.terminal(),
)
.await?;
list_terminals_on_server(&server, Some(target)).await
}
TerminalTarget::Stack { stack, .. } => {
let server = get_check_permissions::<Stack>(
stack,
user,
PermissionLevel::Read.terminal(),
)
.await?
.config
.server_id;
let server = resource::get::<Server>(&server).await?;
list_terminals_on_server(&server, Some(target)).await
}
TerminalTarget::Deployment { deployment } => {
let server = get_check_permissions::<Deployment>(
deployment,
user,
PermissionLevel::Read.terminal(),
)
.await?
.config
.server_id;
let server = resource::get::<Server>(&server).await?;
list_terminals_on_server(&server, Some(target)).await
}
}
}
}
async fn list_all_terminals_for_user(
user: &User,
use_names: bool,
) -> mogh_error::Result<Vec<Terminal>> {
let (mut servers, stacks, deployments) = tokio::try_join!(
resource::list_full_for_user::<Server>(
Default::default(),
user,
PermissionLevel::Read.terminal(),
&[]
)
.map(|res| res.map(|servers| servers
.into_iter()
// true denotes user actually has permission on this Server.
.map(|server| (server, true))
.collect::<Vec<_>>())),
resource::list_full_for_user::<Stack>(
Default::default(),
user,
PermissionLevel::Read.terminal(),
&[]
),
resource::list_full_for_user::<Deployment>(
Default::default(),
user,
PermissionLevel::Read.terminal(),
&[]
),
)?;
// Ensure any missing servers are present to query
for stack in &stacks {
if !stack.config.server_id.is_empty()
&& !servers
.iter()
.any(|(server, _)| server.id == stack.config.server_id)
{
let server =
resource::get::<Server>(&stack.config.server_id).await?;
servers.push((server, false));
}
}
for deployment in &deployments {
if !deployment.config.server_id.is_empty()
&& !servers
.iter()
.any(|(server, _)| server.id == deployment.config.server_id)
{
let server =
resource::get::<Server>(&deployment.config.server_id).await?;
servers.push((server, false));
}
}
let mut terminals = servers
.into_iter()
.map(|(server, server_permission)| async move {
(
list_terminals_on_server(&server, None).await,
(server.id, server.name, server_permission),
)
})
.collect::<FuturesUnordered<_>>()
.collect::<Vec<_>>()
.await
.into_iter()
.flat_map(
|(terminals, (server_id, server_name, server_permission))| {
let terminals = terminals
.ok()?
.into_iter()
.filter_map(|mut terminal| {
// Only keep terminals with appropriate perms.
match terminal.target.clone() {
TerminalTarget::Server { .. } => server_permission
.then(|| {
terminal.target = TerminalTarget::Server {
server: Some(if use_names {
server_name.clone()
} else {
server_id.clone()
}),
};
terminal
}),
TerminalTarget::Container { container, .. } => {
server_permission.then(|| {
terminal.target = TerminalTarget::Container {
server: if use_names {
server_name.clone()
} else {
server_id.clone()
},
container,
};
terminal
})
}
TerminalTarget::Stack { stack, service } => {
stacks.iter().find(|s| s.id == stack).map(|s| {
terminal.target = TerminalTarget::Stack {
stack: if use_names {
s.name.clone()
} else {
s.id.clone()
},
service,
};
terminal
})
}
TerminalTarget::Deployment { deployment } => {
deployments.iter().find(|d| d.id == deployment).map(
|d| {
terminal.target = TerminalTarget::Deployment {
deployment: if use_names {
d.name.clone()
} else {
d.id.clone()
},
};
terminal
},
)
}
}
})
.collect::<Vec<_>>();
Some(terminals)
},
)
.flatten()
.collect::<Vec<_>>();
terminals.sort_by(|a, b| {
a.target.cmp(&b.target).then(a.name.cmp(&b.name))
});
Ok(terminals)
}
async fn list_terminals_on_server(
server: &Server,
target: Option<TerminalTarget>,
) -> mogh_error::Result<Vec<Terminal>> {
periphery_client(server)
.await?
.request(periphery_client::api::terminal::ListTerminals {
target,
})
.await
.with_context(|| {
format!(
"Failed to get Terminal list from Server {} ({})",
server.name, server.id
)
})
.map_err(Into::into)
}

View File

@@ -11,10 +11,11 @@ use komodo_client::{
builder::Builder, deployment::Deployment,
permission::PermissionLevel, procedure::Procedure, repo::Repo,
resource::ResourceQuery, server::Server, stack::Stack,
sync::ResourceSync, toml::ResourcesToml, user::User,
swarm::Swarm, sync::ResourceSync, toml::ResourcesToml,
user::User,
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{
helpers::query::{
@@ -160,7 +161,7 @@ impl Resolve<ReadArgs> for ExportAllResourcesToToml {
async fn resolve(
self,
args: &ReadArgs,
) -> serror::Result<ExportAllResourcesToTomlResponse> {
) -> mogh_error::Result<ExportAllResourcesToTomlResponse> {
let targets = if self.include_resources {
get_all_targets(&self.tags, &args.user).await?
} else {
@@ -196,7 +197,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
async fn resolve(
self,
args: &ReadArgs,
) -> serror::Result<ExportResourcesToTomlResponse> {
) -> mogh_error::Result<ExportResourcesToTomlResponse> {
let ExportResourcesToToml {
targets,
user_groups,
@@ -207,42 +208,21 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
let ReadArgs { user } = args;
for target in targets {
match target {
ResourceTarget::Alerter(id) => {
let mut alerter = get_check_permissions::<Alerter>(
ResourceTarget::Swarm(id) => {
let mut swarm = get_check_permissions::<Swarm>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Alerter::replace_ids(&mut alerter);
res.alerters.push(convert_resource::<Alerter>(
alerter,
Swarm::replace_ids(&mut swarm);
res.swarms.push(convert_resource::<Swarm>(
swarm,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::ResourceSync(id) => {
let mut sync = get_check_permissions::<ResourceSync>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
if sync.config.file_contents.is_empty()
&& (sync.config.files_on_host
|| !sync.config.repo.is_empty()
|| !sync.config.linked_repo.is_empty())
{
ResourceSync::replace_ids(&mut sync);
res.resource_syncs.push(convert_resource::<ResourceSync>(
sync,
false,
vec![],
&id_to_tags,
))
}
}
ResourceTarget::Server(id) => {
let mut server = get_check_permissions::<Server>(
&id,
@@ -258,31 +238,16 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
&id_to_tags,
))
}
ResourceTarget::Builder(id) => {
let mut builder = get_check_permissions::<Builder>(
ResourceTarget::Stack(id) => {
let mut stack = get_check_permissions::<Stack>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Builder::replace_ids(&mut builder);
res.builders.push(convert_resource::<Builder>(
builder,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Build(id) => {
let mut build = get_check_permissions::<Build>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Build::replace_ids(&mut build);
res.builds.push(convert_resource::<Build>(
build,
Stack::replace_ids(&mut stack);
res.stacks.push(convert_resource::<Stack>(
stack,
false,
vec![],
&id_to_tags,
@@ -303,6 +268,21 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
&id_to_tags,
))
}
ResourceTarget::Build(id) => {
let mut build = get_check_permissions::<Build>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Build::replace_ids(&mut build);
res.builds.push(convert_resource::<Build>(
build,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Repo(id) => {
let mut repo = get_check_permissions::<Repo>(
&id,
@@ -318,21 +298,6 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
&id_to_tags,
))
}
ResourceTarget::Stack(id) => {
let mut stack = get_check_permissions::<Stack>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Stack::replace_ids(&mut stack);
res.stacks.push(convert_resource::<Stack>(
stack,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Procedure(id) => {
let mut procedure = get_check_permissions::<Procedure>(
&id,
@@ -363,6 +328,57 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
&id_to_tags,
));
}
ResourceTarget::ResourceSync(id) => {
let mut sync = get_check_permissions::<ResourceSync>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
if sync.config.file_contents.is_empty()
&& (sync.config.files_on_host
|| !sync.config.repo.is_empty()
|| !sync.config.linked_repo.is_empty())
{
ResourceSync::replace_ids(&mut sync);
res.resource_syncs.push(convert_resource::<ResourceSync>(
sync,
false,
vec![],
&id_to_tags,
))
}
}
ResourceTarget::Builder(id) => {
let mut builder = get_check_permissions::<Builder>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Builder::replace_ids(&mut builder);
res.builders.push(convert_resource::<Builder>(
builder,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Alerter(id) => {
let mut alerter = get_check_permissions::<Alerter>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Alerter::replace_ids(&mut alerter);
res.alerters.push(convert_resource::<Alerter>(
alerter,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::System(_) => continue,
};
}

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use anyhow::{Context, anyhow};
use anyhow::Context;
use database::mungos::{
by_id::find_one_by_id,
find::find_collect,
@@ -9,27 +9,18 @@ use database::mungos::{
use komodo_client::{
api::read::{GetUpdate, ListUpdates, ListUpdatesResponse},
entities::{
ResourceTarget,
action::Action,
alerter::Alerter,
build::Build,
builder::Builder,
deployment::Deployment,
permission::PermissionLevel,
procedure::Procedure,
repo::Repo,
server::Server,
stack::Stack,
sync::ResourceSync,
update::{Update, UpdateListItem},
user::User,
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{
config::core_config,
permission::{get_check_permissions, get_resource_ids_for_user},
permission::{
check_user_target_access, user_resource_target_query,
},
state::db_client,
};
@@ -41,121 +32,8 @@ impl Resolve<ReadArgs> for ListUpdates {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListUpdatesResponse> {
let query = if user.admin || core_config().transparent_mode {
self.query
} else {
let server_query = get_resource_ids_for_user::<Server>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Server", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Server" });
let deployment_query =
get_resource_ids_for_user::<Deployment>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Deployment", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Deployment" });
let stack_query = get_resource_ids_for_user::<Stack>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Stack", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Stack" });
let build_query = get_resource_ids_for_user::<Build>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Build", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Build" });
let repo_query = get_resource_ids_for_user::<Repo>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Repo", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Repo" });
let procedure_query =
get_resource_ids_for_user::<Procedure>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Procedure", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Procedure" });
let action_query = get_resource_ids_for_user::<Action>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Action", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Action" });
let builder_query = get_resource_ids_for_user::<Builder>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Builder", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Builder" });
let alerter_query = get_resource_ids_for_user::<Alerter>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Alerter", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Alerter" });
let resource_sync_query = get_resource_ids_for_user::<
ResourceSync,
>(user)
.await?
.map(|ids| {
doc! {
"target.type": "ResourceSync", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ResourceSync" });
let mut query = self.query.unwrap_or_default();
query.extend(doc! {
"$or": [
server_query,
deployment_query,
stack_query,
build_query,
repo_query,
procedure_query,
action_query,
alerter_query,
builder_query,
resource_sync_query,
]
});
query.into()
};
) -> mogh_error::Result<ListUpdatesResponse> {
let query = user_resource_target_query(user, self.query).await?;
let usernames = find_collect(&db_client().users, None, None)
.await
@@ -214,7 +92,7 @@ impl Resolve<ReadArgs> for GetUpdate {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
let update = find_one_by_id(&db_client().updates, &self.id)
.await
.context("failed to query to db")?
@@ -222,93 +100,12 @@ impl Resolve<ReadArgs> for GetUpdate {
if user.admin || core_config().transparent_mode {
return Ok(update);
}
match &update.target {
ResourceTarget::System(_) => {
return Err(
anyhow!("user must be admin to view system updates").into(),
);
}
ResourceTarget::Server(id) => {
get_check_permissions::<Server>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Deployment(id) => {
get_check_permissions::<Deployment>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Build(id) => {
get_check_permissions::<Build>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Repo(id) => {
get_check_permissions::<Repo>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Builder(id) => {
get_check_permissions::<Builder>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Alerter(id) => {
get_check_permissions::<Alerter>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Procedure(id) => {
get_check_permissions::<Procedure>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Action(id) => {
get_check_permissions::<Action>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::ResourceSync(id) => {
get_check_permissions::<ResourceSync>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Stack(id) => {
get_check_permissions::<Stack>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
}
check_user_target_access(
&update.target,
user,
PermissionLevel::Read.into(),
)
.await?;
Ok(update)
}
}

View File

@@ -13,7 +13,7 @@ use komodo_client::{
},
entities::user::{UserConfig, admin_service_user},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{helpers::query::get_user, state::db_client};
@@ -23,7 +23,7 @@ impl Resolve<ReadArgs> for GetUsername {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<GetUsernameResponse> {
) -> mogh_error::Result<GetUsernameResponse> {
if let Some(user) = admin_service_user(&self.user_id) {
return Ok(GetUsernameResponse {
username: user.username,
@@ -53,7 +53,7 @@ impl Resolve<ReadArgs> for FindUser {
async fn resolve(
self,
ReadArgs { user: admin }: &ReadArgs,
) -> serror::Result<FindUserResponse> {
) -> mogh_error::Result<FindUserResponse> {
if !admin.admin {
return Err(anyhow!("This method is admin only.").into());
}
@@ -65,7 +65,7 @@ impl Resolve<ReadArgs> for ListUsers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListUsersResponse> {
) -> mogh_error::Result<ListUsersResponse> {
if !user.admin {
return Err(
anyhow!("this route is only accessable by admins").into(),
@@ -87,7 +87,7 @@ impl Resolve<ReadArgs> for ListApiKeys {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListApiKeysResponse> {
) -> mogh_error::Result<ListApiKeysResponse> {
let api_keys = find_collect(
&db_client().api_keys,
doc! { "user_id": &user.id },
@@ -109,7 +109,7 @@ impl Resolve<ReadArgs> for ListApiKeysForServiceUser {
async fn resolve(
self,
ReadArgs { user: admin }: &ReadArgs,
) -> serror::Result<ListApiKeysForServiceUserResponse> {
) -> mogh_error::Result<ListApiKeysForServiceUserResponse> {
if !admin.admin {
return Err(anyhow!("This method is admin only.").into());
}

View File

@@ -9,7 +9,7 @@ use database::mungos::{
},
};
use komodo_client::api::read::*;
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::state::db_client;
@@ -19,7 +19,7 @@ impl Resolve<ReadArgs> for GetUserGroup {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetUserGroupResponse> {
) -> mogh_error::Result<GetUserGroupResponse> {
let mut filter = match ObjectId::from_str(&self.user_group) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": &self.user_group },
@@ -43,7 +43,7 @@ impl Resolve<ReadArgs> for ListUserGroups {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListUserGroupsResponse> {
) -> mogh_error::Result<ListUserGroupsResponse> {
let mut filter = Document::new();
if !user.admin {
filter.insert("users", &user.id);

View File

@@ -4,7 +4,7 @@ use database::mungos::{
find::find_collect, mongodb::options::FindOptions,
};
use komodo_client::api::read::*;
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{helpers::query::get_variable, state::db_client};
@@ -14,7 +14,7 @@ impl Resolve<ReadArgs> for GetVariable {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetVariableResponse> {
) -> mogh_error::Result<GetVariableResponse> {
let mut variable = get_variable(&self.name).await?;
if !variable.is_secret || user.admin {
return Ok(variable);
@@ -28,7 +28,7 @@ impl Resolve<ReadArgs> for ListVariables {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListVariablesResponse> {
) -> mogh_error::Result<ListVariablesResponse> {
let variables = find_collect(
&db_client().variables,
None,

View File

@@ -1,299 +1,56 @@
use anyhow::Context;
use axum::{Extension, Router, middleware, routing::post};
use komodo_client::{
api::terminal::*,
entities::{
deployment::Deployment, permission::PermissionLevel,
server::Server, stack::Stack, user::User,
},
};
use serror::Json;
use uuid::Uuid;
use komodo_client::{api::terminal::*, entities::user::User};
use mogh_auth_server::middleware::authenticate_request;
use mogh_error::Json;
use crate::{
auth::auth_request, helpers::periphery_client,
permission::get_check_permissions, resource::get,
state::stack_status_cache,
auth::KomodoAuthImpl, helpers::terminal::setup_target_for_user,
};
pub fn router() -> Router {
Router::new()
.route("/execute", post(execute_terminal))
.route("/execute/container", post(execute_container_exec))
.route("/execute/deployment", post(execute_deployment_exec))
.route("/execute/stack", post(execute_stack_exec))
.layer(middleware::from_fn(auth_request))
.layer(middleware::from_fn(
authenticate_request::<KomodoAuthImpl, true>,
))
}
// =================
// ExecuteTerminal
// =================
async fn execute_terminal(
Extension(user): Extension<User>,
Json(request): Json<ExecuteTerminalBody>,
) -> serror::Result<axum::body::Body> {
execute_terminal_inner(Uuid::new_v4(), request, user).await
}
#[instrument(
name = "ExecuteTerminal",
skip(user),
skip_all,
fields(
user_id = user.id,
operator = user.id,
target,
terminal,
init = format!("{init:?}")
)
)]
async fn execute_terminal_inner(
req_id: Uuid,
ExecuteTerminalBody {
server,
async fn execute_terminal(
Extension(user): Extension<User>,
Json(ExecuteTerminalBody {
target,
terminal,
command,
}: ExecuteTerminalBody,
user: User,
) -> serror::Result<axum::body::Body> {
info!("/terminal/execute request | user: {}", user.username);
let res = async {
let server = get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Read.terminal(),
)
.await?;
let periphery = periphery_client(&server)?;
let stream = periphery
.execute_terminal(terminal, command)
.await
.context("Failed to execute command on periphery")?;
anyhow::Ok(stream)
}
.await;
let stream = match res {
Ok(stream) => stream,
Err(e) => {
warn!("/terminal/execute request {req_id} error: {e:#}");
return Err(e.into());
}
};
Ok(axum::body::Body::from_stream(stream.into_line_stream()))
}
// ======================
// ExecuteContainerExec
// ======================
async fn execute_container_exec(
Extension(user): Extension<User>,
Json(request): Json<ExecuteContainerExecBody>,
) -> serror::Result<axum::body::Body> {
execute_container_exec_inner(Uuid::new_v4(), request, user).await
}
#[instrument(
name = "ExecuteContainerExec",
skip(user),
fields(
user_id = user.id,
)
)]
async fn execute_container_exec_inner(
req_id: Uuid,
ExecuteContainerExecBody {
server,
container,
shell,
command,
}: ExecuteContainerExecBody,
user: User,
) -> serror::Result<axum::body::Body> {
init,
}): Json<ExecuteTerminalBody>,
) -> mogh_error::Result<axum::body::Body> {
info!(
"/terminal/execute/container request | user: {}",
user.username
"TERMINAL EXECUTE REQUEST | USER: {} ({})",
user.username, user.id
);
let res = async {
let server = get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Read.terminal(),
)
.await?;
let (target, terminal, periphery) =
setup_target_for_user(target, terminal, init, &user).await?;
let periphery = periphery_client(&server)?;
let stream = periphery
.execute_terminal(target, terminal, command)
.await
.context("Failed to execute command on Terminal")?;
let stream = periphery
.execute_container_exec(container, shell, command)
.await
.context(
"Failed to execute container exec command on periphery",
)?;
anyhow::Ok(stream)
}
.await;
let stream = match res {
Ok(stream) => stream,
Err(e) => {
warn!(
"/terminal/execute/container request {req_id} error: {e:#}"
);
return Err(e.into());
}
};
Ok(axum::body::Body::from_stream(stream.into_line_stream()))
}
// =======================
// ExecuteDeploymentExec
// =======================
async fn execute_deployment_exec(
Extension(user): Extension<User>,
Json(request): Json<ExecuteDeploymentExecBody>,
) -> serror::Result<axum::body::Body> {
execute_deployment_exec_inner(Uuid::new_v4(), request, user).await
}
#[instrument(
name = "ExecuteDeploymentExec",
skip(user),
fields(
user_id = user.id,
)
)]
async fn execute_deployment_exec_inner(
req_id: Uuid,
ExecuteDeploymentExecBody {
deployment,
shell,
command,
}: ExecuteDeploymentExecBody,
user: User,
) -> serror::Result<axum::body::Body> {
info!(
"/terminal/execute/deployment request | user: {}",
user.username
);
let res = async {
let deployment = get_check_permissions::<Deployment>(
&deployment,
&user,
PermissionLevel::Read.terminal(),
)
.await?;
let server = get::<Server>(&deployment.config.server_id).await?;
let periphery = periphery_client(&server)?;
let stream = periphery
.execute_container_exec(deployment.name, shell, command)
.await
.context(
"Failed to execute container exec command on periphery",
)?;
anyhow::Ok(stream)
}
.await;
let stream = match res {
Ok(stream) => stream,
Err(e) => {
warn!(
"/terminal/execute/deployment request {req_id} error: {e:#}"
);
return Err(e.into());
}
};
Ok(axum::body::Body::from_stream(stream.into_line_stream()))
}
// ==================
// ExecuteStackExec
// ==================
async fn execute_stack_exec(
Extension(user): Extension<User>,
Json(request): Json<ExecuteStackExecBody>,
) -> serror::Result<axum::body::Body> {
execute_stack_exec_inner(Uuid::new_v4(), request, user).await
}
#[instrument(
name = "ExecuteStackExec",
skip(user),
fields(
user_id = user.id,
)
)]
async fn execute_stack_exec_inner(
req_id: Uuid,
ExecuteStackExecBody {
stack,
service,
shell,
command,
}: ExecuteStackExecBody,
user: User,
) -> serror::Result<axum::body::Body> {
info!("/terminal/execute/stack request | user: {}", user.username);
let res = async {
let stack = get_check_permissions::<Stack>(
&stack,
&user,
PermissionLevel::Read.terminal(),
)
.await?;
let server = get::<Server>(&stack.config.server_id).await?;
let container = stack_status_cache()
.get(&stack.id)
.await
.context("could not get stack status")?
.curr
.services
.iter()
.find(|s| s.service == service)
.context("could not find service")?
.container
.as_ref()
.context("could not find service container")?
.name
.clone();
let periphery = periphery_client(&server)?;
let stream = periphery
.execute_container_exec(container, shell, command)
.await
.context(
"Failed to execute container exec command on periphery",
)?;
anyhow::Ok(stream)
}
.await;
let stream = match res {
Ok(stream) => stream,
Err(e) => {
warn!("/terminal/execute/stack request {req_id} error: {e:#}");
return Err(e.into());
}
};
Ok(axum::body::Body::from_stream(stream.into_line_stream()))
Ok(axum::body::Body::from_stream(stream))
}

View File

@@ -1,213 +0,0 @@
use std::{collections::VecDeque, time::Instant};
use anyhow::{Context, anyhow};
use axum::{
Extension, Json, Router, extract::Path, middleware, routing::post,
};
use database::mongo_indexed::doc;
use database::mungos::{
by_id::update_one_by_id, mongodb::bson::to_bson,
};
use derive_variants::EnumVariants;
use komodo_client::{
api::user::*,
entities::{api_key::ApiKey, komodo_timestamp, user::User},
};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serde_json::json;
use typeshare::typeshare;
use uuid::Uuid;
use crate::{
auth::auth_request,
helpers::{query::get_user, random_string},
state::db_client,
};
use super::Variant;
pub struct UserArgs {
pub user: User,
}
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
)]
#[args(UserArgs)]
#[response(Response)]
#[error(serror::Error)]
#[serde(tag = "type", content = "params")]
enum UserRequest {
PushRecentlyViewed(PushRecentlyViewed),
SetLastSeenUpdate(SetLastSeenUpdate),
CreateApiKey(CreateApiKey),
DeleteApiKey(DeleteApiKey),
}
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler))
.layer(middleware::from_fn(auth_request))
}
async fn variant_handler(
user: Extension<User>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<axum::response::Response> {
let req: UserRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
}))?;
handler(user, Json(req)).await
}
#[instrument(name = "UserHandler", level = "debug", skip(user))]
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<UserRequest>,
) -> serror::Result<axum::response::Response> {
let timer = Instant::now();
let req_id = Uuid::new_v4();
debug!(
"/user request {req_id} | user: {} ({})",
user.username, user.id
);
let res = request.resolve(&UserArgs { user }).await;
if let Err(e) = &res {
warn!("/user request {req_id} error: {:#}", e.error);
}
let elapsed = timer.elapsed();
debug!("/user request {req_id} | resolve time: {elapsed:?}");
res.map(|res| res.0)
}
const RECENTLY_VIEWED_MAX: usize = 10;
impl Resolve<UserArgs> for PushRecentlyViewed {
#[instrument(
name = "PushRecentlyViewed",
level = "debug",
skip(user)
)]
async fn resolve(
self,
UserArgs { user }: &UserArgs,
) -> serror::Result<PushRecentlyViewedResponse> {
let user = get_user(&user.id).await?;
let (resource_type, id) = self.resource.extract_variant_id();
let update = match user.recents.get(&resource_type) {
Some(recents) => {
let mut recents = recents
.iter()
.filter(|_id| !id.eq(*_id))
.take(RECENTLY_VIEWED_MAX - 1)
.collect::<VecDeque<_>>();
recents.push_front(id);
doc! { format!("recents.{resource_type}"): to_bson(&recents)? }
}
None => {
doc! { format!("recents.{resource_type}"): [id] }
}
};
update_one_by_id(
&db_client().users,
&user.id,
database::mungos::update::Update::Set(update),
None,
)
.await
.with_context(|| {
format!("failed to update recents.{resource_type}")
})?;
Ok(PushRecentlyViewedResponse {})
}
}
impl Resolve<UserArgs> for SetLastSeenUpdate {
#[instrument(
name = "SetLastSeenUpdate",
level = "debug",
skip(user)
)]
async fn resolve(
self,
UserArgs { user }: &UserArgs,
) -> serror::Result<SetLastSeenUpdateResponse> {
update_one_by_id(
&db_client().users,
&user.id,
database::mungos::update::Update::Set(doc! {
"last_update_view": komodo_timestamp()
}),
None,
)
.await
.context("failed to update user last_update_view")?;
Ok(SetLastSeenUpdateResponse {})
}
}
const SECRET_LENGTH: usize = 40;
const BCRYPT_COST: u32 = 10;
impl Resolve<UserArgs> for CreateApiKey {
#[instrument(name = "CreateApiKey", level = "debug", skip(user))]
async fn resolve(
self,
UserArgs { user }: &UserArgs,
) -> serror::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: self.name,
key: key.clone(),
secret: secret_hash,
user_id: user.id.clone(),
created_at: komodo_timestamp(),
expires: self.expires,
};
db_client()
.api_keys
.insert_one(api_key)
.await
.context("failed to create api key on db")?;
Ok(CreateApiKeyResponse { key, secret })
}
}
impl Resolve<UserArgs> for DeleteApiKey {
#[instrument(name = "DeleteApiKey", level = "debug", skip(user))]
async fn resolve(
self,
UserArgs { user }: &UserArgs,
) -> serror::Result<DeleteApiKeyResponse> {
let client = db_client();
let key = client
.api_keys
.find_one(doc! { "key": &self.key })
.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").into());
}
client
.api_keys
.delete_one(doc! { "key": key.key })
.await
.context("failed to delete api key from db")?;
Ok(DeleteApiKeyResponse {})
}
}

View File

@@ -4,61 +4,105 @@ use komodo_client::{
action::Action, permission::PermissionLevel, update::Update,
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{permission::get_check_permissions, resource};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateAction {
#[instrument(name = "CreateAction", skip(user))]
#[instrument(
"CreateAction",
skip_all,
fields(
operator = user.id,
action = self.name,
config = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Action> {
resource::create::<Action>(&self.name, self.config, user).await
) -> mogh_error::Result<Action> {
resource::create::<Action>(&self.name, self.config, None, user)
.await
}
}
impl Resolve<WriteArgs> for CopyAction {
#[instrument(name = "CopyAction", skip(user))]
#[instrument(
"CopyAction",
skip_all,
fields(
operator = user.id,
action = self.name,
copy_action = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Action> {
) -> mogh_error::Result<Action> {
let Action { config, .. } = get_check_permissions::<Action>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::create::<Action>(&self.name, config.into(), user).await
resource::create::<Action>(&self.name, config.into(), None, user)
.await
}
}
impl Resolve<WriteArgs> for UpdateAction {
#[instrument(name = "UpdateAction", skip(user))]
#[instrument(
"UpdateAction",
skip_all,
fields(
operator = user.id,
action = self.id,
update = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Action> {
) -> mogh_error::Result<Action> {
Ok(resource::update::<Action>(&self.id, self.config, user).await?)
}
}
impl Resolve<WriteArgs> for RenameAction {
#[instrument(name = "RenameAction", skip(user))]
#[instrument(
"RenameAction",
skip_all,
fields(
operator = user.id,
action = self.id,
new_name = self.name,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
Ok(resource::rename::<Action>(&self.id, &self.name, user).await?)
}
}
impl Resolve<WriteArgs> for DeleteAction {
#[instrument(name = "DeleteAction", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Action> {
Ok(resource::delete::<Action>(&self.id, args).await?)
#[instrument(
"DeleteAction",
skip_all,
fields(
operator = user.id,
action = self.id
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<Action> {
Ok(resource::delete::<Action>(&self.id, user).await?)
}
}

View File

@@ -0,0 +1,41 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use database::mungos::mongodb::bson::{doc, oid::ObjectId};
use komodo_client::{api::write::CloseAlert, entities::NoData};
use mogh_error::AddStatusCodeError;
use mogh_resolver::Resolve;
use reqwest::StatusCode;
use crate::{api::write::WriteArgs, state::db_client};
impl Resolve<WriteArgs> for CloseAlert {
#[instrument(
"CloseAlert",
skip_all,
fields(
operator = admin.id,
alert_id = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> Result<Self::Response, Self::Error> {
if !admin.admin {
return Err(
anyhow!("This call is admin only")
.status_code(StatusCode::FORBIDDEN),
);
}
db_client()
.alerts
.update_one(
doc! { "_id": ObjectId::from_str(&self.id)? },
doc! { "$set": { "resolved": true } },
)
.await
.context("Failed to close Alert on database")?;
Ok(NoData {})
}
}

View File

@@ -4,54 +4,87 @@ use komodo_client::{
alerter::Alerter, permission::PermissionLevel, update::Update,
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{permission::get_check_permissions, resource};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateAlerter {
#[instrument(name = "CreateAlerter", skip(user))]
#[instrument(
"CreateAlerter",
skip_all,
fields(
operator = user.id,
alerter = self.name,
config = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Alerter> {
resource::create::<Alerter>(&self.name, self.config, user).await
) -> mogh_error::Result<Alerter> {
resource::create::<Alerter>(&self.name, self.config, None, user)
.await
}
}
impl Resolve<WriteArgs> for CopyAlerter {
#[instrument(name = "CopyAlerter", skip(user))]
#[instrument(
"CopyAlerter",
skip_all,
fields(
operator = user.id,
alerter = self.name,
copy_alerter = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Alerter> {
) -> mogh_error::Result<Alerter> {
let Alerter { config, .. } = get_check_permissions::<Alerter>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::create::<Alerter>(&self.name, config.into(), user).await
resource::create::<Alerter>(&self.name, config.into(), None, user)
.await
}
}
impl Resolve<WriteArgs> for DeleteAlerter {
#[instrument(name = "DeleteAlerter", skip(args))]
#[instrument(
"DeleteAlerter",
skip_all,
fields(
operator = user.id,
alerter = self.id,
)
)]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<Alerter> {
Ok(resource::delete::<Alerter>(&self.id, args).await?)
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<Alerter> {
Ok(resource::delete::<Alerter>(&self.id, user).await?)
}
}
impl Resolve<WriteArgs> for UpdateAlerter {
#[instrument(name = "UpdateAlerter", skip(user))]
#[instrument(
"UpdateAlerter",
skip_all,
fields(
operator = user.id,
alerter = self.id,
update = serde_json::to_string(&self.config).unwrap()
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Alerter> {
) -> mogh_error::Result<Alerter> {
Ok(
resource::update::<Alerter>(&self.id, self.config, user)
.await?,
@@ -60,11 +93,19 @@ impl Resolve<WriteArgs> for UpdateAlerter {
}
impl Resolve<WriteArgs> for RenameAlerter {
#[instrument(name = "RenameAlerter", skip(user))]
#[instrument(
"RenameAlerter",
skip_all,
fields(
operator = user.id,
alerter = self.id,
new_name = self.name,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
Ok(resource::rename::<Alerter>(&self.id, &self.name, user).await?)
}
}

View File

@@ -1,65 +1,79 @@
use std::{path::PathBuf, str::FromStr, time::Duration};
use std::{path::PathBuf, time::Duration};
use anyhow::{Context, anyhow};
use database::mongo_indexed::doc;
use database::mungos::mongodb::bson::to_document;
use database::{
mongo_indexed::doc, mungos::mongodb::bson::oid::ObjectId,
};
use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
FileContents, NoData, Operation, RepoExecutionArgs,
all_logs_success,
build::{Build, BuildInfo, PartialBuildConfig},
build::{Build, BuildInfo},
builder::{Builder, BuilderConfig},
config::core::CoreConfig,
permission::PermissionLevel,
repo::Repo,
server::ServerState,
update::Update,
},
};
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
use mogh_resolver::Resolve;
use periphery_client::api::build::{
GetDockerfileContentsOnHost, WriteDockerfileContentsToHost,
};
use periphery_client::{
PeripheryClient,
api::build::{
GetDockerfileContentsOnHost, WriteDockerfileContentsToHost,
},
};
use resolver_api::Resolve;
use tokio::fs;
use crate::{
config::core_config,
connection::PeripheryConnectionArgs,
helpers::{
git_token, periphery_client,
query::get_server_with_state,
update::{add_update, make_update},
},
periphery::PeripheryClient,
permission::get_check_permissions,
resource,
state::{db_client, github_client},
state::db_client,
};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateBuild {
#[instrument(name = "CreateBuild", skip(user))]
#[instrument(
"CreateBuild",
skip_all,
fields(
operator = user.id,
build = self.name,
config = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Build> {
resource::create::<Build>(&self.name, self.config, user).await
) -> mogh_error::Result<Build> {
resource::create::<Build>(&self.name, self.config, None, user)
.await
}
}
impl Resolve<WriteArgs> for CopyBuild {
#[instrument(name = "CopyBuild", skip(user))]
#[instrument(
"CopyBuild",
skip_all,
fields(
operator = user.id,
build = self.name,
copy_build = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Build> {
) -> mogh_error::Result<Build> {
let Build { mut config, .. } = get_check_permissions::<Build>(
&self.id,
user,
@@ -68,40 +82,77 @@ impl Resolve<WriteArgs> for CopyBuild {
.await?;
// reset version to 0.0.0
config.version = Default::default();
resource::create::<Build>(&self.name, config.into(), user).await
resource::create::<Build>(&self.name, config.into(), None, user)
.await
}
}
impl Resolve<WriteArgs> for DeleteBuild {
#[instrument(name = "DeleteBuild", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Build> {
Ok(resource::delete::<Build>(&self.id, args).await?)
#[instrument(
"DeleteBuild",
skip_all,
fields(
operator = user.id,
build = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<Build> {
Ok(resource::delete::<Build>(&self.id, user).await?)
}
}
impl Resolve<WriteArgs> for UpdateBuild {
#[instrument(name = "UpdateBuild", skip(user))]
#[instrument(
"UpdateBuild",
skip_all,
fields(
operator = user.id,
build = self.id,
update = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Build> {
) -> mogh_error::Result<Build> {
Ok(resource::update::<Build>(&self.id, self.config, user).await?)
}
}
impl Resolve<WriteArgs> for RenameBuild {
#[instrument(name = "RenameBuild", skip(user))]
#[instrument(
"RenameBuild",
skip_all,
fields(
operator = user.id,
build = self.id,
new_name = self.name,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
Ok(resource::rename::<Build>(&self.id, &self.name, user).await?)
}
}
impl Resolve<WriteArgs> for WriteBuildFileContents {
#[instrument(name = "WriteBuildFileContents", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
#[instrument(
"WriteBuildFileContents",
skip_all,
fields(
operator = args.user.id,
build = self.build,
)
)]
async fn resolve(
self,
args: &WriteArgs,
) -> mogh_error::Result<Update> {
let build = get_check_permissions::<Build>(
&self.build,
&args.user,
@@ -172,12 +223,13 @@ impl Resolve<WriteArgs> for WriteBuildFileContents {
}
}
#[instrument("WriteDockerfileContentsGit", skip_all)]
async fn write_dockerfile_contents_git(
req: WriteBuildFileContents,
args: &WriteArgs,
build: Build,
mut update: Update,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
let WriteBuildFileContents { build: _, contents } = req;
let mut repo_args: RepoExecutionArgs = if !build
@@ -269,8 +321,9 @@ async fn write_dockerfile_contents_git(
return Ok(update);
}
if let Err(e) =
fs::write(&full_path, &contents).await.with_context(|| {
if let Err(e) = mogh_secret_file::write_async(&full_path, &contents)
.await
.with_context(|| {
format!("Failed to write dockerfile contents to {full_path:?}")
})
{
@@ -317,15 +370,10 @@ async fn write_dockerfile_contents_git(
}
impl Resolve<WriteArgs> for RefreshBuildCache {
#[instrument(
name = "RefreshBuildCache",
level = "debug",
skip(user)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
) -> mogh_error::Result<NoData> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// build should be able to do this.
let build = get_check_permissions::<Build>(
@@ -345,23 +393,28 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
None
};
let (
remote_path,
remote_contents,
remote_error,
latest_hash,
latest_message,
) = if build.config.files_on_host {
let RemoteDockerfileContents {
path,
contents,
error,
hash,
message,
} = if build.config.files_on_host {
// =============
// FILES ON HOST
// =============
match get_on_host_dockerfile(&build).await {
Ok(FileContents { path, contents }) => {
(Some(path), Some(contents), None, None, None)
}
Err(e) => {
(None, None, Some(format_serror(&e.into())), None, None)
RemoteDockerfileContents {
path: Some(path),
contents: Some(contents),
..Default::default()
}
}
Err(e) => RemoteDockerfileContents {
error: Some(format_serror(&e.into())),
..Default::default()
},
}
} else if let Some(repo) = &repo {
let Some(res) = get_git_remote(&build, repo.into()).await?
@@ -381,7 +434,7 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
// =============
// UI BASED FILE
// =============
(None, None, None, None, None)
RemoteDockerfileContents::default()
};
let info = BuildInfo {
@@ -389,11 +442,11 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
built_hash: build.info.built_hash,
built_message: build.info.built_message,
built_contents: build.info.built_contents,
remote_path,
remote_contents,
remote_error,
latest_hash,
latest_message,
remote_path: path,
remote_contents: contents,
remote_error: error,
latest_hash: hash,
latest_message: message,
};
let info = to_document(&info)
@@ -428,13 +481,26 @@ async fn get_on_host_periphery(
Err(anyhow!("Files on host doesn't work with AWS builder"))
}
BuilderConfig::Url(config) => {
// TODO: Ensure connection is actually established.
// Builder id no good because it may be active for multiple connections.
let periphery = PeripheryClient::new(
config.address,
config.passkey,
Duration::from_secs(3),
);
periphery.health_check().await?;
Ok(periphery)
PeripheryConnectionArgs::from_url_builder(
&ObjectId::new().to_hex(),
&config,
),
config.insecure_tls,
)
.await?;
// Poll for connection to be estalished
let mut err = None;
for _ in 0..10 {
tokio::time::sleep(Duration::from_secs(1)).await;
match periphery.health_check().await {
Ok(_) => return Ok(periphery),
Err(e) => err = Some(e),
};
}
Err(err.context("Missing error")?)
}
BuilderConfig::Server(config) => {
if config.server_id.is_empty() {
@@ -449,7 +515,7 @@ async fn get_on_host_periphery(
"Builder server is disabled or not reachable"
));
};
periphery_client(&server)
periphery_client(&server).await
}
}
}
@@ -472,15 +538,7 @@ async fn get_on_host_dockerfile(
async fn get_git_remote(
build: &Build,
mut clone_args: RepoExecutionArgs,
) -> anyhow::Result<
Option<(
Option<String>,
Option<String>,
Option<String>,
Option<String>,
Option<String>,
)>,
> {
) -> anyhow::Result<Option<RemoteDockerfileContents>> {
if clone_args.provider.is_empty() {
// Nothing to do here
return Ok(None);
@@ -507,10 +565,19 @@ async fn get_git_remote(
access_token,
)
.await
.context("failed to clone build repo")?;
.context("Failed to clone Build repo")?;
let relative_path = PathBuf::from_str(&build.config.build_path)
.context("Invalid build path")?
// Ensure clone / pull successful,
// propogate error log -> 'errored' and return.
if let Some(failure) = res.logs.iter().find(|log| !log.success) {
return Ok(Some(RemoteDockerfileContents {
path: Some(format!("Failed at: {}", failure.stage)),
error: Some(failure.combined()),
..Default::default()
}));
}
let relative_path = PathBuf::from(&build.config.build_path)
.join(&build.config.dockerfile_path);
let full_path = repo_path.join(&relative_path);
@@ -521,209 +588,20 @@ async fn get_git_remote(
Ok(contents) => (Some(contents), None),
Err(e) => (None, Some(format_serror(&e.into()))),
};
Ok(Some((
Some(relative_path.display().to_string()),
Ok(Some(RemoteDockerfileContents {
path: Some(relative_path.display().to_string()),
contents,
error,
res.commit_hash,
res.commit_message,
)))
hash: res.commit_hash,
message: res.commit_message,
}))
}
impl Resolve<WriteArgs> for CreateBuildWebhook {
#[instrument(name = "CreateBuildWebhook", skip(args))]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<CreateBuildWebhookResponse> {
let Some(github) = github_client() else {
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let WriteArgs { user } = args;
let build = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Write.into(),
)
.await?;
if build.config.repo.is_empty() {
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = build.config.repo.split('/');
let owner = split.next().context("Build repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
split.next().context("Build repo has no repo after the /")?;
let github_repos = github.repos();
// First make sure the webhook isn't already created (inactive ones are ignored)
let webhooks = github_repos
.list_all_webhooks(owner, repo)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
webhook_secret,
..
} = core_config();
let webhook_secret = if build.config.webhook_secret.is_empty() {
webhook_secret
} else {
&build.config.webhook_secret
};
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = format!("{host}/listener/github/build/{}", build.id);
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
return Ok(NoData {});
}
}
// Now good to create the webhook
let request = ReposCreateWebhookRequest {
active: Some(true),
config: Some(ReposCreateWebhookRequestConfig {
url,
secret: webhook_secret.to_string(),
content_type: String::from("json"),
insecure_ssl: None,
digest: Default::default(),
token: Default::default(),
}),
events: vec![String::from("push")],
name: String::from("web"),
};
github_repos
.create_webhook(owner, repo, &request)
.await
.context("failed to create webhook")?;
if !build.config.webhook_enabled {
UpdateBuild {
id: build.id,
config: PartialBuildConfig {
webhook_enabled: Some(true),
..Default::default()
},
}
.resolve(args)
.await
.map_err(|e| e.error)
.context("failed to update build to enable webhook")?;
}
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteBuildWebhook {
#[instrument(name = "DeleteBuildWebhook", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteBuildWebhookResponse> {
let Some(github) = github_client() else {
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let build = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Write.into(),
)
.await?;
if build.config.git_provider != "github.com" {
return Err(
anyhow!("Can only manage github.com repo webhooks").into(),
);
}
if build.config.repo.is_empty() {
return Err(
anyhow!("No repo configured, can't delete webhook").into(),
);
}
let mut split = build.config.repo.split('/');
let owner = split.next().context("Build repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
split.next().context("Build repo has no repo after the /")?;
let github_repos = github.repos();
let webhooks = github_repos
.list_all_webhooks(owner, repo)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = format!("{host}/listener/github/build/{}", build.id);
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
github_repos
.delete_webhook(owner, repo, webhook.id)
.await
.context("failed to delete webhook")?;
return Ok(NoData {});
}
}
// No webhook to delete, all good
Ok(NoData {})
}
#[derive(Default)]
pub struct RemoteDockerfileContents {
pub path: Option<String>,
pub contents: Option<String>,
pub error: Option<String>,
pub hash: Option<String>,
pub message: Option<String>,
}

View File

@@ -4,54 +4,87 @@ use komodo_client::{
builder::Builder, permission::PermissionLevel, update::Update,
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{permission::get_check_permissions, resource};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateBuilder {
#[instrument(name = "CreateBuilder", skip(user))]
#[instrument(
"CreateBuilder",
skip_all,
fields(
operator = user.id,
builder = self.name,
config = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Builder> {
resource::create::<Builder>(&self.name, self.config, user).await
) -> mogh_error::Result<Builder> {
resource::create::<Builder>(&self.name, self.config, None, user)
.await
}
}
impl Resolve<WriteArgs> for CopyBuilder {
#[instrument(name = "CopyBuilder", skip(user))]
#[instrument(
"CopyBuilder",
skip_all,
fields(
operator = user.id,
builder = self.name,
copy_builder = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Builder> {
) -> mogh_error::Result<Builder> {
let Builder { config, .. } = get_check_permissions::<Builder>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::create::<Builder>(&self.name, config.into(), user).await
resource::create::<Builder>(&self.name, config.into(), None, user)
.await
}
}
impl Resolve<WriteArgs> for DeleteBuilder {
#[instrument(name = "DeleteBuilder", skip(args))]
#[instrument(
"DeleteBuilder",
skip_all,
fields(
operator = user.id,
builder = self.id,
)
)]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<Builder> {
Ok(resource::delete::<Builder>(&self.id, args).await?)
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<Builder> {
Ok(resource::delete::<Builder>(&self.id, user).await?)
}
}
impl Resolve<WriteArgs> for UpdateBuilder {
#[instrument(name = "UpdateBuilder", skip(user))]
#[instrument(
"UpdateBuilder",
skip_all,
fields(
operator = user.id,
builder = self.id,
update = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Builder> {
) -> mogh_error::Result<Builder> {
Ok(
resource::update::<Builder>(&self.id, self.config, user)
.await?,
@@ -60,11 +93,19 @@ impl Resolve<WriteArgs> for UpdateBuilder {
}
impl Resolve<WriteArgs> for RenameBuilder {
#[instrument(name = "RenameBuilder", skip(user))]
#[instrument(
"RenameBuilder",
skip_all,
fields(
operator = user.id,
builder = self.id,
new_name = self.name
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
Ok(resource::rename::<Builder>(&self.id, &self.name, user).await?)
}
}

View File

@@ -1,54 +1,90 @@
use std::sync::OnceLock;
use anyhow::{Context, anyhow};
use database::mungos::{by_id::update_one_by_id, mongodb::bson::doc};
use futures_util::{StreamExt as _, stream::FuturesOrdered};
use komodo_client::{
api::write::*,
api::{execute::Deploy, write::*},
entities::{
Operation,
Operation, ResourceTarget, SwarmOrServer,
alert::{Alert, AlertData, SeverityLevel},
deployment::{
Deployment, DeploymentImage, DeploymentState,
PartialDeploymentConfig, RestartMode,
Deployment, DeploymentImage, DeploymentInfo, DeploymentState,
PartialDeploymentConfig, RestartMode, extract_registry_domain,
},
docker::container::RestartPolicyNameEnum,
komodo_timestamp,
komodo_timestamp, optional_string,
permission::PermissionLevel,
server::{Server, ServerState},
to_container_compatible_name,
update::Update,
user::{auto_redeploy_user, system_user},
},
};
use mogh_cache::SetCache;
use mogh_resolver::Resolve;
use periphery_client::api::{self, container::InspectContainer};
use resolver_api::Resolve;
use crate::{
alert::send_alerts,
api::execute::{self, ExecuteRequest},
helpers::{
periphery_client,
query::get_deployment_state,
query::{get_deployment_state, get_swarm_or_server},
registry_token,
update::{add_update, make_update},
},
permission::get_check_permissions,
resource,
state::{action_states, db_client, server_status_cache},
resource::{
self, list_full_for_user_using_pattern,
setup_deployment_execution,
},
state::{
action_states, db_client, deployment_status_cache,
image_digest_cache, server_status_cache,
},
};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateDeployment {
#[instrument(name = "CreateDeployment", skip(user))]
#[instrument(
"CreateDeployment",
skip_all,
fields(
operator = user.id,
deployment = self.name,
config = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
resource::create::<Deployment>(&self.name, self.config, user)
.await
) -> mogh_error::Result<Deployment> {
resource::create::<Deployment>(
&self.name,
self.config,
None,
user,
)
.await
}
}
impl Resolve<WriteArgs> for CopyDeployment {
#[instrument(name = "CopyDeployment", skip(user))]
#[instrument(
"CopyDeployment",
skip_all,
fields(
operator = user.id,
deployment = self.name,
copy_deployment = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
) -> mogh_error::Result<Deployment> {
let Deployment { config, .. } =
get_check_permissions::<Deployment>(
&self.id,
@@ -56,17 +92,30 @@ impl Resolve<WriteArgs> for CopyDeployment {
PermissionLevel::Read.into(),
)
.await?;
resource::create::<Deployment>(&self.name, config.into(), user)
.await
resource::create::<Deployment>(
&self.name,
config.into(),
None,
user,
)
.await
}
}
impl Resolve<WriteArgs> for CreateDeploymentFromContainer {
#[instrument(name = "CreateDeploymentFromContainer", skip(user))]
#[instrument(
"CreateDeploymentFromContainer",
skip_all,
fields(
operator = user.id,
server = self.server,
deployment = self.name,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
) -> mogh_error::Result<Deployment> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -85,7 +134,8 @@ impl Resolve<WriteArgs> for CreateDeploymentFromContainer {
.into(),
);
}
let container = periphery_client(&server)?
let container = periphery_client(&server)
.await?
.request(InspectContainer {
name: self.name.clone(),
})
@@ -149,39 +199,87 @@ impl Resolve<WriteArgs> for CreateDeploymentFromContainer {
});
}
resource::create::<Deployment>(&self.name, config, user).await
resource::create::<Deployment>(&self.name, config, None, user)
.await
}
}
impl Resolve<WriteArgs> for DeleteDeployment {
#[instrument(name = "DeleteDeployment", skip(args))]
#[instrument(
"DeleteDeployment",
skip_all,
fields(
operator = user.id,
deployment = self.id
)
)]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<Deployment> {
Ok(resource::delete::<Deployment>(&self.id, args).await?)
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<Deployment> {
Ok(resource::delete::<Deployment>(&self.id, user).await?)
}
}
impl Resolve<WriteArgs> for UpdateDeployment {
#[instrument(name = "UpdateDeployment", skip(user))]
#[instrument(
"UpdateDeployment",
skip_all,
fields(
operator = user.id,
deployment = self.id,
update = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
Ok(
) -> mogh_error::Result<Deployment> {
// If the update changes image,
// also update the stored latest image digest.
let image_update = self
.config
.image
.as_ref()
.map(|image| image.as_image().is_some())
.unwrap_or_default();
let deployment =
resource::update::<Deployment>(&self.id, self.config, user)
.await?,
)
.await?;
if image_update {
tokio::spawn(async move {
let _ = (CheckDeploymentForUpdate {
deployment: self.id,
skip_auto_update: false,
wait_for_auto_update: false,
})
.resolve(&WriteArgs {
user: system_user().to_owned(),
})
.await;
});
}
Ok(deployment)
}
}
impl Resolve<WriteArgs> for RenameDeployment {
#[instrument(name = "RenameDeployment", skip(user))]
#[instrument(
"RenameDeployment",
skip_all,
fields(
operator = user.id,
deployment = self.id,
new_name = self.name,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
let deployment = get_check_permissions::<Deployment>(
&self.id,
user,
@@ -231,7 +329,8 @@ impl Resolve<WriteArgs> for RenameDeployment {
if container_state != DeploymentState::NotDeployed {
let server =
resource::get::<Server>(&deployment.config.server_id).await?;
let log = periphery_client(&server)?
let log = periphery_client(&server)
.await?
.request(api::container::RenameContainer {
curr_name: deployment.name.clone(),
new_name: name.clone(),
@@ -254,3 +353,312 @@ impl Resolve<WriteArgs> for RenameDeployment {
Ok(update)
}
}
//
impl Resolve<WriteArgs> for CheckDeploymentForUpdate {
#[instrument(
"CheckDeploymentForUpdate",
skip_all,
fields(
operator = user.id,
deployment = self.deployment,
skip_auto_update = self.skip_auto_update,
wait_for_auto_update = self.wait_for_auto_update,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<Self::Response> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// deployment should be able to do this.
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
user,
PermissionLevel::Execute.into(),
)
.await?;
swarm_or_server.verify_has_target()?;
check_deployment_for_update_inner(
deployment,
&swarm_or_server,
self.skip_auto_update,
self.wait_for_auto_update,
)
.await
.map_err(Into::into)
}
}
/// If it goes down the "update available" path,
/// only send alert if deployment id is not in this cache.
/// If alert is sent, add ID to cache.
/// If later it goes down non "update available" path,
/// remove the id from cache, so next time it does another alert
/// will be sent.
fn deployment_alert_sent_cache() -> &'static SetCache<String> {
static CACHE: OnceLock<SetCache<String>> = OnceLock::new();
CACHE.get_or_init(Default::default)
}
/// Checks remote registry for latest image digest,
/// and saves it to database associated with the deployment.
///
/// Returns true if update is available and auto deploy is false.
/// If auto deploy is true, this will deploy.
#[instrument(
"CheckDeploymentForUpdateInner",
skip_all,
fields(
deployment = deployment.id,
skip_auto_update,
wait_for_auto_update,
)
)]
pub async fn check_deployment_for_update_inner(
deployment: Deployment,
swarm_or_server: &SwarmOrServer,
skip_auto_update: bool,
// Otherwise spawns task to run in background
wait_for_auto_update: bool,
) -> anyhow::Result<CheckDeploymentForUpdateResponse> {
let alert_cache = deployment_alert_sent_cache();
let (image, account, token) = match &deployment.config.image {
DeploymentImage::Image { image } => {
if image.contains('@') {
// Images with a hardcoded digest can't have update.
return Ok(CheckDeploymentForUpdateResponse {
deployment: deployment.id,
update_available: false,
});
}
let domain = extract_registry_domain(&image)?;
let account =
optional_string(&deployment.config.image_registry_account);
let token = if let Some(account) = &account {
registry_token(&domain, account).await?
} else {
None
};
(image, account, token)
}
DeploymentImage::Build { .. } => {
alert_cache.remove(&deployment.id).await;
// This method not used for build based deployments
// as deployed version vs built version can be inferred from Updates.
return Ok(CheckDeploymentForUpdateResponse {
deployment: deployment.id,
update_available: false,
});
}
};
let latest_digest = image_digest_cache()
.get(&swarm_or_server, &image, account, token)
.await?;
resource::update_info::<Deployment>(
&deployment.id,
&DeploymentInfo {
latest_image_digest: latest_digest.clone(),
},
)
.await?;
let Some((state, Some(current_digests))) =
deployment_status_cache()
.get(&deployment.id)
.await
.map(|s| (s.curr.state, s.curr.image_digests.clone()))
else {
alert_cache.remove(&deployment.id).await;
return Ok(CheckDeploymentForUpdateResponse {
deployment: deployment.id,
update_available: false,
});
};
// If not running or latest digest matches current, early return
if !matches!(state, DeploymentState::Running)
|| !latest_digest.update_available(&current_digests)
{
alert_cache.remove(&deployment.id).await;
return Ok(CheckDeploymentForUpdateResponse {
deployment: deployment.id,
update_available: false,
});
}
if !skip_auto_update && deployment.config.auto_update {
// Trigger deploy + alert
// Conservatively remove from alert cache so 'skip_auto_update'
// doesn't cause alerts not to be sent on subsequent calls.
alert_cache.remove(&deployment.id).await;
let swarm_id = swarm_or_server.swarm_id().map(str::to_string);
let swarm_name = swarm_or_server.swarm_name().map(str::to_string);
let server_id = swarm_or_server.server_id().map(str::to_string);
let server_name =
swarm_or_server.server_name().map(str::to_string);
let id = deployment.id.clone();
let name = deployment.name.clone();
let image = image.clone();
let run = async move {
match execute::inner_handler(
ExecuteRequest::Deploy(Deploy {
deployment: name.clone(),
stop_signal: None,
stop_time: None,
}),
auto_redeploy_user().to_owned(),
)
.await
{
Ok(_) => {
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
ts,
resolved: true,
resolved_ts: ts.into(),
level: SeverityLevel::Ok,
target: ResourceTarget::Deployment(id.clone()),
data: AlertData::DeploymentAutoUpdated {
id,
name,
swarm_id,
swarm_name,
server_id,
server_name,
image,
},
};
let res = db_client().alerts.insert_one(&alert).await;
if let Err(e) = res {
error!(
"Failed to record DeploymentAutoUpdated to db | {e:#}"
);
}
send_alerts(&[alert]).await;
}
Err(e) => {
warn!("Failed to auto update Deployment {name} | {e:#}",)
}
}
};
if wait_for_auto_update {
run.await
} else {
tokio::spawn(run);
}
} else {
// Avoid spamming alerts
if alert_cache.contains(&deployment.id).await {
return Ok(CheckDeploymentForUpdateResponse {
deployment: deployment.id,
update_available: true,
});
}
alert_cache.insert(deployment.id.clone()).await;
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
ts,
resolved: true,
resolved_ts: ts.into(),
level: SeverityLevel::Ok,
target: ResourceTarget::Deployment(deployment.id.clone()),
data: AlertData::DeploymentImageUpdateAvailable {
id: deployment.id.clone(),
name: deployment.name.clone(),
swarm_id: swarm_or_server.swarm_id().map(str::to_string),
swarm_name: swarm_or_server.swarm_name().map(str::to_string),
server_id: swarm_or_server.server_id().map(str::to_string),
server_name: swarm_or_server
.server_name()
.map(str::to_string),
image: image.clone(),
},
};
let res = db_client().alerts.insert_one(&alert).await;
if let Err(e) = res {
error!(
"Failed to record DeploymentImageUpdateAvailable to db | {e:#}"
);
}
send_alerts(&[alert]).await;
}
Ok(CheckDeploymentForUpdateResponse {
deployment: deployment.id,
update_available: !deployment.config.auto_update,
})
}
//
impl Resolve<WriteArgs> for BatchCheckDeploymentForUpdate {
#[instrument(
"BatchCheckDeploymentForUpdate",
skip_all,
fields(
operator = user.id,
pattern = self.pattern,
skip_auto_update = self.skip_auto_update,
wait_for_auto_update = self.wait_for_auto_update,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> Result<Self::Response, Self::Error> {
let deployments = list_full_for_user_using_pattern::<Deployment>(
&self.pattern,
Default::default(),
user,
PermissionLevel::Execute.into(),
&[],
)
.await?;
let res = deployments
.into_iter()
.map(|deployment| async move {
let swarm_or_server = get_swarm_or_server(
&deployment.config.swarm_id,
&deployment.config.server_id,
)
.await?;
swarm_or_server.verify_has_target().map_err(|e| e.error)?;
check_deployment_for_update_inner(
deployment,
&swarm_or_server,
self.skip_auto_update,
self.wait_for_auto_update,
)
.await
})
.collect::<FuturesOrdered<_>>()
.collect::<Vec<_>>()
.await
.into_iter()
.filter_map(|res| {
res
.inspect_err(|e| {
warn!(
"Failed to check deployment for update in batch run | {e:#}"
)
})
.ok()
})
.collect();
Ok(res)
}
}

View File

@@ -1,28 +1,30 @@
use std::time::Instant;
use anyhow::Context;
use axum::{
Extension, Router, extract::Path, middleware, routing::post,
};
use derive_variants::{EnumVariants, ExtractVariant};
use komodo_client::{api::write::*, entities::user::User};
use resolver_api::Resolve;
use response::Response;
use mogh_auth_server::middleware::authenticate_request;
use mogh_error::Json;
use mogh_error::Response;
use mogh_resolver::Resolve;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serror::Json;
use strum::Display;
use strum::EnumDiscriminants;
use typeshare::typeshare;
use uuid::Uuid;
use crate::auth::auth_request;
use crate::auth::KomodoAuthImpl;
use super::Variant;
mod action;
mod alert;
mod alerter;
mod build;
mod builder;
mod deployment;
mod onboarding;
mod permissions;
mod procedure;
mod provider;
@@ -31,56 +33,43 @@ mod resource;
mod server;
mod service_user;
mod stack;
mod swarm;
mod sync;
mod tag;
mod terminal;
mod user;
mod user_group;
mod variable;
pub use {
deployment::check_deployment_for_update_inner,
stack::check_stack_for_update_inner,
};
pub struct WriteArgs {
pub user: User,
}
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants,
)]
#[variant_derive(Debug)]
#[strum_discriminants(name(WriteRequestVariant), derive(Display))]
#[args(WriteArgs)]
#[response(Response)]
#[error(serror::Error)]
#[error(mogh_error::Error)]
#[serde(tag = "type", content = "params")]
pub enum WriteRequest {
// ==== USER ====
CreateLocalUser(CreateLocalUser),
UpdateUserUsername(UpdateUserUsername),
UpdateUserPassword(UpdateUserPassword),
DeleteUser(DeleteUser),
// ==== SERVICE USER ====
CreateServiceUser(CreateServiceUser),
UpdateServiceUserDescription(UpdateServiceUserDescription),
CreateApiKeyForServiceUser(CreateApiKeyForServiceUser),
DeleteApiKeyForServiceUser(DeleteApiKeyForServiceUser),
// ==== USER GROUP ====
CreateUserGroup(CreateUserGroup),
RenameUserGroup(RenameUserGroup),
DeleteUserGroup(DeleteUserGroup),
AddUserToUserGroup(AddUserToUserGroup),
RemoveUserFromUserGroup(RemoveUserFromUserGroup),
SetUsersInUserGroup(SetUsersInUserGroup),
SetEveryoneUserGroup(SetEveryoneUserGroup),
// ==== PERMISSIONS ====
UpdateUserAdmin(UpdateUserAdmin),
UpdateUserBasePermissions(UpdateUserBasePermissions),
UpdatePermissionOnResourceType(UpdatePermissionOnResourceType),
UpdatePermissionOnTarget(UpdatePermissionOnTarget),
// ==== RESOURCE ====
UpdateResourceMeta(UpdateResourceMeta),
// ==== SWARM ====
CreateSwarm(CreateSwarm),
CopySwarm(CopySwarm),
DeleteSwarm(DeleteSwarm),
UpdateSwarm(UpdateSwarm),
RenameSwarm(RenameSwarm),
// ==== SERVER ====
CreateServer(CreateServer),
CopyServer(CopyServer),
@@ -88,9 +77,14 @@ pub enum WriteRequest {
UpdateServer(UpdateServer),
RenameServer(RenameServer),
CreateNetwork(CreateNetwork),
UpdateServerPublicKey(UpdateServerPublicKey),
RotateServerKeys(RotateServerKeys),
// ==== TERMINAL ====
CreateTerminal(CreateTerminal),
DeleteTerminal(DeleteTerminal),
DeleteAllTerminals(DeleteAllTerminals),
BatchDeleteAllTerminals(BatchDeleteAllTerminals),
// ==== STACK ====
CreateStack(CreateStack),
@@ -100,8 +94,8 @@ pub enum WriteRequest {
RenameStack(RenameStack),
WriteStackFileContents(WriteStackFileContents),
RefreshStackCache(RefreshStackCache),
CreateStackWebhook(CreateStackWebhook),
DeleteStackWebhook(DeleteStackWebhook),
CheckStackForUpdate(CheckStackForUpdate),
BatchCheckStackForUpdate(BatchCheckStackForUpdate),
// ==== DEPLOYMENT ====
CreateDeployment(CreateDeployment),
@@ -110,6 +104,8 @@ pub enum WriteRequest {
DeleteDeployment(DeleteDeployment),
UpdateDeployment(UpdateDeployment),
RenameDeployment(RenameDeployment),
CheckDeploymentForUpdate(CheckDeploymentForUpdate),
BatchCheckDeploymentForUpdate(BatchCheckDeploymentForUpdate),
// ==== BUILD ====
CreateBuild(CreateBuild),
@@ -119,15 +115,6 @@ pub enum WriteRequest {
RenameBuild(RenameBuild),
WriteBuildFileContents(WriteBuildFileContents),
RefreshBuildCache(RefreshBuildCache),
CreateBuildWebhook(CreateBuildWebhook),
DeleteBuildWebhook(DeleteBuildWebhook),
// ==== BUILDER ====
CreateBuilder(CreateBuilder),
CopyBuilder(CopyBuilder),
DeleteBuilder(DeleteBuilder),
UpdateBuilder(UpdateBuilder),
RenameBuilder(RenameBuilder),
// ==== REPO ====
CreateRepo(CreateRepo),
@@ -136,15 +123,6 @@ pub enum WriteRequest {
UpdateRepo(UpdateRepo),
RenameRepo(RenameRepo),
RefreshRepoCache(RefreshRepoCache),
CreateRepoWebhook(CreateRepoWebhook),
DeleteRepoWebhook(DeleteRepoWebhook),
// ==== ALERTER ====
CreateAlerter(CreateAlerter),
CopyAlerter(CopyAlerter),
DeleteAlerter(DeleteAlerter),
UpdateAlerter(UpdateAlerter),
RenameAlerter(RenameAlerter),
// ==== PROCEDURE ====
CreateProcedure(CreateProcedure),
@@ -169,8 +147,52 @@ pub enum WriteRequest {
WriteSyncFileContents(WriteSyncFileContents),
CommitSync(CommitSync),
RefreshResourceSyncPending(RefreshResourceSyncPending),
CreateSyncWebhook(CreateSyncWebhook),
DeleteSyncWebhook(DeleteSyncWebhook),
// ==== BUILDER ====
CreateBuilder(CreateBuilder),
CopyBuilder(CopyBuilder),
DeleteBuilder(DeleteBuilder),
UpdateBuilder(UpdateBuilder),
RenameBuilder(RenameBuilder),
// ==== ALERTER ====
CreateAlerter(CreateAlerter),
CopyAlerter(CopyAlerter),
DeleteAlerter(DeleteAlerter),
UpdateAlerter(UpdateAlerter),
RenameAlerter(RenameAlerter),
// ==== ONBOARDING KEY ====
CreateOnboardingKey(CreateOnboardingKey),
UpdateOnboardingKey(UpdateOnboardingKey),
DeleteOnboardingKey(DeleteOnboardingKey),
// ==== USER ====
PushRecentlyViewed(PushRecentlyViewed),
SetLastSeenUpdate(SetLastSeenUpdate),
CreateLocalUser(CreateLocalUser),
DeleteUser(DeleteUser),
// ==== SERVICE USER ====
CreateServiceUser(CreateServiceUser),
UpdateServiceUserDescription(UpdateServiceUserDescription),
CreateApiKeyForServiceUser(CreateApiKeyForServiceUser),
DeleteApiKeyForServiceUser(DeleteApiKeyForServiceUser),
// ==== USER GROUP ====
CreateUserGroup(CreateUserGroup),
RenameUserGroup(RenameUserGroup),
DeleteUserGroup(DeleteUserGroup),
AddUserToUserGroup(AddUserToUserGroup),
RemoveUserFromUserGroup(RemoveUserFromUserGroup),
SetUsersInUserGroup(SetUsersInUserGroup),
SetEveryoneUserGroup(SetEveryoneUserGroup),
// ==== PERMISSIONS ====
UpdateUserAdmin(UpdateUserAdmin),
UpdateUserBasePermissions(UpdateUserBasePermissions),
UpdatePermissionOnResourceType(UpdatePermissionOnResourceType),
UpdatePermissionOnTarget(UpdatePermissionOnTarget),
// ==== TAG ====
CreateTag(CreateTag),
@@ -185,27 +207,32 @@ pub enum WriteRequest {
UpdateVariableIsSecret(UpdateVariableIsSecret),
DeleteVariable(DeleteVariable),
// ==== PROVIDERS ====
// ==== PROVIDER ====
CreateGitProviderAccount(CreateGitProviderAccount),
UpdateGitProviderAccount(UpdateGitProviderAccount),
DeleteGitProviderAccount(DeleteGitProviderAccount),
CreateDockerRegistryAccount(CreateDockerRegistryAccount),
UpdateDockerRegistryAccount(UpdateDockerRegistryAccount),
DeleteDockerRegistryAccount(DeleteDockerRegistryAccount),
// ==== ALERT ====
CloseAlert(CloseAlert),
}
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler))
.layer(middleware::from_fn(auth_request))
.layer(middleware::from_fn(
authenticate_request::<KomodoAuthImpl, true>,
))
}
async fn variant_handler(
user: Extension<User>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<axum::response::Response> {
) -> mogh_error::Result<axum::response::Response> {
let req: WriteRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
@@ -216,41 +243,34 @@ async fn variant_handler(
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<WriteRequest>,
) -> serror::Result<axum::response::Response> {
let req_id = Uuid::new_v4();
let res = tokio::spawn(task(req_id, request, user))
) -> mogh_error::Result<axum::response::Response> {
let res = tokio::spawn(task(request, user))
.await
.context("failure in spawned task");
res?
}
#[instrument(
name = "WriteRequest",
skip(user, request),
fields(
user_id = user.id,
request = format!("{:?}", request.extract_variant())
)
)]
async fn task(
req_id: Uuid,
request: WriteRequest,
user: User,
) -> serror::Result<axum::response::Response> {
info!("/write request | user: {}", user.username);
) -> mogh_error::Result<axum::response::Response> {
let req_id = Uuid::new_v4();
let variant: WriteRequestVariant = (&request).into();
let timer = Instant::now();
info!(
"WRITE REQUEST {req_id} | METHOD: {variant} | USER: {} ({})",
user.username, user.id
);
let res = request.resolve(&WriteArgs { user }).await;
if let Err(e) = &res {
warn!("/write request {req_id} error: {:#}", e.error);
warn!(
"WRITE REQUEST {req_id} | METHOD: {variant} | ERROR: {:#}",
e.error
);
}
let elapsed = timer.elapsed();
debug!("/write request {req_id} | resolve time: {elapsed:?}");
res.map(|res| res.0)
}

View File

@@ -0,0 +1,208 @@
use anyhow::{Context, anyhow};
use database::mungos::mongodb::bson::{Document, doc};
use komodo_client::{
api::write::{
CreateOnboardingKey, CreateOnboardingKeyResponse,
DeleteOnboardingKey, DeleteOnboardingKeyResponse,
UpdateOnboardingKey, UpdateOnboardingKeyResponse,
},
entities::{
komodo_timestamp, onboarding_key::OnboardingKey, random_string,
},
};
use mogh_error::{AddStatusCode, AddStatusCodeError};
use mogh_pki::EncodedKeyPair;
use mogh_resolver::Resolve;
use reqwest::StatusCode;
use crate::{api::write::WriteArgs, state::db_client};
//
impl Resolve<WriteArgs> for CreateOnboardingKey {
#[instrument(
"CreateOnboardingKey",
skip_all,
fields(
operator = admin.id,
name = self.name,
expires = self.expires,
tags = format!("{:?}", self.tags),
copy_server = self.copy_server,
create_builder = self.create_builder,
)
)]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> mogh_error::Result<CreateOnboardingKeyResponse> {
if !admin.admin {
return Err(
anyhow!("This call is admin only")
.status_code(StatusCode::FORBIDDEN),
);
}
let private_key = if let Some(private_key) = self.private_key {
private_key
} else {
format!("O_{}_O", random_string(28))
};
let public_key = EncodedKeyPair::from_private_key(
mogh_pki::PkiKind::Mutual,
&private_key,
)?
.public
.into_inner();
let onboarding_key = OnboardingKey {
public_key,
name: self.name,
enabled: true,
onboarded: Default::default(),
created_at: komodo_timestamp(),
expires: self.expires,
tags: self.tags,
fix_existing_servers: self.fix_existing_servers,
copy_server: self.copy_server,
create_builder: self.create_builder,
};
let db = db_client();
// Create the key
db.onboarding_keys
.insert_one(&onboarding_key)
.await
.context(
"Failed to create Server onboarding key on database",
)?;
let created = db
.onboarding_keys
.find_one(doc! { "public_key": &onboarding_key.public_key })
.await
.context("Failed to query database for Server onboarding keys")?
.context(
"No Server onboarding key found on database after create",
)?;
Ok(CreateOnboardingKeyResponse {
private_key,
created,
})
}
}
//
impl Resolve<WriteArgs> for UpdateOnboardingKey {
#[instrument(
"UpdateOnboardingKey",
skip_all,
fields(
operator = admin.id,
public_key = self.public_key,
update = format!("{:?}", self),
)
)]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> mogh_error::Result<UpdateOnboardingKeyResponse> {
if !admin.admin {
return Err(
anyhow!("This call is admin only")
.status_code(StatusCode::FORBIDDEN),
);
}
let query = doc! { "public_key": &self.public_key };
// No changes
if self.is_none() {
return db_client()
.onboarding_keys
.find_one(query)
.await
.context("Failed to query database for onboarding key")?
.context("No matching onboarding key found")
.status_code(StatusCode::NOT_FOUND);
}
let mut update = Document::new();
if let Some(enabled) = self.enabled {
update.insert("enabled", enabled);
}
if let Some(name) = self.name {
update.insert("name", name);
}
if let Some(expires) = self.expires {
update.insert("expires", expires);
}
if let Some(tags) = self.tags {
update.insert("tags", tags);
}
if let Some(fix_existing_servers) = self.fix_existing_servers {
update.insert("fix_existing_servers", fix_existing_servers);
}
if let Some(copy_server) = self.copy_server {
update.insert("copy_server", copy_server);
}
if let Some(create_builder) = self.create_builder {
update.insert("create_builder", create_builder);
}
db_client()
.onboarding_keys
.update_one(query.clone(), doc! { "$set": update })
.await
.context("Failed to update onboarding key on database")?;
db_client()
.onboarding_keys
.find_one(query)
.await
.context("Failed to query database for onboarding key")?
.context("No matching onboarding key found")
.status_code(StatusCode::NOT_FOUND)
}
}
//
impl Resolve<WriteArgs> for DeleteOnboardingKey {
#[instrument(
"DeleteOnboardingKey",
skip_all,
fields(
operator = admin.id,
public_key = self.public_key,
)
)]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> mogh_error::Result<DeleteOnboardingKeyResponse> {
if !admin.admin {
return Err(
anyhow!("This call is admin only")
.status_code(StatusCode::FORBIDDEN),
);
}
let db = db_client();
let query = doc! { "public_key": &self.public_key };
let creation_key = db
.onboarding_keys
.find_one(query.clone())
.await
.context("Failed to query database for Server onboarding keys")?
.context("Server onboarding key matching provided public key not found")
.status_code(StatusCode::NOT_FOUND)?;
db.onboarding_keys.delete_one(query).await.context(
"Failed to delete Server onboarding key from database",
)?;
Ok(creation_key)
}
}

View File

@@ -15,18 +15,26 @@ use komodo_client::{
permission::{UserTarget, UserTargetVariant},
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{helpers::query::get_user, state::db_client};
use super::WriteArgs;
impl Resolve<WriteArgs> for UpdateUserAdmin {
#[instrument(name = "UpdateUserAdmin", skip(super_admin))]
#[instrument(
"UpdateUserAdmin",
skip_all,
fields(
operator = super_admin.id,
target_user = self.user_id,
admin = self.admin,
)
)]
async fn resolve(
self,
WriteArgs { user: super_admin }: &WriteArgs,
) -> serror::Result<UpdateUserAdminResponse> {
) -> mogh_error::Result<UpdateUserAdminResponse> {
if !super_admin.super_admin {
return Err(
anyhow!("Only super admins can call this method.").into(),
@@ -60,11 +68,21 @@ impl Resolve<WriteArgs> for UpdateUserAdmin {
}
impl Resolve<WriteArgs> for UpdateUserBasePermissions {
#[instrument(name = "UpdateUserBasePermissions", skip(admin))]
#[instrument(
"UpdateUserBasePermissions",
skip_all,
fields(
operator = admin.id,
target_user = self.user_id,
enabled = self.enabled,
create_servers = self.create_servers,
create_builds = self.create_builds,
)
)]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdateUserBasePermissionsResponse> {
) -> mogh_error::Result<UpdateUserBasePermissionsResponse> {
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
@@ -117,11 +135,20 @@ impl Resolve<WriteArgs> for UpdateUserBasePermissions {
}
impl Resolve<WriteArgs> for UpdatePermissionOnResourceType {
#[instrument(name = "UpdatePermissionOnResourceType", skip(admin))]
#[instrument(
"UpdatePermissionOnResourceType",
skip_all,
fields(
operator = admin.id,
user_target = format!("{:?}", self.user_target),
resource_type = self.resource_type.to_string(),
permission = format!("{:?}", self.permission),
)
)]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdatePermissionOnResourceTypeResponse> {
) -> mogh_error::Result<UpdatePermissionOnResourceTypeResponse> {
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
@@ -185,11 +212,21 @@ impl Resolve<WriteArgs> for UpdatePermissionOnResourceType {
}
impl Resolve<WriteArgs> for UpdatePermissionOnTarget {
#[instrument(name = "UpdatePermissionOnTarget", skip(admin))]
#[instrument(
"UpdatePermissionOnTarget",
skip_all,
fields(
operator = admin.id,
user_target = format!("{:?}", self.user_target),
resource_type = self.resource_target.extract_variant().to_string(),
resource_id = self.resource_target.extract_variant_id().1,
permission = format!("{:?}", self.permission),
)
)]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdatePermissionOnTargetResponse> {
) -> mogh_error::Result<UpdatePermissionOnTargetResponse> {
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
@@ -258,7 +295,7 @@ impl Resolve<WriteArgs> for UpdatePermissionOnTarget {
/// 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,
) -> serror::Result<(UserTargetVariant, String)> {
) -> mogh_error::Result<(UserTargetVariant, String)> {
match user_target {
UserTarget::User(ident) => {
let filter = match ObjectId::from_str(ident) {
@@ -269,8 +306,8 @@ async fn extract_user_target_with_validation(
.users
.find_one(filter)
.await
.context("failed to query db for users")?
.context("no matching user found")?
.context("Failed to query db for users")?
.context("No matching user found")?
.id;
Ok((UserTargetVariant::User, id))
}
@@ -283,8 +320,8 @@ async fn extract_user_target_with_validation(
.user_groups
.find_one(filter)
.await
.context("failed to query db for user_groups")?
.context("no matching user_group found")?
.context("Failed to query db for user_groups")?
.context("No matching user_group found")?
.id;
Ok((UserTargetVariant::UserGroup, id))
}
@@ -294,53 +331,25 @@ async fn extract_user_target_with_validation(
/// 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,
) -> serror::Result<(ResourceTargetVariant, String)> {
) -> mogh_error::Result<(ResourceTargetVariant, String)> {
match resource_target {
ResourceTarget::System(_) => {
let res = resource_target.extract_variant_id();
Ok((res.0, res.1.clone()))
}
ResourceTarget::Build(ident) => {
ResourceTarget::Swarm(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.builds
.swarms
.find_one(filter)
.await
.context("failed to query db for builds")?
.context("no matching build found")?
.context("Failed to query db for swarms")?
.context("No matching server 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()
.builders
.find_one(filter)
.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()
.deployments
.find_one(filter)
.await
.context("failed to query db for deployments")?
.context("no matching deployment found")?
.id;
Ok((ResourceTargetVariant::Deployment, id))
Ok((ResourceTargetVariant::Server, id))
}
ResourceTarget::Server(ident) => {
let filter = match ObjectId::from_str(ident) {
@@ -351,11 +360,53 @@ async fn extract_resource_target_with_validation(
.servers
.find_one(filter)
.await
.context("failed to query db for servers")?
.context("no matching server found")?
.context("Failed to query db for servers")?
.context("No matching server found")?
.id;
Ok((ResourceTargetVariant::Server, id))
}
ResourceTarget::Stack(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.stacks
.find_one(filter)
.await
.context("Failed to query db for stacks")?
.context("No matching stack found")?
.id;
Ok((ResourceTargetVariant::Stack, id))
}
ResourceTarget::Deployment(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.deployments
.find_one(filter)
.await
.context("Failed to query db for deployments")?
.context("No matching deployment found")?
.id;
Ok((ResourceTargetVariant::Deployment, id))
}
ResourceTarget::Build(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.builds
.find_one(filter)
.await
.context("Failed to query db for builds")?
.context("No matching build found")?
.id;
Ok((ResourceTargetVariant::Build, id))
}
ResourceTarget::Repo(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
@@ -365,25 +416,11 @@ async fn extract_resource_target_with_validation(
.repos
.find_one(filter)
.await
.context("failed to query db for repos")?
.context("no matching repo found")?
.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()
.alerters
.find_one(filter)
.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 },
@@ -393,8 +430,8 @@ async fn extract_resource_target_with_validation(
.procedures
.find_one(filter)
.await
.context("failed to query db for procedures")?
.context("no matching procedure found")?
.context("Failed to query db for procedures")?
.context("No matching procedure found")?
.id;
Ok((ResourceTargetVariant::Procedure, id))
}
@@ -407,8 +444,8 @@ async fn extract_resource_target_with_validation(
.actions
.find_one(filter)
.await
.context("failed to query db for actions")?
.context("no matching action found")?
.context("Failed to query db for actions")?
.context("No matching action found")?
.id;
Ok((ResourceTargetVariant::Action, id))
}
@@ -421,24 +458,38 @@ async fn extract_resource_target_with_validation(
.resource_syncs
.find_one(filter)
.await
.context("failed to query db for resource syncs")?
.context("no matching resource sync found")?
.context("Failed to query db for resource syncs")?
.context("No matching resource sync found")?
.id;
Ok((ResourceTargetVariant::ResourceSync, id))
}
ResourceTarget::Stack(ident) => {
ResourceTarget::Builder(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.stacks
.builders
.find_one(filter)
.await
.context("failed to query db for stacks")?
.context("no matching stack found")?
.context("Failed to query db for builders")?
.context("No matching builder found")?
.id;
Ok((ResourceTargetVariant::Stack, id))
Ok((ResourceTargetVariant::Builder, id))
}
ResourceTarget::Alerter(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.alerters
.find_one(filter)
.await
.context("Failed to query db for alerters")?
.context("No matching alerter found")?
.id;
Ok((ResourceTargetVariant::Alerter, id))
}
}
}

View File

@@ -4,28 +4,45 @@ use komodo_client::{
permission::PermissionLevel, procedure::Procedure, update::Update,
},
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use crate::{permission::get_check_permissions, resource};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateProcedure {
#[instrument(name = "CreateProcedure", skip(user))]
#[instrument(
"CreateProcedure",
skip_all,
fields(
operator = user.id,
procedure = self.name,
config = serde_json::to_string(&self.config).unwrap()
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateProcedureResponse> {
resource::create::<Procedure>(&self.name, self.config, user).await
) -> mogh_error::Result<CreateProcedureResponse> {
resource::create::<Procedure>(&self.name, self.config, None, user)
.await
}
}
impl Resolve<WriteArgs> for CopyProcedure {
#[instrument(name = "CopyProcedure", skip(user))]
#[instrument(
"CopyProcedure",
skip_all,
fields(
operator = user.id,
procedure = self.name,
copy_procedure = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CopyProcedureResponse> {
) -> mogh_error::Result<CopyProcedureResponse> {
let Procedure { config, .. } =
get_check_permissions::<Procedure>(
&self.id,
@@ -33,17 +50,30 @@ impl Resolve<WriteArgs> for CopyProcedure {
PermissionLevel::Write.into(),
)
.await?;
resource::create::<Procedure>(&self.name, config.into(), user)
.await
resource::create::<Procedure>(
&self.name,
config.into(),
None,
user,
)
.await
}
}
impl Resolve<WriteArgs> for UpdateProcedure {
#[instrument(name = "UpdateProcedure", skip(user))]
#[instrument(
"UpdateProcedure",
skip_all,
fields(
operator = user.id,
procedure = self.id,
update = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateProcedureResponse> {
) -> mogh_error::Result<UpdateProcedureResponse> {
Ok(
resource::update::<Procedure>(&self.id, self.config, user)
.await?,
@@ -52,11 +82,19 @@ impl Resolve<WriteArgs> for UpdateProcedure {
}
impl Resolve<WriteArgs> for RenameProcedure {
#[instrument(name = "RenameProcedure", skip(user))]
#[instrument(
"RenameProcedure",
skip_all,
fields(
operator = user.id,
procedure = self.id,
new_name = self.name,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
Ok(
resource::rename::<Procedure>(&self.id, &self.name, user)
.await?,
@@ -65,11 +103,18 @@ impl Resolve<WriteArgs> for RenameProcedure {
}
impl Resolve<WriteArgs> for DeleteProcedure {
#[instrument(name = "DeleteProcedure", skip(args))]
#[instrument(
"DeleteProcedure",
skip_all,
fields(
operator = user.id,
procedure = self.id
)
)]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<DeleteProcedureResponse> {
Ok(resource::delete::<Procedure>(&self.id, args).await?)
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<DeleteProcedureResponse> {
Ok(resource::delete::<Procedure>(&self.id, user).await?)
}
}

View File

@@ -10,7 +10,9 @@ use komodo_client::{
provider::{DockerRegistryAccount, GitProviderAccount},
},
};
use resolver_api::Resolve;
use mogh_error::AddStatusCodeError;
use mogh_resolver::Resolve;
use reqwest::StatusCode;
use crate::{
helpers::update::{add_update, make_update},
@@ -20,25 +22,41 @@ use crate::{
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateGitProviderAccount {
#[instrument(
"CreateGitProviderAccount",
skip_all,
fields(
operator = user.id,
domain = self.account.domain,
username = self.account.username,
https = self.account.https.unwrap_or(true),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateGitProviderAccountResponse> {
) -> mogh_error::Result<CreateGitProviderAccountResponse> {
if !user.admin {
return Err(
anyhow!("only admins can create git provider accounts")
.into(),
anyhow!("Only admins can create git provider accounts")
.status_code(StatusCode::FORBIDDEN),
);
}
let mut account: GitProviderAccount = self.account.into();
if account.domain.is_empty() {
return Err(anyhow!("domain cannot be empty string.").into());
return Err(
anyhow!("Domain cannot be empty string.")
.status_code(StatusCode::BAD_REQUEST),
);
}
if account.username.is_empty() {
return Err(anyhow!("username cannot be empty string.").into());
return Err(
anyhow!("Username cannot be empty string.")
.status_code(StatusCode::BAD_REQUEST),
);
}
let mut update = make_update(
@@ -51,14 +69,14 @@ impl Resolve<WriteArgs> for CreateGitProviderAccount {
.git_accounts
.insert_one(&account)
.await
.context("failed to create git provider account on db")?
.context("Failed to create git provider account on db")?
.inserted_id
.as_object_id()
.context("inserted id is not ObjectId")?
.context("Inserted id is not ObjectId")?
.to_string();
update.push_simple_log(
"create git provider account",
"Create git provider account",
format!(
"Created git provider account for {} with username {}",
account.domain, account.username
@@ -70,7 +88,7 @@ impl Resolve<WriteArgs> for CreateGitProviderAccount {
add_update(update)
.await
.inspect_err(|e| {
error!("failed to add update for create git provider account | {e:#}")
error!("Failed to add update for create git provider account | {e:#}")
})
.ok();
@@ -79,14 +97,25 @@ impl Resolve<WriteArgs> for CreateGitProviderAccount {
}
impl Resolve<WriteArgs> for UpdateGitProviderAccount {
#[instrument(
"UpdateGitProviderAccount",
skip_all,
fields(
operator = user.id,
id = self.id,
domain = self.account.domain,
username = self.account.username,
https = self.account.https.unwrap_or(true),
)
)]
async fn resolve(
mut self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateGitProviderAccountResponse> {
) -> mogh_error::Result<UpdateGitProviderAccountResponse> {
if !user.admin {
return Err(
anyhow!("only admins can update git provider accounts")
.into(),
anyhow!("Only admins can update git provider accounts")
.status_code(StatusCode::FORBIDDEN),
);
}
@@ -94,8 +123,8 @@ impl Resolve<WriteArgs> for UpdateGitProviderAccount {
&& domain.is_empty()
{
return Err(
anyhow!("cannot update git provider with empty domain")
.into(),
anyhow!("Cannot update git provider with empty domain")
.status_code(StatusCode::BAD_REQUEST),
);
}
@@ -103,8 +132,8 @@ impl Resolve<WriteArgs> for UpdateGitProviderAccount {
&& username.is_empty()
{
return Err(
anyhow!("cannot update git provider with empty username")
.into(),
anyhow!("Cannot update git provider with empty username")
.status_code(StatusCode::BAD_REQUEST),
);
}
@@ -118,7 +147,7 @@ impl Resolve<WriteArgs> for UpdateGitProviderAccount {
);
let account = to_document(&self.account).context(
"failed to serialize partial git provider account to bson",
"Failed to serialize partial git provider account to bson",
)?;
let db = db_client();
update_one_by_id(
@@ -128,17 +157,17 @@ impl Resolve<WriteArgs> for UpdateGitProviderAccount {
None,
)
.await
.context("failed to update git provider account on db")?;
.context("Failed to update git provider account on db")?;
let Some(account) = find_one_by_id(&db.git_accounts, &self.id)
.await
.context("failed to query db for git accounts")?
.context("Failed to query db for git accounts")?
else {
return Err(anyhow!("no account found with given id").into());
return Err(anyhow!("No account found with given id").into());
};
update.push_simple_log(
"update git provider account",
"Update git provider account",
format!(
"Updated git provider account for {} with username {}",
account.domain, account.username
@@ -150,7 +179,7 @@ impl Resolve<WriteArgs> for UpdateGitProviderAccount {
add_update(update)
.await
.inspect_err(|e| {
error!("failed to add update for update git provider account | {e:#}")
error!("Failed to add update for update git provider account | {e:#}")
})
.ok();
@@ -159,14 +188,22 @@ impl Resolve<WriteArgs> for UpdateGitProviderAccount {
}
impl Resolve<WriteArgs> for DeleteGitProviderAccount {
#[instrument(
"DeleteGitProviderAccount",
skip_all,
fields(
operator = user.id,
id = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteGitProviderAccountResponse> {
) -> mogh_error::Result<DeleteGitProviderAccountResponse> {
if !user.admin {
return Err(
anyhow!("only admins can delete git provider accounts")
.into(),
anyhow!("Only admins can delete git provider accounts")
.status_code(StatusCode::FORBIDDEN),
);
}
@@ -179,16 +216,19 @@ impl Resolve<WriteArgs> for DeleteGitProviderAccount {
let db = db_client();
let Some(account) = find_one_by_id(&db.git_accounts, &self.id)
.await
.context("failed to query db for git accounts")?
.context("Failed to query db for git accounts")?
else {
return Err(anyhow!("no account found with given id").into());
return Err(
anyhow!("No account found with given id")
.status_code(StatusCode::BAD_REQUEST),
);
};
delete_one_by_id(&db.git_accounts, &self.id, None)
.await
.context("failed to delete git account on db")?;
update.push_simple_log(
"delete git provider account",
"Delete git provider account",
format!(
"Deleted git provider account for {} with username {}",
account.domain, account.username
@@ -200,7 +240,7 @@ impl Resolve<WriteArgs> for DeleteGitProviderAccount {
add_update(update)
.await
.inspect_err(|e| {
error!("failed to add update for delete git provider account | {e:#}")
error!("Failed to add update for delete git provider account | {e:#}")
})
.ok();
@@ -209,27 +249,42 @@ impl Resolve<WriteArgs> for DeleteGitProviderAccount {
}
impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
#[instrument(
"CreateDockerRegistryAccount",
skip_all,
fields(
operator = user.id,
domain = self.account.domain,
username = self.account.username,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateDockerRegistryAccountResponse> {
) -> mogh_error::Result<CreateDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!(
"only admins can create docker registry account accounts"
"Only admins can create docker registry account accounts"
)
.into(),
.status_code(StatusCode::FORBIDDEN),
);
}
let mut account: DockerRegistryAccount = self.account.into();
if account.domain.is_empty() {
return Err(anyhow!("domain cannot be empty string.").into());
return Err(
anyhow!("Domain cannot be empty string.")
.status_code(StatusCode::BAD_REQUEST),
);
}
if account.username.is_empty() {
return Err(anyhow!("username cannot be empty string.").into());
return Err(
anyhow!("Username cannot be empty string.")
.status_code(StatusCode::BAD_REQUEST),
);
}
let mut update = make_update(
@@ -243,15 +298,15 @@ impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
.insert_one(&account)
.await
.context(
"failed to create docker registry account account on db",
"Failed to create docker registry account account on db",
)?
.inserted_id
.as_object_id()
.context("inserted id is not ObjectId")?
.context("Inserted id is not ObjectId")?
.to_string();
update.push_simple_log(
"create docker registry account",
"Create docker registry account",
format!(
"Created docker registry account account for {} with username {}",
account.domain, account.username
@@ -263,7 +318,7 @@ impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
add_update(update)
.await
.inspect_err(|e| {
error!("failed to add update for create docker registry account | {e:#}")
error!("Failed to add update for create docker registry account | {e:#}")
})
.ok();
@@ -272,14 +327,24 @@ impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
}
impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
#[instrument(
"UpdateDockerRegistryAccount",
skip_all,
fields(
operator = user.id,
id = self.id,
domain = self.account.domain,
username = self.account.username,
)
)]
async fn resolve(
mut self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateDockerRegistryAccountResponse> {
) -> mogh_error::Result<UpdateDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!("only admins can update docker registry accounts")
.into(),
anyhow!("Only admins can update docker registry accounts")
.status_code(StatusCode::FORBIDDEN),
);
}
@@ -288,9 +353,9 @@ impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
{
return Err(
anyhow!(
"cannot update docker registry account with empty domain"
"Cannot update docker registry account with empty domain"
)
.into(),
.status_code(StatusCode::BAD_REQUEST),
);
}
@@ -299,9 +364,9 @@ impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
{
return Err(
anyhow!(
"cannot update docker registry account with empty username"
"Cannot update docker registry account with empty username"
)
.into(),
.status_code(StatusCode::BAD_REQUEST),
);
}
@@ -314,7 +379,7 @@ impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
);
let account = to_document(&self.account).context(
"failed to serialize partial docker registry account account to bson",
"Failed to serialize partial docker registry account account to bson",
)?;
let db = db_client();
@@ -326,19 +391,19 @@ impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
)
.await
.context(
"failed to update docker registry account account on db",
"Failed to update docker registry account account on db",
)?;
let Some(account) =
find_one_by_id(&db.registry_accounts, &self.id)
.await
.context("failed to query db for registry accounts")?
.context("Failed to query db for registry accounts")?
else {
return Err(anyhow!("no account found with given id").into());
return Err(anyhow!("No account found with given id").into());
};
update.push_simple_log(
"update docker registry account",
"Update docker registry account",
format!(
"Updated docker registry account account for {} with username {}",
account.domain, account.username
@@ -350,7 +415,7 @@ impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
add_update(update)
.await
.inspect_err(|e| {
error!("failed to add update for update docker registry account | {e:#}")
error!("Failed to add update for update docker registry account | {e:#}")
})
.ok();
@@ -359,14 +424,22 @@ impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
}
impl Resolve<WriteArgs> for DeleteDockerRegistryAccount {
#[instrument(
"DeleteDockerRegistryAccount",
skip_all,
fields(
operator = user.id,
id = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteDockerRegistryAccountResponse> {
) -> mogh_error::Result<DeleteDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!("only admins can delete docker registry accounts")
.into(),
anyhow!("Only admins can delete docker registry accounts")
.status_code(StatusCode::FORBIDDEN),
);
}
@@ -380,16 +453,19 @@ impl Resolve<WriteArgs> for DeleteDockerRegistryAccount {
let Some(account) =
find_one_by_id(&db.registry_accounts, &self.id)
.await
.context("failed to query db for git accounts")?
.context("Failed to query db for git accounts")?
else {
return Err(anyhow!("no account found with given id").into());
return Err(
anyhow!("No account found with given id")
.status_code(StatusCode::BAD_REQUEST),
);
};
delete_one_by_id(&db.registry_accounts, &self.id, None)
.await
.context("failed to delete registry account on db")?;
.context("Failed to delete registry account on db")?;
update.push_simple_log(
"delete registry account",
"Delete registry account",
format!(
"Deleted registry account for {} with username {}",
account.domain, account.username
@@ -401,7 +477,7 @@ impl Resolve<WriteArgs> for DeleteDockerRegistryAccount {
add_update(update)
.await
.inspect_err(|e| {
error!("failed to add update for delete docker registry account | {e:#}")
error!("Failed to add update for delete docker registry account | {e:#}")
})
.ok();

View File

@@ -1,4 +1,4 @@
use anyhow::{Context, anyhow};
use anyhow::Context;
use database::mongo_indexed::doc;
use database::mungos::{
by_id::update_one_by_id, mongodb::bson::to_document,
@@ -7,21 +7,16 @@ use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
NoData, Operation, RepoExecutionArgs,
config::core::CoreConfig,
komodo_timestamp,
NoData, Operation, RepoExecutionArgs, komodo_timestamp,
permission::PermissionLevel,
repo::{PartialRepoConfig, Repo, RepoInfo},
repo::{Repo, RepoInfo},
server::Server,
to_path_compatible_name,
update::{Log, Update},
},
};
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
};
use mogh_resolver::Resolve;
use periphery_client::api;
use resolver_api::Resolve;
use crate::{
config::core_config,
@@ -31,60 +26,104 @@ use crate::{
},
permission::get_check_permissions,
resource,
state::{action_states, db_client, github_client},
state::{action_states, db_client},
};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateRepo {
#[instrument(name = "CreateRepo", skip(user))]
#[instrument(
"CreateRepo",
skip_all,
fields(
operator = user.id,
repo = self.name,
config = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Repo> {
resource::create::<Repo>(&self.name, self.config, user).await
) -> mogh_error::Result<Repo> {
resource::create::<Repo>(&self.name, self.config, None, user)
.await
}
}
impl Resolve<WriteArgs> for CopyRepo {
#[instrument(name = "CopyRepo", skip(user))]
#[instrument(
"CopyRepo",
skip_all,
fields(
operator = user.id,
repo = self.name,
copy_repo = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Repo> {
) -> mogh_error::Result<Repo> {
let Repo { config, .. } = get_check_permissions::<Repo>(
&self.id,
user,
PermissionLevel::Read.into(),
)
.await?;
resource::create::<Repo>(&self.name, config.into(), user).await
resource::create::<Repo>(&self.name, config.into(), None, user)
.await
}
}
impl Resolve<WriteArgs> for DeleteRepo {
#[instrument(name = "DeleteRepo", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Repo> {
Ok(resource::delete::<Repo>(&self.id, args).await?)
#[instrument(
"DeleteRepo",
skip_all,
fields(
operator = user.id,
repo = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<Repo> {
Ok(resource::delete::<Repo>(&self.id, user).await?)
}
}
impl Resolve<WriteArgs> for UpdateRepo {
#[instrument(name = "UpdateRepo", skip(user))]
#[instrument(
"UpdateRepo",
skip_all,
fields(
operator = user.id,
repo = self.id,
update = serde_json::to_string(&self.config).unwrap()
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Repo> {
) -> mogh_error::Result<Repo> {
Ok(resource::update::<Repo>(&self.id, self.config, user).await?)
}
}
impl Resolve<WriteArgs> for RenameRepo {
#[instrument(name = "RenameRepo", skip(user))]
#[instrument(
"RenameRepo",
skip_all,
fields(
operator = user.id,
repo = self.id,
new_name = self.name
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
let repo = get_check_permissions::<Repo>(
&self.id,
user,
@@ -127,7 +166,8 @@ impl Resolve<WriteArgs> for RenameRepo {
let server =
resource::get::<Server>(&repo.config.server_id).await?;
let log = match periphery_client(&server)?
let log = match periphery_client(&server)
.await?
.request(api::git::RenameRepo {
curr_name: to_path_compatible_name(&repo.name),
new_name: name.clone(),
@@ -156,15 +196,10 @@ impl Resolve<WriteArgs> for RenameRepo {
}
impl Resolve<WriteArgs> for RefreshRepoCache {
#[instrument(
name = "RefreshRepoCache",
level = "debug",
skip(user)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
) -> mogh_error::Result<NoData> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// repo should be able to do this.
let repo = get_check_permissions::<Repo>(
@@ -232,220 +267,3 @@ impl Resolve<WriteArgs> for RefreshRepoCache {
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for CreateRepoWebhook {
#[instrument(name = "CreateRepoWebhook", skip(args))]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<CreateRepoWebhookResponse> {
let Some(github) = github_client() else {
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let repo = get_check_permissions::<Repo>(
&self.repo,
&args.user,
PermissionLevel::Write.into(),
)
.await?;
if repo.config.repo.is_empty() {
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = repo.config.repo.split('/');
let owner = split.next().context("Repo repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo_name =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
// First make sure the webhook isn't already created (inactive ones are ignored)
let webhooks = github_repos
.list_all_webhooks(owner, repo_name)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
webhook_secret,
..
} = core_config();
let webhook_secret = if repo.config.webhook_secret.is_empty() {
webhook_secret
} else {
&repo.config.webhook_secret
};
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = match self.action {
RepoWebhookAction::Clone => {
format!("{host}/listener/github/repo/{}/clone", repo.id)
}
RepoWebhookAction::Pull => {
format!("{host}/listener/github/repo/{}/pull", repo.id)
}
RepoWebhookAction::Build => {
format!("{host}/listener/github/repo/{}/build", repo.id)
}
};
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
return Ok(NoData {});
}
}
// Now good to create the webhook
let request = ReposCreateWebhookRequest {
active: Some(true),
config: Some(ReposCreateWebhookRequestConfig {
url,
secret: webhook_secret.to_string(),
content_type: String::from("json"),
insecure_ssl: None,
digest: Default::default(),
token: Default::default(),
}),
events: vec![String::from("push")],
name: String::from("web"),
};
github_repos
.create_webhook(owner, repo_name, &request)
.await
.context("failed to create webhook")?;
if !repo.config.webhook_enabled {
UpdateRepo {
id: repo.id,
config: PartialRepoConfig {
webhook_enabled: Some(true),
..Default::default()
},
}
.resolve(args)
.await
.map_err(|e| e.error)
.context("failed to update repo to enable webhook")?;
}
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteRepoWebhook {
#[instrument(name = "DeleteRepoWebhook", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteRepoWebhookResponse> {
let Some(github) = github_client() else {
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let repo = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Write.into(),
)
.await?;
if repo.config.git_provider != "github.com" {
return Err(
anyhow!("Can only manage github.com repo webhooks").into(),
);
}
if repo.config.repo.is_empty() {
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = repo.config.repo.split('/');
let owner = split.next().context("Repo repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo_name =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
// First make sure the webhook isn't already created (inactive ones are ignored)
let webhooks = github_repos
.list_all_webhooks(owner, repo_name)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = match self.action {
RepoWebhookAction::Clone => {
format!("{host}/listener/github/repo/{}/clone", repo.id)
}
RepoWebhookAction::Pull => {
format!("{host}/listener/github/repo/{}/pull", repo.id)
}
RepoWebhookAction::Build => {
format!("{host}/listener/github/repo/{}/build", repo.id)
}
};
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
github_repos
.delete_webhook(owner, repo_name, webhook.id)
.await
.context("failed to delete webhook")?;
return Ok(NoData {});
}
}
// No webhook to delete, all good
Ok(NoData {})
}
}

View File

@@ -4,21 +4,35 @@ use komodo_client::{
entities::{
ResourceTarget, action::Action, alerter::Alerter, build::Build,
builder::Builder, deployment::Deployment, procedure::Procedure,
repo::Repo, server::Server, stack::Stack, sync::ResourceSync,
repo::Repo, server::Server, stack::Stack, swarm::Swarm,
sync::ResourceSync,
},
};
use resolver_api::Resolve;
use mogh_error::AddStatusCodeError;
use mogh_resolver::Resolve;
use reqwest::StatusCode;
use crate::resource::{self, ResourceMetaUpdate};
use super::WriteArgs;
impl Resolve<WriteArgs> for UpdateResourceMeta {
#[instrument(name = "UpdateResourceMeta", skip(args))]
#[instrument(
"UpdateResourceMeta",
skip_all,
fields(
operator = args.user.id,
resource_type = self.target.extract_variant().to_string(),
resource_id = self.target.extract_variant_id().1,
description = self.description,
template = self.template,
tags = format!("{:?}", self.tags),
)
)]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<UpdateResourceMetaResponse> {
) -> mogh_error::Result<UpdateResourceMetaResponse> {
let meta = ResourceMetaUpdate {
description: self.description,
template: self.template,
@@ -28,12 +42,18 @@ impl Resolve<WriteArgs> for UpdateResourceMeta {
ResourceTarget::System(_) => {
return Err(
anyhow!("cannot update meta of System resource target")
.into(),
.status_code(StatusCode::BAD_REQUEST),
);
}
ResourceTarget::Swarm(id) => {
resource::update_meta::<Swarm>(&id, meta, args).await?;
}
ResourceTarget::Server(id) => {
resource::update_meta::<Server>(&id, meta, args).await?;
}
ResourceTarget::Stack(id) => {
resource::update_meta::<Stack>(&id, meta, args).await?;
}
ResourceTarget::Deployment(id) => {
resource::update_meta::<Deployment>(&id, meta, args).await?;
}
@@ -43,12 +63,6 @@ impl Resolve<WriteArgs> for UpdateResourceMeta {
ResourceTarget::Repo(id) => {
resource::update_meta::<Repo>(&id, meta, args).await?;
}
ResourceTarget::Builder(id) => {
resource::update_meta::<Builder>(&id, meta, args).await?;
}
ResourceTarget::Alerter(id) => {
resource::update_meta::<Alerter>(&id, meta, args).await?;
}
ResourceTarget::Procedure(id) => {
resource::update_meta::<Procedure>(&id, meta, args).await?;
}
@@ -59,8 +73,11 @@ impl Resolve<WriteArgs> for UpdateResourceMeta {
resource::update_meta::<ResourceSync>(&id, meta, args)
.await?;
}
ResourceTarget::Stack(id) => {
resource::update_meta::<Stack>(&id, meta, args).await?;
ResourceTarget::Builder(id) => {
resource::update_meta::<Builder>(&id, meta, args).await?;
}
ResourceTarget::Alerter(id) => {
resource::update_meta::<Alerter>(&id, meta, args).await?;
}
}
Ok(UpdateResourceMetaResponse {})

View File

@@ -1,17 +1,17 @@
use anyhow::Context;
use formatting::format_serror;
use formatting::{bold, format_serror};
use komodo_client::{
api::write::*,
entities::{
NoData, Operation,
Operation,
permission::PermissionLevel,
server::Server,
server::{Server, ServerInfo},
to_docker_compatible_name,
update::{Update, UpdateStatus},
},
};
use mogh_resolver::Resolve;
use periphery_client::api;
use resolver_api::Resolve;
use crate::{
helpers::{
@@ -19,27 +19,52 @@ use crate::{
update::{add_update, make_update, update_update},
},
permission::get_check_permissions,
resource,
resource::{self, update_server_public_key},
};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateServer {
#[instrument(name = "CreateServer", skip(user))]
#[instrument(
"CreateServer",
skip_all,
fields(
operator = user.id,
server = self.name,
config = serde_json::to_string(&self.config).unwrap()
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Server> {
resource::create::<Server>(&self.name, self.config, user).await
) -> mogh_error::Result<Server> {
resource::create::<Server>(
&self.name,
self.config,
self.public_key.map(|public_key| ServerInfo {
public_key,
..Default::default()
}),
user,
)
.await
}
}
impl Resolve<WriteArgs> for CopyServer {
#[instrument(name = "CopyServer", skip(user))]
#[instrument(
"CopyServer",
skip_all,
fields(
operator = user.id,
server = self.name,
copy_server = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Server> {
) -> mogh_error::Result<Server> {
let Server { config, .. } = get_check_permissions::<Server>(
&self.id,
user,
@@ -47,43 +72,86 @@ impl Resolve<WriteArgs> for CopyServer {
)
.await?;
resource::create::<Server>(&self.name, config.into(), user).await
resource::create::<Server>(
&self.name,
config.into(),
self.public_key.map(|public_key| ServerInfo {
public_key,
..Default::default()
}),
user,
)
.await
}
}
impl Resolve<WriteArgs> for DeleteServer {
#[instrument(name = "DeleteServer", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Server> {
Ok(resource::delete::<Server>(&self.id, args).await?)
#[instrument(
"DeleteServer",
skip_all,
fields(
operator = user.id,
server = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<Server> {
Ok(resource::delete::<Server>(&self.id, user).await?)
}
}
impl Resolve<WriteArgs> for UpdateServer {
#[instrument(name = "UpdateServer", skip(user))]
#[instrument(
"UpdateServer",
skip_all,
fields(
operator = user.id,
server = self.id,
update = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Server> {
) -> mogh_error::Result<Server> {
Ok(resource::update::<Server>(&self.id, self.config, user).await?)
}
}
impl Resolve<WriteArgs> for RenameServer {
#[instrument(name = "RenameServer", skip(user))]
#[instrument(
"RenameServer",
skip_all,
fields(
operator = user.id,
server = self.id,
new_name = self.name,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
Ok(resource::rename::<Server>(&self.id, &self.name, user).await?)
}
}
impl Resolve<WriteArgs> for CreateNetwork {
#[instrument(name = "CreateNetwork", skip(user))]
#[instrument(
"CreateNetwork",
skip_all,
fields(
operator = user.id,
server = self.server,
network = self.name
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
@@ -91,7 +159,7 @@ impl Resolve<WriteArgs> for CreateNetwork {
)
.await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
let mut update =
make_update(&server, Operation::CreateNetwork, user);
@@ -99,7 +167,7 @@ impl Resolve<WriteArgs> for CreateNetwork {
update.id = add_update(update.clone()).await?;
match periphery
.request(api::network::CreateNetwork {
.request(api::docker::CreateNetwork {
name: to_docker_compatible_name(&self.name),
driver: None,
})
@@ -108,7 +176,7 @@ impl Resolve<WriteArgs> for CreateNetwork {
Ok(log) => update.logs.push(log),
Err(e) => update.push_error_log(
"create network",
format_serror(&e.context("failed to create network").into()),
format_serror(&e.context("Failed to create network").into()),
),
};
@@ -119,80 +187,80 @@ impl Resolve<WriteArgs> for CreateNetwork {
}
}
impl Resolve<WriteArgs> for CreateTerminal {
#[instrument(name = "CreateTerminal", skip(user))]
//
impl Resolve<WriteArgs> for UpdateServerPublicKey {
#[instrument(
"UpdateServerPublicKey",
skip_all,
fields(
operator = args.user.id,
server = self.server,
public_key = self.public_key,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
args: &WriteArgs,
) -> Result<Self::Response, Self::Error> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write.terminal(),
&args.user,
PermissionLevel::Write.into(),
)
.await?;
let periphery = periphery_client(&server)?;
update_server_public_key(&server.id, &self.public_key).await?;
periphery
.request(api::terminal::CreateTerminal {
name: self.name,
command: self.command,
recreate: self.recreate,
})
.await
.context("Failed to create terminal on periphery")?;
let mut update =
make_update(&server, Operation::UpdateServerKey, &args.user);
Ok(NoData {})
update.push_simple_log(
"Update Server Public Key",
format!("Public key updated to {}", bold(&self.public_key)),
);
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<WriteArgs> for DeleteTerminal {
#[instrument(name = "DeleteTerminal", skip(user))]
//
impl Resolve<WriteArgs> for RotateServerKeys {
#[instrument(
"RotateServerKeys",
skip_all,
fields(
operator = args.user.id,
server = self.server,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
args: &WriteArgs,
) -> Result<Self::Response, Self::Error> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write.terminal(),
&args.user,
PermissionLevel::Write.into(),
)
.await?;
let periphery = periphery_client(&server)?;
let periphery = periphery_client(&server).await?;
periphery
.request(api::terminal::DeleteTerminal {
terminal: self.terminal,
})
let public_key = periphery
.request(api::keys::RotatePrivateKey {})
.await
.context("Failed to delete terminal on periphery")?;
.context("Failed to rotate Periphery private key")?
.public_key;
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteAllTerminals {
#[instrument(name = "DeleteAllTerminals", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write.terminal(),
)
.await?;
let periphery = periphery_client(&server)?;
periphery
.request(api::terminal::DeleteAllTerminals {})
.await
.context("Failed to delete all terminals on periphery")?;
Ok(NoData {})
UpdateServerPublicKey {
server: server.id,
public_key,
}
.resolve(args)
.await
}
}

View File

@@ -1,54 +1,62 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use database::mungos::{
by_id::find_one_by_id,
mongodb::bson::{doc, oid::ObjectId},
};
use database::mungos::{by_id::find_one_by_id, mongodb::bson::doc};
use komodo_client::{
api::{user::CreateApiKey, write::*},
api::write::*,
entities::{
komodo_timestamp,
user::{User, UserConfig},
user::{NewUserParams, User, UserConfig},
},
};
use resolver_api::Resolve;
use mogh_auth_client::api::manage::CreateApiKey;
use mogh_auth_server::api::manage::api_key::create_api_key;
use mogh_error::{AddStatusCode as _, AddStatusCodeError as _};
use mogh_resolver::Resolve;
use reqwest::StatusCode;
use crate::{api::user::UserArgs, state::db_client};
use crate::{
auth::KomodoAuthImpl, helpers::validations::validate_username,
state::db_client,
};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateServiceUser {
#[instrument(name = "CreateServiceUser", skip(user))]
#[instrument(
"CreateServiceUser",
skip_all,
fields(
operator = user.id,
username = self.username,
description = self.description,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateServiceUserResponse> {
) -> mogh_error::Result<CreateServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin").into());
}
if ObjectId::from_str(&self.username).is_ok() {
return Err(
anyhow!("username cannot be valid ObjectId").into(),
anyhow!("Only Admins can manage Service Users")
.status_code(StatusCode::FORBIDDEN),
);
}
validate_username(&self.username)
.status_code(StatusCode::BAD_REQUEST)?;
let config = UserConfig::Service {
description: self.description,
};
let mut user = User {
id: Default::default(),
let mut user = User::new(NewUserParams {
username: self.username,
config,
enabled: true,
admin: false,
super_admin: false,
create_server_permissions: false,
create_build_permissions: false,
last_update_view: 0,
recents: Default::default(),
all: Default::default(),
config,
updated_at: komodo_timestamp(),
};
});
user.id = db_client()
.users
.insert_one(&user)
@@ -58,29 +66,48 @@ impl Resolve<WriteArgs> for CreateServiceUser {
.as_object_id()
.context("inserted id is not object id")?
.to_string();
Ok(user)
}
}
impl Resolve<WriteArgs> for UpdateServiceUserDescription {
#[instrument(name = "UpdateServiceUserDescription", skip(user))]
#[instrument(
"UpdateServiceUserDescription",
skip_all,
fields(
operator = user.id,
username = self.username,
description = self.description,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateServiceUserDescriptionResponse> {
) -> mogh_error::Result<UpdateServiceUserDescriptionResponse> {
if !user.admin {
return Err(anyhow!("user not admin").into());
return Err(
anyhow!("Only Admins can manage Service Users")
.status_code(StatusCode::FORBIDDEN),
);
}
let db = db_client();
let service_user = db
.users
.find_one(doc! { "username": &self.username })
.await
.context("failed to query db for user")?
.context("no user with given username")?;
.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").into());
return Err(
anyhow!("Target user is not Service User")
.status_code(StatusCode::FORBIDDEN),
);
};
db.users
.update_one(
doc! { "username": &self.username },
@@ -88,66 +115,107 @@ impl Resolve<WriteArgs> for UpdateServiceUserDescription {
)
.await
.context("failed to update user on db")?;
let res = db
let service_user = db
.users
.find_one(doc! { "username": &self.username })
.await
.context("failed to query db for user")?
.context("user with username not found")?;
Ok(res)
Ok(service_user)
}
}
impl Resolve<WriteArgs> for CreateApiKeyForServiceUser {
#[instrument(name = "CreateApiKeyForServiceUser", skip(user))]
#[instrument(
"CreateApiKeyForServiceUser",
skip_all,
fields(
operator = user.id,
service_user = self.user_id,
name = self.name,
expires = self.expires,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateApiKeyForServiceUserResponse> {
) -> mogh_error::Result<CreateApiKeyForServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin").into());
return Err(
anyhow!("Only Admins can manage Service Users")
.status_code(StatusCode::FORBIDDEN),
);
}
let service_user =
find_one_by_id(&db_client().users, &self.user_id)
.await
.context("failed to query db for user")?
.context("no user found with id")?;
.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").into());
return Err(
anyhow!("Target user is not Service User")
.status_code(StatusCode::FORBIDDEN),
);
};
CreateApiKey {
name: self.name,
expires: self.expires,
}
.resolve(&UserArgs { user: service_user })
create_api_key(
&KomodoAuthImpl,
service_user.id,
CreateApiKey {
name: self.name,
expires: self.expires as u64,
},
)
.await
}
}
impl Resolve<WriteArgs> for DeleteApiKeyForServiceUser {
#[instrument(name = "DeleteApiKeyForServiceUser", skip(user))]
#[instrument(
"DeleteApiKeyForServiceUser",
skip_all,
fields(
operator = user.id,
key = self.key,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteApiKeyForServiceUserResponse> {
) -> mogh_error::Result<DeleteApiKeyForServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin").into());
return Err(
anyhow!("Only Admins can manage Service Users")
.status_code(StatusCode::FORBIDDEN),
);
}
let db = db_client();
let api_key = db
.api_keys
.find_one(doc! { "key": &self.key })
.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().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").into());
return Err(
anyhow!("Target user is not Service User")
.status_code(StatusCode::FORBIDDEN),
);
};
db.api_keys
.delete_one(doc! { "key": self.key })
.await

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,108 @@
use komodo_client::{
api::write::*,
entities::{
permission::PermissionLevel, swarm::Swarm, update::Update,
},
};
use mogh_resolver::Resolve;
use crate::{permission::get_check_permissions, resource};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateSwarm {
#[instrument(
"CreateSwarm",
skip_all,
fields(
operator = user.id,
swarm = self.name,
config = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<Swarm> {
resource::create::<Swarm>(&self.name, self.config, None, user)
.await
}
}
impl Resolve<WriteArgs> for CopySwarm {
#[instrument(
"CopySwarm",
skip_all,
fields(
operator = user.id,
swarm = self.name,
copy_swarm = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<Swarm> {
let Swarm { config, .. } = get_check_permissions::<Swarm>(
&self.id,
user,
PermissionLevel::Read.into(),
)
.await?;
resource::create::<Swarm>(&self.name, config.into(), None, user)
.await
}
}
impl Resolve<WriteArgs> for DeleteSwarm {
#[instrument(
"DeleteSwarm",
skip_all,
fields(
operator = user.id,
swarm = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<Swarm> {
Ok(resource::delete::<Swarm>(&self.id, user).await?)
}
}
impl Resolve<WriteArgs> for UpdateSwarm {
#[instrument(
"UpdateSwarm",
skip_all,
fields(
operator = user.id,
swarm = self.id,
update = serde_json::to_string(&self.config).unwrap()
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<Swarm> {
Ok(resource::update::<Swarm>(&self.id, self.config, user).await?)
}
}
impl Resolve<WriteArgs> for RenameSwarm {
#[instrument(
"RenameSwarm",
skip_all,
fields(
operator = user.id,
swarm = self.id,
new_name = self.name
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<Update> {
Ok(resource::rename::<Swarm>(&self.id, &self.name, user).await?)
}
}

View File

@@ -12,14 +12,13 @@ use formatting::format_serror;
use komodo_client::{
api::{read::ExportAllResourcesToToml, write::*},
entities::{
self, NoData, Operation, RepoExecutionArgs, ResourceTarget,
self, Operation, RepoExecutionArgs, ResourceTarget,
action::Action,
alert::{Alert, AlertData, SeverityLevel},
alerter::Alerter,
all_logs_success,
build::Build,
builder::Builder,
config::core::CoreConfig,
deployment::Deployment,
komodo_timestamp,
permission::PermissionLevel,
@@ -27,19 +26,14 @@ use komodo_client::{
repo::Repo,
server::Server,
stack::Stack,
sync::{
PartialResourceSyncConfig, ResourceSync, ResourceSyncInfo,
SyncDeployUpdate,
},
sync::{ResourceSync, ResourceSyncInfo, SyncDeployUpdate},
to_path_compatible_name,
update::{Log, Update},
user::sync_user,
},
};
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
};
use resolver_api::Resolve;
use mogh_resolver::Resolve;
use tracing::Instrument;
use crate::{
alert::send_alerts,
@@ -53,7 +47,7 @@ use crate::{
},
permission::get_check_permissions,
resource,
state::{db_client, github_client},
state::db_client,
sync::{
deploy::SyncDeployParams, remote::RemoteResources,
view::push_updates_for_view,
@@ -63,22 +57,43 @@ use crate::{
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateResourceSync {
#[instrument(name = "CreateResourceSync", skip(user))]
#[instrument(
"CreateResourceSync",
skip_all,
fields(
operator = user.id,
sync = self.name,
config = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ResourceSync> {
resource::create::<ResourceSync>(&self.name, self.config, user)
.await
) -> mogh_error::Result<ResourceSync> {
resource::create::<ResourceSync>(
&self.name,
self.config,
None,
user,
)
.await
}
}
impl Resolve<WriteArgs> for CopyResourceSync {
#[instrument(name = "CopyResourceSync", skip(user))]
#[instrument(
"CopyResourceSync",
skip_all,
fields(
operator = user.id,
sync = self.name,
copy_sync = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ResourceSync> {
) -> mogh_error::Result<ResourceSync> {
let ResourceSync { config, .. } =
get_check_permissions::<ResourceSync>(
&self.id,
@@ -86,27 +101,47 @@ impl Resolve<WriteArgs> for CopyResourceSync {
PermissionLevel::Write.into(),
)
.await?;
resource::create::<ResourceSync>(&self.name, config.into(), user)
.await
resource::create::<ResourceSync>(
&self.name,
config.into(),
None,
user,
)
.await
}
}
impl Resolve<WriteArgs> for DeleteResourceSync {
#[instrument(name = "DeleteResourceSync", skip(args))]
#[instrument(
"DeleteResourceSync",
skip_all,
fields(
operator = user.id,
sync = self.id,
)
)]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<ResourceSync> {
Ok(resource::delete::<ResourceSync>(&self.id, args).await?)
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<ResourceSync> {
Ok(resource::delete::<ResourceSync>(&self.id, user).await?)
}
}
impl Resolve<WriteArgs> for UpdateResourceSync {
#[instrument(name = "UpdateResourceSync", skip(user))]
#[instrument(
"UpdateResourceSync",
skip_all,
fields(
operator = user.id,
sync = self.id,
update = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ResourceSync> {
) -> mogh_error::Result<ResourceSync> {
Ok(
resource::update::<ResourceSync>(&self.id, self.config, user)
.await?,
@@ -115,11 +150,19 @@ impl Resolve<WriteArgs> for UpdateResourceSync {
}
impl Resolve<WriteArgs> for RenameResourceSync {
#[instrument(name = "RenameResourceSync", skip(user))]
#[instrument(
"RenameResourceSync",
skip_all,
fields(
operator = user.id,
sync = self.id,
new_name = self.name
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
Ok(
resource::rename::<ResourceSync>(&self.id, &self.name, user)
.await?,
@@ -128,8 +171,20 @@ impl Resolve<WriteArgs> for RenameResourceSync {
}
impl Resolve<WriteArgs> for WriteSyncFileContents {
#[instrument(name = "WriteSyncFileContents", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
#[instrument(
"WriteSyncFileContents",
skip_all,
fields(
operator = args.user.id,
sync = self.sync,
resource_path = self.resource_path,
file_path = self.file_path,
)
)]
async fn resolve(
self,
args: &WriteArgs,
) -> mogh_error::Result<Update> {
let sync = get_check_permissions::<ResourceSync>(
&self.sync,
&args.user,
@@ -173,12 +228,13 @@ impl Resolve<WriteArgs> for WriteSyncFileContents {
}
}
#[instrument("WriteSyncFileContentsOnHost", skip_all)]
async fn write_sync_file_contents_on_host(
req: WriteSyncFileContents,
args: &WriteArgs,
sync: ResourceSync,
mut update: Update,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
let WriteSyncFileContents {
sync: _,
resource_path,
@@ -196,15 +252,7 @@ async fn write_sync_file_contents_on_host(
.context("Invalid resource path")?;
let full_path = root.join(&resource_path).join(&file_path);
if let Some(parent) = full_path.parent() {
tokio::fs::create_dir_all(parent).await.with_context(|| {
format!(
"Failed to initialize resource file parent directory {parent:?}"
)
})?;
}
if let Err(e) = tokio::fs::write(&full_path, &contents)
if let Err(e) = mogh_secret_file::write_async(&full_path, &contents)
.await
.with_context(|| {
format!(
@@ -243,13 +291,14 @@ async fn write_sync_file_contents_on_host(
Ok(update)
}
#[instrument("WriteSyncFileContentsGit", skip_all)]
async fn write_sync_file_contents_git(
req: WriteSyncFileContents,
args: &WriteArgs,
sync: ResourceSync,
repo: Option<Repo>,
mut update: Update,
) -> serror::Result<Update> {
) -> mogh_error::Result<Update> {
let WriteSyncFileContents {
sync: _,
resource_path,
@@ -394,8 +443,18 @@ async fn write_sync_file_contents_git(
}
impl Resolve<WriteArgs> for CommitSync {
#[instrument(name = "CommitSync", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
#[instrument(
"CommitSync",
skip_all,
fields(
operator = args.user.id,
sync = self.sync,
)
)]
async fn resolve(
self,
args: &WriteArgs,
) -> mogh_error::Result<Update> {
let WriteArgs { user } = args;
let sync = get_check_permissions::<entities::sync::ResourceSync>(
@@ -481,16 +540,14 @@ impl Resolve<WriteArgs> for CommitSync {
.sync_directory
.join(to_path_compatible_name(&sync.name))
.join(&resource_path);
if let Some(parent) = file_path.parent() {
tokio::fs::create_dir_all(parent)
let span = info_span!("CommitSyncOnHost");
if let Err(e) =
mogh_secret_file::write_async(&file_path, &res.toml)
.instrument(span)
.await
.with_context(|| format!("Failed to initialize resource file parent directory {parent:?}"))?;
};
if let Err(e) = tokio::fs::write(&file_path, &res.toml)
.await
.with_context(|| {
format!("Failed to write resource file to {file_path:?}",)
})
.with_context(|| {
format!("Failed to write resource file to {file_path:?}",)
})
{
update.push_error_log(
"Write resource file",
@@ -579,6 +636,7 @@ impl Resolve<WriteArgs> for CommitSync {
}
}
#[instrument("CommitSyncGit", skip_all)]
async fn commit_git_sync(
mut args: RepoExecutionArgs,
resource_path: &Path,
@@ -623,15 +681,10 @@ async fn commit_git_sync(
}
impl Resolve<WriteArgs> for RefreshResourceSyncPending {
#[instrument(
name = "RefreshResourceSyncPending",
level = "debug",
skip(user)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ResourceSync> {
) -> mogh_error::Result<ResourceSync> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// sync should be able to do this.
let mut sync =
@@ -972,215 +1025,3 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
Ok(crate::resource::get::<ResourceSync>(&sync.id).await?)
}
}
impl Resolve<WriteArgs> for CreateSyncWebhook {
#[instrument(name = "CreateSyncWebhook", skip(args))]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<CreateSyncWebhookResponse> {
let WriteArgs { user } = args;
let Some(github) = github_client() else {
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let sync = get_check_permissions::<ResourceSync>(
&self.sync,
user,
PermissionLevel::Write.into(),
)
.await?;
if sync.config.repo.is_empty() {
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = sync.config.repo.split('/');
let owner = split.next().context("Sync repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
// First make sure the webhook isn't already created (inactive ones are ignored)
let webhooks = github_repos
.list_all_webhooks(owner, repo)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
webhook_secret,
..
} = core_config();
let webhook_secret = if sync.config.webhook_secret.is_empty() {
webhook_secret
} else {
&sync.config.webhook_secret
};
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = match self.action {
SyncWebhookAction::Refresh => {
format!("{host}/listener/github/sync/{}/refresh", sync.id)
}
SyncWebhookAction::Sync => {
format!("{host}/listener/github/sync/{}/sync", sync.id)
}
};
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
return Ok(NoData {});
}
}
// Now good to create the webhook
let request = ReposCreateWebhookRequest {
active: Some(true),
config: Some(ReposCreateWebhookRequestConfig {
url,
secret: webhook_secret.to_string(),
content_type: String::from("json"),
insecure_ssl: None,
digest: Default::default(),
token: Default::default(),
}),
events: vec![String::from("push")],
name: String::from("web"),
};
github_repos
.create_webhook(owner, repo, &request)
.await
.context("failed to create webhook")?;
if !sync.config.webhook_enabled {
UpdateResourceSync {
id: sync.id,
config: PartialResourceSyncConfig {
webhook_enabled: Some(true),
..Default::default()
},
}
.resolve(args)
.await
.map_err(|e| e.error)
.context("failed to update sync to enable webhook")?;
}
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteSyncWebhook {
#[instrument(name = "DeleteSyncWebhook", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteSyncWebhookResponse> {
let Some(github) = github_client() else {
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let sync = get_check_permissions::<ResourceSync>(
&self.sync,
user,
PermissionLevel::Write.into(),
)
.await?;
if sync.config.git_provider != "github.com" {
return Err(
anyhow!("Can only manage github.com repo webhooks").into(),
);
}
if sync.config.repo.is_empty() {
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = sync.config.repo.split('/');
let owner = split.next().context("Sync repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
split.next().context("Sync repo has no repo after the /")?;
let github_repos = github.repos();
// First make sure the webhook isn't already created (inactive ones are ignored)
let webhooks = github_repos
.list_all_webhooks(owner, repo)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = match self.action {
SyncWebhookAction::Refresh => {
format!("{host}/listener/github/sync/{}/refresh", sync.id)
}
SyncWebhookAction::Sync => {
format!("{host}/listener/github/sync/{}/sync", sync.id)
}
};
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
github_repos
.delete_webhook(owner, repo, webhook.id)
.await
.context("failed to delete webhook")?;
return Ok(NoData {});
}
}
// No webhook to delete, all good
Ok(NoData {})
}
}

Some files were not shown because too many files have changed in this diff Show More