Compare commits

..
547 Commits
Author SHA1 Message Date
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
mbecker20andMaxwell Becker 973aae82fd improve 2fa ux 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker c3626e0949 cli support clearing users 2fa methods 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 5ba96a58e4 binaries use bookworm 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker c6a1cf2867 deploy 2.0.0-dev-98 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker bd39aec1fd support passkey 2fa 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 079895a64d fix imports in permissions module 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 5af1c646a5 deploy 2.0.0-dev-97 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 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
mbecker20andMaxwell Becker 78c6761eca add swarm specific permission config 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker a949891d26 apply login credential lock (for demo users) to 2FA 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 9d256e420a improve totp enrollment dialog closing, fix redirect away from /login when using two factor 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker b460504b5a deploy 2.0.0-dev-96 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker e60316f2c1 TOTP 2FA with third party logins 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker c45e3eab16 deploy 2.0.0-dev-95 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker ec12f63272 double recovery code length 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker f2f77b4eb6 totp unenroll 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 677a65f8b0 local login TOTP 2fa support 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker a5a1090177 deploy 2.0.0-dev-94 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker ccb18fc88a fix server auto rotate keys default for sync 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 0ca583fe5e deploy Deployments to swarms 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 4013cd3f1e dev-93 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 5854a7de99 Deployment as swarm service 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 8c52232852 swarm stack deploy and destroy 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker fb9e5b1860 ts types 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker a1c00a8d32 implement the swarm resource associations for stack / deployment targeting swarm 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 775f5bf703 more stack / deployment deploy on swarm 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker effe737ffe periphery swarm stack deploy 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 436959cbcd docker service create 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 9957287443 remove buttons for swarm resources 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker c77de8683c swarm stack logs and Remove 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 087bcb7044 improve swarm stack page 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 0dfcaaf06a add swarm remove executions to procedure UI excluded types 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 6624a821a5 deploy 2.0.0-dev-92 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker c32af84e02 remove swarm entities apis 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 9d9add7b34 Uppercase server docker ops logs 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker db099dbe1e improve config with tall linkto sidebars 2025-12-10 21:39:56 -08:00
mbecker20andMaxwell Becker 413a8abc84 default rate limit attempts to 5 per 15 sec 2025-12-10 21:34:30 -08:00
mbecker20andMaxwell Becker 36243e83c1 KL-4 must fallback to axum extracted IP for cases not using reverse proxy 2025-12-10 21:34:30 -08:00
mbecker20andMaxwell Becker 076113a1de KL-4: Align log consistency 2025-12-10 21:34:30 -08:00
mbecker20andMaxwell Becker c998edaa73 KL-1: core_allowed_origins example config should be empty 2025-12-10 21:34:30 -08:00
mbecker20andMaxwell Becker 4de32b08e5 KL-8 modify action state internal behavior comments 2025-12-10 21:34:30 -08:00
mbecker20andMaxwell Becker 262999c58f KL-7 Improve typescript safety: disable allow any 2025-12-10 21:34:30 -08:00
mbecker20andMaxwell Becker be57f52e6e KL-6 Improve error handling in startup paths 2025-12-10 21:34:30 -08:00
mbecker20andMaxwell Becker 3591789316 KL-6 Improve alert cache error handling 2025-12-10 21:34:30 -08:00
mbecker20andMaxwell Becker 83f2bd65fe KL-6 Remove monitoring_interval panic 2025-12-10 21:34:30 -08:00
mbecker20andMaxwell Becker 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
mbecker20andMaxwell Becker 721038a1df KL-5 JWT clock skew tolerance 2025-12-10 21:34:30 -08:00
mbecker20andMaxwell Becker 7de98ad519 cargo clippy and bump to rust 1.91.1 2025-12-10 21:34:30 -08:00
mbecker20andMaxwell Becker 8c62f2b5c5 KL-4 ext 2: Improve rate limiting / attempt state conveyance with response 2025-12-10 21:34:30 -08:00
mbecker20andMaxwell Becker 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
mbecker20andMaxwell Becker 25e2d4e926 KL-4 Authentication rate limiting 2025-12-10 21:34:30 -08:00
mbecker20andMaxwell Becker b9b8d45cbc KL-2/3 Input validation for local auth, service users, api keys, and variables 2025-12-10 21:34:30 -08:00
mbecker20andMaxwell Becker 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
ChanningHeandGitHub 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
ChanningHeandGitHub 0b7f28360f Add optional command wrapper for Docker Compose in StackConfig (#973) 2025-11-10 23:59:09 -08:00
ChanningHeandGitHub 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 SinghandGitHub 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
908 changed files with 59078 additions and 66238 deletions
+1 -4
View File
@@ -1,5 +1,2 @@
[alias]
xtask = "run --package xtask --"
[build]
rustflags = ["-Wunused-crate-dependencies"]
rustflags = ["-Wunused-crate-dependencies"]
+3 -9
View File
@@ -1,6 +1,6 @@
services:
dev:
image: mcr.microsoft.com/devcontainers/rust:1-bookworm
image: mcr.microsoft.com/devcontainers/rust:1-1-bullseye
volumes:
# Mount the root folder that contains .git
- ../:/workspace:cached
@@ -10,19 +10,13 @@ services:
- stacks:/etc/komodo/stacks
command: sleep infinity
ports:
- "9120:9120"
- "9121:9121"
environment:
KOMODO_HOST: http://localhost:9120
KOMODO_FIRST_SERVER: http://localhost:8120
KOMODO_DATABASE_ADDRESS: db
KOMODO_ENABLE_NEW_USERS: true
KOMODO_LOCAL_AUTH: true
KOMODO_JWT_SECRET: a_random_secret
VITE_KOMODO_HOST: http://localhost:9120
KOMODO_CORS_ALLOWED_ORIGINS: http://localhost:5173
KOMODO_CORS_ALLOW_CREDENTIALS: true
PERIPHERY_SSL_ENABLED: false
KOMODO_SESSION_ALLOW_CROSS_SITE: true
links:
- db
# ...
@@ -36,4 +30,4 @@ volumes:
data:
repo-cache:
repos:
stacks:
stacks:
+3 -9
View File
@@ -10,7 +10,7 @@
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "22.12.0"
"version": "20.12.2"
},
"ghcr.io/devcontainers-community/features/deno:1": {
@@ -28,8 +28,7 @@
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
5173,
9120
9121
],
// Use 'postCreateCommand' to run commands after the container is created.
@@ -37,16 +36,11 @@
"runServices": [
"db"
],
]
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
"remoteEnv": {
// Avoid out of memory error when running `yarn build`
"NODE_OPTIONS": "--max-old-space-size=4096"
}
}
+1 -7
View File
@@ -1,9 +1,3 @@
#!/bin/sh
cargo install typeshare-cli
sudo mkdir -p /etc/komodo/keys
sudo chown -R $(whoami) /etc/komodo
sudo mkdir -p /config/keys
sudo chown -R $(whoami) /config
cargo install typeshare-cli
+123
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`
-56
View File
@@ -1,56 +0,0 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
jobs:
build:
name: Build & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
# - name: Cache cargo registry
# uses: actions/cache@v4
# with:
# path: |
# ~/.cargo/registry
# ~/.cargo/git
# target
# key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# restore-keys: |
# ${{ runner.os }}-cargo-
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
fmt:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
components: rustfmt
- name: Check formatting
run: cargo fmt --all -- --check
+14 -14
View File
@@ -106,62 +106,62 @@
"problemMatcher": []
},
{
"label": "Init UI Client",
"label": "Init Frontend Client",
"type": "shell",
"command": "yarn link komodo_client && yarn install",
"options": {
"cwd": "${workspaceFolder}/ui",
"cwd": "${workspaceFolder}/frontend",
},
"problemMatcher": []
},
{
"label": "Init UI",
"label": "Init Frontend",
"dependsOn": [
"Build TS Client Types",
"Init TS Client",
"Init UI Client"
"Init Frontend Client"
],
"dependsOrder": "sequence",
"problemMatcher": []
},
{
"label": "Build UI",
"label": "Build Frontend",
"type": "shell",
"command": "yarn build",
"options": {
"cwd": "${workspaceFolder}/ui",
"cwd": "${workspaceFolder}/frontend",
},
"problemMatcher": []
},
{
"label": "Prepare UI For Run",
"label": "Prepare Frontend For Run",
"type": "shell",
"command": "cp -r ./client/core/ts/dist/. ui/public/client/.",
"command": "cp -r ./client/core/ts/dist/. frontend/public/client/.",
"options": {
"cwd": "${workspaceFolder}",
},
"dependsOn": [
"Build TS Client Types",
"Build UI"
"Build Frontend"
],
"dependsOrder": "sequence",
"problemMatcher": []
},
{
"label": "Run UI",
"label": "Run Frontend",
"type": "shell",
"command": "yarn dev",
"options": {
"cwd": "${workspaceFolder}/ui",
"cwd": "${workspaceFolder}/frontend",
},
"dependsOn": ["Prepare UI For Run"],
"dependsOn": ["Prepare Frontend For Run"],
"problemMatcher": []
},
{
"label": "Init",
"dependsOn": [
"Build Backend",
"Init UI"
"Init Frontend"
],
"dependsOrder": "sequence",
"problemMatcher": []
@@ -171,7 +171,7 @@
"dependsOn": [
"Run Core",
"Run Periphery",
"Run UI"
"Run Frontend"
],
"problemMatcher": []
},
Generated
+1067 -1532
View File
File diff suppressed because it is too large Load Diff
+62 -66
View File
@@ -3,18 +3,21 @@ resolver = "2"
members = [
"bin/*",
"lib/*",
"xtask",
"client/core/rs",
"client/periphery/rs",
]
[workspace.package]
version = "0.0.0"
version = "2.0.0-dev-112"
edition = "2024"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
repository = "https://github.com/moghtech/komodo"
homepage = "https://komo.do"
[profile.release]
strip = "debuginfo"
[workspace.dependencies]
# LOCAL
komodo_client = { path = "client/core/rs" }
@@ -30,113 +33,106 @@ git = { path = "lib/git" }
# MOGH
slack = { version = "2.0.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
mogh_error = { version = "1.0.4", default-features = false }
mogh_error = { version = "1.0.3", default-features = false }
derive_default_builder = "0.1.8"
async_timing_util = "1.1.0"
mogh_auth_client = "1.7.0"
mogh_auth_server = "1.8.0"
mogh_secret_file = "1.0.1"
mogh_validations = "1.0.1"
mogh_rate_limit = "1.0.1"
partial_derive2 = "0.5.0"
mongo_indexed = "2.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"
mogh_resolver = "1.0.0"
mogh_config = "1.4.2"
mogh_logger = "1.3.4"
mogh_server = "1.5.0"
mogh_config = "1.0.1"
mogh_logger = "1.3.0"
mogh_server = "1.1.2"
toml_pretty = "2.0.0"
mogh_cache = "1.1.2"
mogh_pki = "1.1.3"
mungos = "3.7.0"
mogh_cache = "1.1.1"
mogh_pki = "1.1.0"
mungos = "3.2.2"
svi = "1.2.0"
# ASYNC
reqwest = { version = "0.13.4", default-features = false, features = ["json", "stream", "form", "query", "rustls"] }
tokio = { version = "1.53.1", features = ["full"] }
tokio-util = { version = "0.7.19", features = ["io", "codec"] }
tokio-stream = { version = "0.1.19", features = ["sync"] }
pin-project-lite = "0.2.17"
futures-util = "0.3.34"
arc-swap = "1.9.2"
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-util = "0.3.31"
arc-swap = "1.8.0"
# SERVER
tokio-tungstenite = { version = "0.30.0", features = ["rustls-tls-native-roots"] }
axum = { version = "0.8.9", features = ["ws", "json", "macros"] }
axum-extra = { version = "0.12.6", features = ["typed-header"] }
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.5.0"
utoipa = "5.4.0"
# SER/DE
ipnetwork = { version = "0.21.1", features = ["serde"] }
indexmap = { version = "2.14.0", features = ["serde"] }
serde = { version = "1.0.229", features = ["derive"] }
strum = { version = "0.28.0", 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 = "1.1.4"
toml = "0.9.11"
serde_yaml_ng = "0.10.0"
serde_json = "1.0.151"
serde_qs = "1.1.3"
url = "2.5.8"
serde_json = "1.0.148"
serde_qs = "0.15.0"
url = "2.5.7"
# ERROR
anyhow = "1.0.104"
thiserror = "2.0.20"
# SYSTEM
libc = "0.2.189"
anyhow = "1.0.100"
thiserror = "2.0.18"
# LOGGING
tracing = "0.1.44"
# CONFIG
clap = { version = "4.6.6", features = ["derive"] }
clap = { version = "4.5.54", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
# CRYPTO / AUTH
uuid = { version = "1.25.0", features = ["v4", "fast-rng", "serde"] }
rustls = { version = "0.23.43", features = ["aws-lc-rs"] }
data-encoding = "2.11.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"
bcrypt = "0.19.3"
hmac = "0.13.0"
sha1 = "0.11.0"
sha2 = "0.11.0"
rand = "0.10.2"
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.26.1"
hickory-resolver = "0.25.2"
portable-pty = "0.9.0"
shell-escape = "0.1.5"
crossterm = "0.29.0"
bollard = "0.21.1"
sysinfo = "0.39.6"
shlex = "2.0.1"
bollard = "0.20.0"
sysinfo = "0.37.1"
shlex = "1.3.0"
# CLOUD
aws-config = "1.11.0"
aws-sdk-ec2 = "1.252.0"
aws-credential-types = "1.3.0"
aws-config = "1.8.12"
aws-sdk-ec2 = "1.200.0"
aws-credential-types = "1.2.11"
## CRON
english-to-cron = "0.1.7"
chrono-tz = "0.10.4"
chrono = "0.4.45"
croner = "4.0.0"
chrono = "0.4.43"
croner = "3.0.1"
# MISC
async-compression = { version = "0.4.43", features = ["tokio", "gzip"] }
schemars = { version = "1.2.2", features = ["indexmap2"] }
async-compression = { version = "0.4.37", features = ["tokio", "gzip"] }
derive_builder = "0.20.2"
comfy-table = "8.0.0"
comfy-table = "7.2.2"
typeshare = "1.0.5"
wildcard = "0.3.0"
colored = "3.1.1"
bytes = "1.12.1"
regex = "1.13.1"
[profile.release]
strip = "debuginfo"
colored = "3.0.0"
bytes = "1.11.0"
regex = "1.12.2"
+5 -1
View File
@@ -1 +1,5 @@
{}
{
"imports": {
"@std/toml": "jsr:@std/toml"
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
const cmd = "km run -y action deploy-komodo-ui-change";
const cmd = "km run -y action deploy-komodo-fe-change";
new Deno.Command("bash", {
args: ["-c", cmd],
}).spawn();
+42 -1
View File
@@ -1,3 +1,5 @@
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"],
@@ -5,7 +7,46 @@ export const run = async (action: string) => {
.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", `km run -y action ${action} "BRANCH=${branch}"`],
args: ["-c", cmd],
}).spawn();
};
+2 -8
View File
@@ -1,8 +1,8 @@
## Builds the Komodo Core, Periphery, and Util binaries
## for a specific architecture. Requires OpenSSL 3 or later.
FROM rust:1.98.0-bookworm AS builder
RUN cargo install cargo-strip cargo-edit
FROM rust:1.91.1-bookworm AS builder
RUN cargo install cargo-strip
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -12,12 +12,6 @@ COPY ./client/periphery ./client/periphery
COPY ./bin/core ./bin/core
COPY ./bin/periphery ./bin/periphery
COPY ./bin/cli ./bin/cli
COPY ./xtask ./xtask
# Set Version
ARG VERSION="0.0.0"
ARG IMAGE_TAG=""
RUN cargo set-version -p komodo_core ${VERSION}${IMAGE_TAG:+-${IMAGE_TAG}}
# Compile bin
RUN \
+4 -12
View File
@@ -3,7 +3,7 @@
## Uses chef for dependency caching to help speed up back-to-back builds.
FROM lukemathwalker/cargo-chef:latest-rust-1.98.0-bookworm 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
@@ -12,20 +12,12 @@ COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
RUN cargo install cargo-strip cargo-edit
# Build JUST dependencies - cached layer
RUN cargo install cargo-strip
COPY --from=planner /builder/recipe.json recipe.json
# Build JUST dependencies - cached layer
RUN cargo chef cook --release --recipe-path recipe.json
# NOW copy again (this time into builder), set version, and build app
# NOW copy again (this time into builder) and build app
COPY . .
# Set Version
ARG VERSION="0.0.0"
ARG IMAGE_TAG=""
RUN cargo set-version -p komodo_core ${VERSION}${IMAGE_TAG:+-${IMAGE_TAG}}
RUN \
cargo build --release --bin core && \
cargo build --release --bin periphery && \
+1 -2
View File
@@ -3,6 +3,7 @@ name = "komodo_cli"
description = "Command line tool for Komodo"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
@@ -31,9 +32,7 @@ tracing.workspace = true
colored.workspace = true
dotenvy.workspace = true
anyhow.workspace = true
bcrypt.workspace = true
chrono.workspace = true
rustls.workspace = true
tokio.workspace = true
serde.workspace = true
clap.workspace = true
+2 -7
View File
@@ -1,5 +1,5 @@
FROM rust:1.98.0-trixie AS builder
RUN cargo install cargo-strip cargo-edit
FROM rust:1.91.1-bullseye AS builder
RUN cargo install cargo-strip
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -8,11 +8,6 @@ COPY ./client/core/rs ./client/core/rs
COPY ./client/periphery ./client/periphery
COPY ./bin/cli ./bin/cli
# Set Version
ARG VERSION="0.0.0"
ARG IMAGE_TAG=""
RUN cargo set-version -p komodo_cli ${VERSION}${IMAGE_TAG:+-${IMAGE_TAG}}
# Compile bin
RUN cargo build -p komodo_cli --release && cargo strip
+2 -2
View File
@@ -1,7 +1,7 @@
## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile).
## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:2
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64
ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64
@@ -9,7 +9,7 @@ ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64
FROM ${X86_64_BINARIES} AS x86_64
FROM ${AARCH64_BINARIES} AS aarch64
FROM debian:trixie-slim
FROM debian:bullseye-slim
WORKDIR /app
+1 -1
View File
@@ -1,6 +1,6 @@
## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:2
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
# This is required to work with COPY --from
FROM ${BINARIES_IMAGE} AS binaries
+27 -25
View File
@@ -7,16 +7,16 @@ use futures_util::{
FutureExt, TryStreamExt, stream::FuturesUnordered,
};
use komodo_client::{
api::read::{self, ListAllContainers, ListServers},
api::read::{
InspectDockerContainer, ListAllDockerContainers, ListServers,
},
entities::{
config::cli::args::container::{
Container, ContainerCommand, InspectContainer,
},
docker::{
self,
container::{
ContainerListItem, ContainerSortBy, ContainerStateStatusEnum,
},
container::{ContainerListItem, ContainerStateStatusEnum},
},
},
};
@@ -44,12 +44,11 @@ async fn list_containers(
down,
links,
reverse,
containers: terms,
containers: names,
images,
networks,
servers,
format,
page,
command: _,
}: &Container,
) -> anyhow::Result<()> {
@@ -61,16 +60,9 @@ async fn list_containers(
.into_iter()
.map(|s| (s.id.clone(), s))
.collect::<HashMap<_, _>>())),
client.read(ListAllContainers {
client.read(ListAllDockerContainers {
servers: Default::default(),
tags: Default::default(),
terms: terms.clone(),
state: Default::default(),
limit: Some(100),
// Page is more naturally given starting as 1, 2, 3.
page: if *page == 0 { 0 } else { *page - 1 },
sort_by: ContainerSortBy::Server,
sort_desc: false,
containers: Default::default(),
}),
)?;
@@ -86,6 +78,7 @@ async fn list_containers(
(Some(server.name.as_str()), c)
});
let names = parse_wildcards(names);
let servers = parse_wildcards(servers);
let images = parse_wildcards(images);
let networks = parse_wildcards(networks);
@@ -112,6 +105,7 @@ async fn list_containers(
);
state_check
&& network_check
&& matches_wildcards(&names, &[c.name.as_str()])
&& matches_wildcards(
&servers,
&server_name
@@ -151,15 +145,9 @@ pub async fn inspect_container(
.into_iter()
.map(|s| (s.id.clone(), s))
.collect::<HashMap<_, _>>())),
client.read(ListAllContainers {
servers: inspect.servers.clone(),
tags: Default::default(),
terms: vec![inspect.container.clone()],
state: Default::default(),
limit: Some(0),
page: 0,
sort_by: ContainerSortBy::Server,
sort_desc: false,
client.read(ListAllDockerContainers {
servers: Default::default(),
containers: Default::default()
}),
)?;
@@ -174,11 +162,25 @@ pub async fn inspect_container(
c.server_id = Some(server.name.clone());
});
let names = [inspect.container.to_string()];
let names = parse_wildcards(&names);
let servers = parse_wildcards(&inspect.servers);
let mut containers = containers
.into_iter()
.filter(|c| {
matches_wildcards(&names, &[c.name.as_str()])
&& matches_wildcards(
&servers,
&c.server_id
.as_deref()
.map(|i| vec![i])
.unwrap_or_default(),
)
})
.map(|c| async move {
client
.read(read::InspectContainer {
.read(InspectDockerContainer {
container: c.name,
server: c.server_id.context("No server...")?,
})
-13
View File
@@ -1,13 +0,0 @@
use anyhow::Context as _;
use komodo_client::api::read::GetCoreInfo;
pub async fn handle() -> anyhow::Result<()> {
let client = super::komodo_client().await?;
let info = client.read(GetCoreInfo {}).await?;
println!(
"{}",
serde_json::to_string_pretty(&info)
.context("Failed to serialize core info to JSON")?
);
Ok(())
}
-101
View File
@@ -1,101 +0,0 @@
use anyhow::Context as _;
use database::bson::doc;
use komodo_client::{
api::read::FindUser,
entities::{
api_key::ApiKey, config::cli::args::create::CreateApiKey,
komodo_timestamp, random_string,
},
};
use serde_json::json;
use crate::config::cli_config;
pub async fn create(
CreateApiKey {
name,
for_user,
expires,
use_api,
}: &CreateApiKey,
) -> anyhow::Result<()> {
let expires = if let Some(expires_days) = expires {
// now + expires in ms
komodo_timestamp() + expires_days * 24 * 60 * 60 * 1000
} else {
0
};
if *use_api {
// USE API
let client = crate::command::komodo_client().await?;
let keys = if let Some(username) = for_user {
// For service user
let user = client
.read(FindUser {
user: username.to_string(),
})
.await?;
client
.write(
komodo_client::api::write::CreateApiKeyForServiceUser {
user_id: user.id,
name: name.clone().unwrap_or_default(),
expires,
},
)
.await?
} else {
// For self
client
.auth_manage(komodo_client::api::auth::manage::CreateApiKey {
name: name.clone().unwrap_or_default(),
expires: expires as u64,
})
.await?
};
println!(
"{}",
serde_json::to_string_pretty(&keys)
.context("Failed to serialize api keys to JSON")?
);
} else {
// USE DATABASE
let db = database::Client::new(&cli_config().database).await?;
let user = db
.users
.find_one(doc! { "username": for_user })
.await
.context("Failed to query database for user")?
.context("No user found with given username")?;
let key = format!("K_{}_K", random_string(40));
let secret = format!("S_{}_S", random_string(40));
let hashed_secret = bcrypt::hash(&secret, 10)
.context("Failed at hashing secret string")?;
db.api_keys
.insert_one(&ApiKey {
name: name.clone().unwrap_or_default(),
user_id: user.id,
key: key.clone(),
secret: hashed_secret.clone(),
created_at: komodo_timestamp(),
expires,
})
.await?;
println!(
"{}",
serde_json::to_string_pretty(
&json!({ "key": key, "secret": secret })
)
.context("Failed to serialize api keys to JSON")?
);
}
Ok(())
}
-13
View File
@@ -1,13 +0,0 @@
use komodo_client::entities::config::cli::args::create::CreateCommand;
mod api_key;
mod onboarding_key;
pub async fn handle(command: &CreateCommand) -> anyhow::Result<()> {
match command {
CreateCommand::ApiKey(api_key) => api_key::create(api_key).await,
CreateCommand::OnboardingKey(onboarding_key) => {
onboarding_key::create(onboarding_key).await
}
}
}
@@ -1,46 +0,0 @@
use anyhow::Context as _;
use komodo_client::entities::{
config::cli::args::create::CreateOnboardingKey, komodo_timestamp,
};
pub async fn create(
CreateOnboardingKey {
name,
expires,
private_key,
tags,
privileged,
copy_server,
create_builder,
}: &CreateOnboardingKey,
) -> anyhow::Result<()> {
let expires = if let Some(expires_days) = expires {
// now + expires in ms
komodo_timestamp() + expires_days * 24 * 60 * 60 * 1000
} else {
0
};
// USE API
let client = crate::command::komodo_client().await?;
let key = client
.write(komodo_client::api::write::CreateOnboardingKey {
name: name.clone().unwrap_or_default(),
expires,
private_key: private_key.clone(),
tags: tags.clone(),
privileged: *privileged,
copy_server: copy_server.clone().unwrap_or_default(),
create_builder: *create_builder,
})
.await?;
println!(
"{}",
serde_json::to_string_pretty(&key)
.context("Failed to serialize onboarding key to JSON")?
);
Ok(())
}
+550 -133
View File
@@ -28,141 +28,558 @@ pub async fn handle(
}
println!("\n{}: Execution", "Mode".dimmed());
macro_rules! handle_execution {
(
execute: [$($ExecVariant:ident),* $(,)?],
batch: [$($BatchVariant:ident),* $(,)?],
) => {{
// Print data
match execution {
$(Execution::$ExecVariant(data) => println!("{}: {data:?}", "Data".dimmed()),)*
$(Execution::$BatchVariant(data) => println!("{}: {data:?}", "Data".dimmed()),)*
Execution::CommitSync(data) => println!("{}: {data:?}", "Data".dimmed()),
Execution::Sleep(data) => println!("{}: {data:?}", "Data".dimmed()),
Execution::None(data) => println!("{}: {data:?}", "Data".dimmed()),
}
$crate::command::wait_for_enter("run execution", yes)?;
info!("Running Execution...");
let client = $crate::command::komodo_client().await?;
// Execute and get result
match execution.clone() {
$(
Execution::$ExecVariant(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
)*
$(
Execution::$BatchVariant(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
)*
Execution::CommitSync(request) => client
.write(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::Sleep(request) => {
let duration =
Duration::from_millis(request.duration_ms as u64);
tokio::time::sleep(duration).await;
println!("Finished sleeping!");
std::process::exit(0)
}
Execution::None(_) => unreachable!(),
}
}};
match execution {
Execution::None(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunAction(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchRunAction(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunProcedure(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchRunProcedure(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchRunBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CancelBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::Deploy(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeploy(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PullDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RestartDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PauseDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::UnpauseDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StopDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DestroyDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDestroyDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CloneRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchCloneRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PullRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchPullRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BuildRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchBuildRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CancelRepoBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RestartContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PauseContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::UnpauseContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StopContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DestroyContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartAllContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RestartAllContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PauseAllContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::UnpauseAllContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StopAllContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeleteNetwork(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneNetworks(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeleteImage(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneImages(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeleteVolume(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneVolumes(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneDockerBuilders(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneBuildx(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneSystem(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunSync(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CommitSync(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeployStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeployStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeployStackIfChanged(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeployStackIfChanged(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PullStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchPullStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RestartStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PauseStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::UnpauseStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StopStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunStackService(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::TestAlerter(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::SendAlert(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RemoveSwarmNodes(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RemoveSwarmStacks(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RemoveSwarmServices(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CreateSwarmConfig(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RotateSwarmConfig(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RemoveSwarmConfigs(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CreateSwarmSecret(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RotateSwarmSecret(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RemoveSwarmSecrets(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::ClearRepoCache(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BackupCoreDatabase(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::GlobalAutoUpdate(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RotateAllServerKeys(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RotateCoreKeys(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::Sleep(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
}
let res = handle_execution!(
execute: [
RunAction,
CancelAction,
RunProcedure,
CancelProcedure,
RunBuild,
CancelBuild,
Deploy,
PullDeployment,
StartDeployment,
RestartDeployment,
PauseDeployment,
UnpauseDeployment,
StopDeployment,
DestroyDeployment,
CloneRepo,
PullRepo,
BuildRepo,
CancelRepoBuild,
StartContainer,
RestartContainer,
PauseContainer,
UnpauseContainer,
StopContainer,
DestroyContainer,
StartAllContainers,
RestartAllContainers,
PauseAllContainers,
UnpauseAllContainers,
StopAllContainers,
PruneContainers,
DeleteNetwork,
PruneNetworks,
DeleteImage,
PruneImages,
DeleteVolume,
PruneVolumes,
PruneDockerBuilders,
PruneBuildx,
PruneSystem,
RunSync,
DeployStack,
DeployStackIfChanged,
PullStack,
StartStack,
RestartStack,
PauseStack,
UnpauseStack,
StopStack,
DestroyStack,
RunStackService,
TestAlerter,
SendAlert,
RemoveSwarmNodes,
UpdateSwarmNode,
RemoveSwarmStacks,
RemoveSwarmServices,
CreateSwarmConfig,
RotateSwarmConfig,
RemoveSwarmConfigs,
CreateSwarmSecret,
RotateSwarmSecret,
RemoveSwarmSecrets,
ClearRepoCache,
BackupCoreDatabase,
GlobalAutoUpdate,
RotateAllServerKeys,
RotateCoreKeys,
],
batch: [
BatchRunAction,
BatchRunProcedure,
BatchRunBuild,
BatchDeploy,
BatchDestroyDeployment,
BatchCloneRepo,
BatchPullRepo,
BatchBuildRepo,
BatchDeployStack,
BatchDeployStackIfChanged,
BatchPullStack,
BatchDestroyStack,
],
);
super::wait_for_enter("run execution", yes)?;
info!("Running Execution...");
let client = super::komodo_client().await?;
let res = match execution.clone() {
Execution::RunAction(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchRunAction(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::RunProcedure(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchRunProcedure(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::RunBuild(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchRunBuild(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::CancelBuild(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::Deploy(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchDeploy(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::PullDeployment(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::StartDeployment(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RestartDeployment(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PauseDeployment(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::UnpauseDeployment(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::StopDeployment(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::DestroyDeployment(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchDestroyDeployment(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::CloneRepo(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchCloneRepo(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::PullRepo(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchPullRepo(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::BuildRepo(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchBuildRepo(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::CancelRepoBuild(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::StartContainer(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RestartContainer(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PauseContainer(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::UnpauseContainer(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::StopContainer(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::DestroyContainer(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::StartAllContainers(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RestartAllContainers(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PauseAllContainers(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::UnpauseAllContainers(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::StopAllContainers(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneContainers(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::DeleteNetwork(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneNetworks(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::DeleteImage(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneImages(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::DeleteVolume(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneVolumes(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneDockerBuilders(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneBuildx(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneSystem(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RunSync(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::CommitSync(request) => client
.write(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::DeployStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchDeployStack(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::DeployStackIfChanged(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchDeployStackIfChanged(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::PullStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchPullStack(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::StartStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RestartStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PauseStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::UnpauseStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::StopStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::DestroyStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchDestroyStack(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::RunStackService(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::TestAlerter(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::SendAlert(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RemoveSwarmNodes(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RemoveSwarmStacks(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RemoveSwarmServices(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::CreateSwarmConfig(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RotateSwarmConfig(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RemoveSwarmConfigs(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::CreateSwarmSecret(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RotateSwarmSecret(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RemoveSwarmSecrets(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::ClearRepoCache(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BackupCoreDatabase(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::GlobalAutoUpdate(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RotateAllServerKeys(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RotateCoreKeys(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::Sleep(request) => {
let duration =
Duration::from_millis(request.duration_ms as u64);
tokio::time::sleep(duration).await;
println!("Finished sleeping!");
std::process::exit(0)
}
Execution::None(_) => unreachable!(),
};
match res {
Ok(ExecutionResult::Single(update)) => {
+31 -193
View File
@@ -7,8 +7,7 @@ use komodo_client::{
api::read::{
ListActions, ListAlerters, ListBuilders, ListBuilds,
ListDeployments, ListProcedures, ListRepos, ListResourceSyncs,
ListSchedules, ListServers, ListStacks, ListSwarms, ListTags,
ListTerminals,
ListSchedules, ListServers, ListStacks, ListTags, ListTerminals,
},
entities::{
ResourceTargetVariant,
@@ -32,7 +31,6 @@ use komodo_client::{
schedule::Schedule,
server::{ServerListItem, ServerListItemInfo, ServerState},
stack::{StackListItem, StackListItemInfo, StackState},
swarm::{SwarmListItem, SwarmListItemInfo, SwarmState},
sync::{
ResourceSyncListItem, ResourceSyncListItemInfo,
ResourceSyncState,
@@ -54,46 +52,28 @@ pub async fn handle(list: &args::list::List) -> anyhow::Result<()> {
match &list.command {
None => list_all(list).await,
Some(ListCommand::Servers(filters)) => {
list_resources::<ServerListItem>(filters, list.page, false)
.await
}
Some(ListCommand::Swarms(filters)) => {
list_resources::<SwarmListItem>(filters, list.page, false).await
list_resources::<ServerListItem>(filters, false).await
}
Some(ListCommand::Stacks(filters)) => {
list_resources::<StackListItem>(filters, list.page, false).await
list_resources::<StackListItem>(filters, false).await
}
Some(ListCommand::Deployments(filters)) => {
list_resources::<DeploymentListItem>(filters, list.page, false)
.await
list_resources::<DeploymentListItem>(filters, false).await
}
Some(ListCommand::Builds(filters)) => {
list_resources::<BuildListItem>(filters, list.page, false).await
list_resources::<BuildListItem>(filters, false).await
}
Some(ListCommand::Repos(filters)) => {
list_resources::<RepoListItem>(filters, list.page, false).await
list_resources::<RepoListItem>(filters, false).await
}
Some(ListCommand::Procedures(filters)) => {
list_resources::<ProcedureListItem>(filters, list.page, false)
.await
list_resources::<ProcedureListItem>(filters, false).await
}
Some(ListCommand::Actions(filters)) => {
list_resources::<ActionListItem>(filters, list.page, false)
.await
list_resources::<ActionListItem>(filters, false).await
}
Some(ListCommand::Syncs(filters)) => {
list_resources::<ResourceSyncListItem>(
filters, list.page, false,
)
.await
}
Some(ListCommand::Builders(filters)) => {
list_resources::<BuilderListItem>(filters, list.page, false)
.await
}
Some(ListCommand::Alerters(filters)) => {
list_resources::<AlerterListItem>(filters, list.page, false)
.await
list_resources::<ResourceSyncListItem>(filters, false).await
}
Some(ListCommand::Terminals(filters)) => {
list_terminals(filters).await
@@ -101,6 +81,12 @@ pub async fn handle(list: &args::list::List) -> anyhow::Result<()> {
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
}
}
}
@@ -111,7 +97,6 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> {
let (
tags,
mut servers,
mut swarms,
mut stacks,
mut deployments,
mut builds,
@@ -124,15 +109,14 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> {
.into_iter()
.map(|t| (t.id, t.name))
.collect::<HashMap<_, _>>())),
ServerListItem::list(client, &filters, list.page, true),
SwarmListItem::list(client, &filters, list.page, true),
StackListItem::list(client, &filters, list.page, true),
DeploymentListItem::list(client, &filters, list.page, true),
BuildListItem::list(client, &filters, list.page, true),
RepoListItem::list(client, &filters, list.page, true),
ProcedureListItem::list(client, &filters, list.page, true),
ActionListItem::list(client, &filters, list.page, true),
ResourceSyncListItem::list(client, &filters, list.page, true),
ServerListItem::list(client, &filters, true),
StackListItem::list(client, &filters, true),
DeploymentListItem::list(client, &filters, true),
BuildListItem::list(client, &filters, true),
RepoListItem::list(client, &filters, true),
ProcedureListItem::list(client, &filters, true),
ActionListItem::list(client, &filters, true),
ResourceSyncListItem::list(client, &filters, true),
)?;
if !servers.is_empty() {
@@ -141,12 +125,6 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> {
println!();
}
if !swarms.is_empty() {
fix_tags(&mut swarms, &tags);
print_items(swarms, filters.format, list.links)?;
println!();
}
if !stacks.is_empty() {
fix_tags(&mut stacks, &tags);
print_items(stacks, filters.format, list.links)?;
@@ -194,7 +172,6 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> {
async fn list_resources<T>(
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<()>
where
@@ -203,7 +180,7 @@ where
{
let client = crate::command::komodo_client().await?;
let (mut resources, tags) = tokio::try_join!(
T::list(client, filters, page, minimal),
T::list(client, filters, minimal),
client.read(ListTags::default()).map(|res| res.map(|res| res
.into_iter()
.map(|t| (t.id, t.name))
@@ -228,8 +205,6 @@ async fn list_terminals(
.read(ListTerminals {
target: None,
use_names: true,
limit: Some(0),
..Default::default()
})
.await?;
if !terminals.is_empty() {
@@ -247,8 +222,6 @@ async fn list_schedules(
.read(ListSchedules {
tags: filters.tags.clone(),
tag_behavior: Default::default(),
limit: Some(0),
..Default::default()
})
.map(|res| res.map(|res| res
.into_iter()
@@ -306,7 +279,6 @@ where
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
// For use with root `km ls`
minimal: bool,
) -> anyhow::Result<Vec<ResourceListItem<Self::Info>>>;
@@ -319,7 +291,6 @@ impl ListResources for ServerListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
_minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let servers = client
@@ -329,10 +300,6 @@ impl ListResources for ServerListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.await?;
let names = parse_wildcards(&filters.names);
@@ -362,68 +329,17 @@ impl ListResources for ServerListItem {
}
}
impl ListResources for SwarmListItem {
type Info = SwarmListItemInfo;
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
_minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let servers = client
.read(ListSwarms {
query: ResourceQuery::builder()
.tags(filters.tags.clone())
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.await?;
let names = parse_wildcards(&filters.names);
let server_wildcards = parse_wildcards(&filters.servers);
let mut servers = servers
.into_iter()
.filter(|server| {
let state_check = if filters.all {
true
} else if filters.down {
!matches!(server.info.state, SwarmState::Healthy)
} else if filters.in_progress {
false
} else {
matches!(server.info.state, SwarmState::Healthy)
};
let name_items = &[server.name.as_str()];
state_check
&& matches_wildcards(&names, name_items)
&& matches_wildcards(&server_wildcards, name_items)
})
.collect::<Vec<_>>();
servers.sort_by(|a, b| {
a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))
});
Ok(servers)
}
}
impl ListResources for StackListItem {
type Info = StackListItemInfo;
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
_minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let (servers, mut stacks) = tokio::try_join!(
client
.read(ListServers {
query: ResourceQuery::builder().build(),
limit: Some(0),
..Default::default()
})
.map(|res| res.map(|res| res
.into_iter()
@@ -435,10 +351,6 @@ impl ListResources for StackListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false
})
)?;
stacks.iter_mut().for_each(|stack| {
@@ -494,15 +406,12 @@ impl ListResources for DeploymentListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
_minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let (servers, mut deployments) = tokio::try_join!(
client
.read(ListServers {
query: ResourceQuery::builder().build(),
limit: Some(0),
..Default::default()
})
.map(|res| res.map(|res| res
.into_iter()
@@ -514,10 +423,6 @@ impl ListResources for DeploymentListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false
})
)?;
deployments.iter_mut().for_each(|deployment| {
@@ -574,15 +479,12 @@ impl ListResources for BuildListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let (builders, mut builds) = tokio::try_join!(
client
.read(ListBuilders {
query: ResourceQuery::builder().build(),
limit: Some(0),
..Default::default()
})
.map(|res| res.map(|res| res
.into_iter()
@@ -594,10 +496,6 @@ impl ListResources for BuildListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false
})
)?;
builds.iter_mut().for_each(|build| {
@@ -635,7 +533,10 @@ impl ListResources for BuildListItem {
})
.collect::<Vec<_>>();
builds.sort_by(|a, b| {
a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))
a.name
.cmp(&b.name)
.then(a.info.builder_id.cmp(&b.info.builder_id))
.then(a.info.state.cmp(&b.info.state))
});
Ok(builds)
}
@@ -646,7 +547,6 @@ impl ListResources for RepoListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
@@ -657,10 +557,6 @@ impl ListResources for RepoListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.await?
.into_iter()
@@ -699,7 +595,6 @@ impl ListResources for ProcedureListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
@@ -710,10 +605,6 @@ impl ListResources for ProcedureListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.await?
.into_iter()
@@ -741,7 +632,7 @@ impl ListResources for ProcedureListItem {
(Some(a), Some(b)) => return a.cmp(&b),
(None, None) => {}
}
a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))
a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))
});
Ok(procedures)
}
@@ -752,7 +643,6 @@ impl ListResources for ActionListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
@@ -763,10 +653,6 @@ impl ListResources for ActionListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.await?
.into_iter()
@@ -794,7 +680,7 @@ impl ListResources for ActionListItem {
(Some(a), Some(b)) => return a.cmp(&b),
(None, None) => {}
}
a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))
a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))
});
Ok(actions)
}
@@ -805,7 +691,6 @@ impl ListResources for ResourceSyncListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
@@ -816,10 +701,6 @@ impl ListResources for ResourceSyncListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.await?
.into_iter()
@@ -844,7 +725,7 @@ impl ListResources for ResourceSyncListItem {
})
.collect::<Vec<_>>();
syncs.sort_by(|a, b| {
a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))
a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))
});
Ok(syncs)
}
@@ -855,7 +736,6 @@ impl ListResources for BuilderListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
@@ -866,10 +746,6 @@ impl ListResources for BuilderListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.await?
.into_iter()
@@ -892,7 +768,6 @@ impl ListResources for AlerterListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let names = parse_wildcards(&filters.names);
@@ -903,10 +778,6 @@ impl ListResources for AlerterListItem {
// .tag_behavior(TagQueryBehavior::Any)
.templates(filters.templates)
.build(),
limit: None,
page: page.saturating_sub(1),
sort_by: Default::default(),
sort_desc: false,
})
.await?
.into_iter()
@@ -961,39 +832,6 @@ impl PrintTable for ResourceListItem<ServerListItemInfo> {
}
}
impl PrintTable for ResourceListItem<SwarmListItemInfo> {
fn header(links: bool) -> &'static [&'static str] {
if links {
&["Swarm", "State", "Tags", "Link"]
} else {
&["Swarm", "State", "Tags"]
}
}
fn row(self, links: bool) -> Vec<Cell> {
let color = match self.info.state {
SwarmState::Healthy => Color::Green,
SwarmState::Unhealthy => Color::Red,
SwarmState::Down => Color::Blue,
SwarmState::Unknown => Color::Magenta,
};
let mut res = vec![
Cell::new(self.name).add_attribute(Attribute::Bold),
Cell::new(self.info.state.to_string())
.fg(color)
.add_attribute(Attribute::Bold),
Cell::new(self.tags.join(", ")),
];
if links {
res.push(Cell::new(resource_link(
&cli_config().host,
ResourceTargetVariant::Server,
&self.id,
)))
}
res
}
}
impl PrintTable for ResourceListItem<StackListItemInfo> {
fn header(links: bool) -> &'static [&'static str] {
if links {
+2 -4
View File
@@ -15,8 +15,6 @@ use wildcard::Wildcard;
use crate::config::cli_config;
pub mod container;
pub mod core_info;
pub mod create;
pub mod database;
pub mod execute;
pub mod list;
@@ -100,7 +98,7 @@ fn print_items<T: PrintTable + Serialize>(
match format {
CliFormat::Table => {
let mut table = Table::new();
let style = {
let preset = {
use comfy_table::presets::*;
match cli_config().table_borders {
None | Some(CliTableBorders::Horizontal) => {
@@ -112,7 +110,7 @@ fn print_items<T: PrintTable + Serialize>(
Some(CliTableBorders::All) => UTF8_FULL,
}
};
table.load_style(style).set_header(
table.load_preset(preset).set_header(
T::header(links)
.iter()
.map(|h| Cell::new(h).add_attribute(Attribute::Bold)),
+44 -99
View File
@@ -2,7 +2,7 @@ use anyhow::{Context, anyhow};
use colored::Colorize;
use komodo_client::{
api::{
read::{GetServer, ListAllContainers, ListServers},
read::{ListAllDockerContainers, ListServers},
terminal::InitTerminal,
},
entities::{
@@ -26,7 +26,7 @@ pub async fn handle_connect(
recreate,
}: &Connect,
) -> anyhow::Result<()> {
handle_terminal_forwarding(server, async {
handle_terminal_forwarding(async {
super::komodo_client()
.await?
.connect_server_terminal(
@@ -56,28 +56,25 @@ pub async fn handle_exec(
}: &Exec,
) -> anyhow::Result<()> {
let server = get_server(server.clone(), container).await?;
handle_terminal_forwarding(
&format!("{server}/{container}"),
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
},
)
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
}
@@ -89,28 +86,25 @@ pub async fn handle_attach(
}: &Attach,
) -> anyhow::Result<()> {
let server = get_server(server.clone(), container).await?;
handle_terminal_forwarding(
&format!("{server}/{container}-attach"),
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
},
)
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
}
@@ -118,8 +112,6 @@ async fn get_server(
server: Option<String>,
container: &str,
) -> anyhow::Result<String> {
// Don't need to find server for terminal,
// user provided it, early return.
if let Some(server) = server {
return Ok(server);
}
@@ -127,15 +119,9 @@ async fn get_server(
let client = super::komodo_client().await?;
let mut containers = client
.read(ListAllContainers {
.read(ListAllDockerContainers {
servers: Default::default(),
tags: Default::default(),
terms: vec![container.to_string()],
state: Default::default(),
limit: Some(0),
page: 0,
sort_by: Default::default(),
sort_desc: false,
containers: vec![container.to_string()],
})
.await?;
@@ -146,14 +132,11 @@ async fn get_server(
}
if containers.len() == 1 {
let server_id = containers
return containers
.pop()
.context("Shouldn't happen")?
.server_id
.context("Container doesn't have server_id")?;
let server_name =
client.read(GetServer { server: server_id }).await?.name;
return Ok(server_name);
.context("Container doesn't have server_id");
}
let servers = containers
@@ -164,8 +147,6 @@ async fn get_server(
let servers = client
.read(ListServers {
query: ServerQuery::builder().names(servers).build(),
limit: Some(0),
..Default::default()
})
.await?
.into_iter()
@@ -182,7 +163,6 @@ async fn get_server(
async fn handle_terminal_forwarding<
C: Future<Output = anyhow::Result<TerminalWebsocket>>,
>(
label: &str,
connect: C,
) -> anyhow::Result<()> {
// Need to forward multiple sources into ws write
@@ -267,16 +247,6 @@ async fn handle_terminal_forwarding<
let forward_read = async {
let mut stdout = tokio::io::stdout();
// Write connection message
if let Err(e) = write_connection_message(&mut stdout, label)
.await
.context("Failed to write text to stdout")
{
cancel.cancel();
return Some(e);
}
while let Some(msg) =
future_or_cancel(ws_read.receive_stdout(), &cancel).await
{
@@ -327,31 +297,6 @@ async fn handle_terminal_forwarding<
std::process::exit(0)
}
async fn write_connection_message(
stdout: &mut tokio::io::Stdout,
label: &str,
) -> anyhow::Result<()> {
// Use message without ansi for correct length
let message_clean = format!("# Connected to {label} (km) #");
let bounder = "=".repeat(message_clean.chars().count());
let message = format!(
"# {} to {} {} #",
"Connected".green().bold(),
label.bold(),
"(km)".dimmed()
);
stdout
.write_all(
format!("\n{bounder}\r\n{message}\r\n{bounder}\r\n").as_bytes(),
)
.await?;
let _ = stdout.flush().await;
Ok(())
}
fn resize_message() -> anyhow::Result<TerminalStdinMessage> {
let (cols, rows) = crossterm::terminal::size()
.context("Failed to get terminal size")?;
-3
View File
@@ -264,9 +264,6 @@ pub fn cli_config() -> &'static CliConfig {
ansi: env
.komodo_cli_logging_ansi
.unwrap_or(config.cli_logging.ansi),
timestamps: env
.komodo_cli_logging_timestamps
.unwrap_or(config.cli_logging.timestamps),
otlp_endpoint: env
.komodo_cli_logging_otlp_endpoint
.unwrap_or(config.cli_logging.otlp_endpoint),
-7
View File
@@ -12,9 +12,6 @@ mod config;
async fn app() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.expect("Failed to install default crypto provider");
mogh_logger::init(&config::cli_config().cli_logging)?;
let args = config::cli_args();
let env = config::cli_env();
@@ -45,7 +42,6 @@ async fn app() -> anyhow::Result<()> {
}
Ok(())
}
args::Command::CoreInfo => command::core_info::handle().await,
args::Command::Container(container) => {
command::container::handle(container).await
}
@@ -56,9 +52,6 @@ async fn app() -> anyhow::Result<()> {
args::Command::Execute(args) => {
command::execute::handle(&args.execution, args.yes).await
}
args::Command::Create { command } => {
command::create::handle(command).await
}
args::Command::Update { command } => {
command::update::handle(command).await
}
+1
View File
@@ -2,6 +2,7 @@
name = "komodo_core"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
+8 -15
View File
@@ -1,8 +1,8 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
# Build Core
FROM rust:1.98.0-trixie AS core-builder
RUN cargo install cargo-strip cargo-edit
FROM rust:1.91.1-trixie AS core-builder
RUN cargo install cargo-strip
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -11,25 +11,19 @@ COPY ./client/core/rs ./client/core/rs
COPY ./client/periphery ./client/periphery
COPY ./bin/core ./bin/core
COPY ./bin/cli ./bin/cli
COPY ./xtask ./xtask
# Set Version
ARG VERSION="0.0.0"
ARG IMAGE_TAG=""
RUN cargo set-version -p komodo_core ${VERSION}${IMAGE_TAG:+-${IMAGE_TAG}}
# Compile app
RUN cargo build -p komodo_core --release && \
cargo build -p komodo_cli --release && \
cargo strip
# Build UI
FROM node:22.12-alpine AS ui-builder
# Build Frontend
FROM node:22.12-alpine AS frontend-builder
WORKDIR /builder
COPY ./ui ./ui
COPY ./frontend ./frontend
COPY ./client/core/ts ./client
RUN cd client && yarn && yarn build && yarn link
RUN cd ui && yarn link komodo_client && yarn && yarn build
RUN cd frontend && yarn link komodo_client && yarn && yarn build
# Final Image
FROM debian:trixie-slim
@@ -43,7 +37,7 @@ WORKDIR /app
# Copy
COPY ./config/core.config.toml /config/.default.config.toml
COPY --from=ui-builder /builder/ui/dist /app/ui
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
COPY --from=core-builder /builder/target/release/core /usr/local/bin/core
COPY --from=core-builder /builder/target/release/km /usr/local/bin/km
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
@@ -64,8 +58,7 @@ 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" ]
CMD [ "/bin/bash", "-c", "update-ca-certificates && core" ]
# Label to prevent Komodo from stopping with StopAllContainers
LABEL komodo.skip="true"
+5 -5
View File
@@ -2,15 +2,15 @@
## Sets up the necessary runtime container dependencies for Komodo Core.
## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:2
ARG UI_IMAGE=ghcr.io/moghtech/komodo-ui:2
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
ARG FRONTEND_IMAGE=ghcr.io/moghtech/komodo-frontend:latest
ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64
ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64
# This is required to work with COPY --from
FROM ${X86_64_BINARIES} AS x86_64
FROM ${AARCH64_BINARIES} AS aarch64
FROM ${UI_IMAGE} AS ui
FROM ${FRONTEND_IMAGE} AS frontend
# Final Image
FROM debian:trixie-slim
@@ -33,9 +33,9 @@ COPY --from=x86_64 /km /app/km/linux/amd64
COPY --from=aarch64 /km /app/km/linux/arm64
RUN mv /app/km/${TARGETPLATFORM} /usr/local/bin/km && rm -r /app/km
# Copy default config / static ui / deno binary
# Copy default config / static frontend / deno binary
COPY ./config/core.config.toml /config/.default.config.toml
COPY --from=ui /ui /app/ui
COPY --from=frontend /frontend /app/frontend
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
# Set $DENO_DIR and preload external Deno deps
+6 -6
View File
@@ -1,18 +1,18 @@
## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).
## Sets up the necessary runtime container dependencies for Komodo Core.
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:2
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
# This is required to work with COPY --from
FROM ${BINARIES_IMAGE} AS binaries
# Build UI
FROM node:22.12-alpine AS ui-builder
# Build Frontend
FROM node:22.12-alpine AS frontend-builder
WORKDIR /builder
COPY ./ui ./ui
COPY ./frontend ./frontend
COPY ./client/core/ts ./client
RUN cd client && yarn && yarn build && yarn link
RUN cd ui && yarn link komodo_client && yarn && yarn build
RUN cd frontend && yarn link komodo_client && yarn && yarn build
FROM debian:trixie-slim
@@ -22,7 +22,7 @@ RUN sh ./debian-deps.sh && rm ./debian-deps.sh
# Copy
COPY ./config/core.config.toml /config/.default.config.toml
COPY --from=ui-builder /builder/ui/dist /app/ui
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
COPY --from=binaries /core /usr/local/bin/core
COPY --from=binaries /km /usr/local/bin/km
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
+30 -32
View File
@@ -20,14 +20,12 @@ pub async fn send_alert(
let link = resource_link(ResourceTargetVariant::Swarm, id);
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | Swarm **{name}** is now **healthy**\n{link}"
)
format!("{level} | Swarm **{name}** is now **healthy**\n{link}")
}
SeverityLevel::Critical => {
let err = err
.as_ref()
.map(|e| format!("\n**error**: {e:#?}"))
.map(|e| format!("\n**error**: {e}"))
.unwrap_or_default();
format!(
"{level} | Swarm **{name}** is **unhealthy** ❌\n{link}{err}"
@@ -36,6 +34,28 @@ pub async fn send_alert(
_ => unreachable!(),
}
}
AlertData::ServerVersionMismatch {
id,
name,
region,
server_version,
core_version,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | **{name}**{region} | Periphery version now matches Core version ✅\n{link}"
)
}
_ => {
format!(
"{level} | **{name}**{region} | Version mismatch detected ⚠️\nPeriphery: **{server_version}** | Core: **{core_version}**\n{link}"
)
}
}
}
AlertData::ServerUnreachable {
id,
name,
@@ -62,28 +82,6 @@ pub async fn send_alert(
_ => unreachable!(),
}
}
AlertData::ServerVersionMismatch {
id,
name,
region,
server_version,
core_version,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | **{name}**{region} | Periphery version now matches Core version ✅\n{link}"
)
}
_ => {
format!(
"{level} | **{name}**{region} | Version mismatch detected ⚠️\nPeriphery: **{server_version}** | Core: **{core_version}**\n{link}"
)
}
}
}
AlertData::ServerCpu {
id,
name,
@@ -142,7 +140,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
String::new()
format!("")
};
format!(
"📦 Deployment **{name}** is now **{to}**{target}\nprevious: **{from}**\n{link}"
@@ -163,7 +161,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
String::new()
format!("")
};
format!(
"⬆ Deployment **{name}** has an update available{target}\nimage: **{image}**\n{link}"
@@ -184,7 +182,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
String::new()
format!("")
};
format!(
"⬆ Deployment **{name}** was updated automatically ⏫{target}\nimage: **{image}**\n{link}"
@@ -207,7 +205,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
String::new()
format!("")
};
format!(
"🥞 Stack **{name}** is now {to}{target}\nprevious: **{from}**\n{link}"
@@ -229,7 +227,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
String::new()
format!("")
};
format!(
"⬆ Stack **{name}** has an update available{target}\nservice: **{service}**\nimage: **{image}**\n{link}"
@@ -253,7 +251,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
String::new()
format!("")
};
format!(
"⬆ Stack **{name}** was updated automatically ⏫{target}\n{images_label}: **{images}**\n{link}"
+32 -34
View File
@@ -260,35 +260,9 @@ fn standard_alert_content(alert: &Alert) -> String {
SeverityLevel::Critical => {
let err = err
.as_ref()
.map(|e| format!("\nerror: {e:#?}"))
.map(|e| format!("\nerror: {e}"))
.unwrap_or_default();
format!(
"{level} | Swarm {name} is unhealthy ❌\n{link}{err}"
)
}
_ => unreachable!(),
}
}
AlertData::ServerUnreachable {
id,
name,
region,
err,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
match alert.level {
SeverityLevel::Ok => {
format!("{level} | {name}{region} is now connected\n{link}")
}
SeverityLevel::Critical => {
let err = err
.as_ref()
.map(|e| format!("\nerror: {e:#?}"))
.unwrap_or_default();
format!(
"{level} | {name}{region} is unreachable ❌\n{link}{err}"
)
format!("{level} | Swarm {name} is unhealthy ❌\n{link}{err}")
}
_ => unreachable!(),
}
@@ -315,6 +289,30 @@ fn standard_alert_content(alert: &Alert) -> String {
}
}
}
AlertData::ServerUnreachable {
id,
name,
region,
err,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
match alert.level {
SeverityLevel::Ok => {
format!("{level} | {name}{region} is now connected\n{link}")
}
SeverityLevel::Critical => {
let err = err
.as_ref()
.map(|e| format!("\nerror: {e:#?}"))
.unwrap_or_default();
format!(
"{level} | {name}{region} is unreachable ❌\n{link}{err}"
)
}
_ => unreachable!(),
}
}
AlertData::ServerCpu {
id,
name,
@@ -373,7 +371,7 @@ fn standard_alert_content(alert: &Alert) -> String {
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
String::new()
format!("")
};
format!(
"📦Deployment {name} is now {to_state}{target}\nprevious: {from}\n{link}",
@@ -394,7 +392,7 @@ fn standard_alert_content(alert: &Alert) -> String {
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
String::new()
format!("")
};
format!(
"⬆ Deployment {name} has an update available{target}\nimage: {image}\n{link}",
@@ -415,7 +413,7 @@ fn standard_alert_content(alert: &Alert) -> String {
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
String::new()
format!("")
};
format!(
"⬆ Deployment {name} was updated automatically{target}\nimage: {image}\n{link}",
@@ -438,7 +436,7 @@ fn standard_alert_content(alert: &Alert) -> String {
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
String::new()
format!("")
};
format!(
"🥞 Stack {name} is now {to_state}{target}\nprevious: {from}\n{link}",
@@ -460,7 +458,7 @@ fn standard_alert_content(alert: &Alert) -> String {
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
String::new()
format!("")
};
format!(
"⬆ Stack {name} has an update available{target}\nservice: {service}\nimage: {image}\n{link}",
@@ -484,7 +482,7 @@ fn standard_alert_content(alert: &Alert) -> String {
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
String::new()
format!("")
};
format!(
"⬆ Stack {name} was updated automatically ⏫{target}\n{images_label}: {images_str}\n{link}",
+36 -36
View File
@@ -42,7 +42,7 @@ pub async fn send_alert(
format!("{level} | Swarm *{name}* is *unhealthy* ❌");
let err = err
.as_ref()
.map(|e| format!("\nerror: {e:#?}"))
.map(|e| format!("\nerror: {e}"))
.unwrap_or_default();
let blocks = vec![
Block::header(level),
@@ -59,6 +59,35 @@ pub async fn send_alert(
_ => unreachable!(),
}
}
AlertData::ServerVersionMismatch {
id,
name,
region,
server_version,
core_version,
} => {
let region = fmt_region(region);
let text = match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | *{name}*{region} | Periphery version now matches Core version ✅"
)
}
_ => {
format!(
"{level} | *{name}*{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}"
)
}
};
let blocks = vec![
Block::header(text.clone()),
Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
];
(text, blocks.into())
}
AlertData::ServerUnreachable {
id,
name,
@@ -100,35 +129,6 @@ pub async fn send_alert(
_ => unreachable!(),
}
}
AlertData::ServerVersionMismatch {
id,
name,
region,
server_version,
core_version,
} => {
let region = fmt_region(region);
let text = match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | *{name}*{region} | Periphery version now matches Core version ✅"
)
}
_ => {
format!(
"{level} | *{name}*{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}"
)
}
};
let blocks = vec![
Block::header(text.clone()),
Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
];
(text, blocks.into())
}
AlertData::ServerCpu {
id,
name,
@@ -289,7 +289,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("server: *{server}*\n")
} else {
String::new()
format!("")
};
let blocks = vec![
Block::header(text.clone()),
@@ -317,7 +317,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("server: *{server}*\n")
} else {
String::new()
format!("")
};
let blocks = vec![
Block::header(text.clone()),
@@ -345,7 +345,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("server: *{server}*\n")
} else {
String::new()
format!("")
};
let blocks = vec![
Block::header(text.clone()),
@@ -374,7 +374,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("server: *{server}*\n")
} else {
String::new()
format!("")
};
let blocks = vec![
Block::header(text.clone()),
@@ -402,7 +402,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("server: *{server}*\n")
} else {
String::new()
format!("")
};
let blocks = vec![
Block::header(text.clone()),
@@ -435,7 +435,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("server: *{server}*\n")
} else {
String::new()
format!("")
};
let blocks = vec![
Block::header(text.clone()),
+18 -105
View File
@@ -4,26 +4,23 @@ use std::{
sync::OnceLock,
};
use anyhow::Context as _;
use command::{CommandOptions, run_komodo_standard_command};
use database::{
bson::doc,
mungos::{by_id::update_one_by_id, mongodb::bson::to_document},
use anyhow::Context;
use command::run_komodo_standard_command;
use database::mungos::{
by_id::update_one_by_id, mongodb::bson::to_document,
};
use interpolate::Interpolator;
use komodo_client::{
api::execute::{
BatchExecutionResponse, BatchRunAction, CancelAction, RunAction,
},
api::execute::{BatchExecutionResponse, BatchRunAction, RunAction},
entities::{
FileFormat, JsonObject, Operation,
FileFormat, JsonObject,
action::Action,
alert::{Alert, AlertData, SeverityLevel},
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
random_string,
update::{Update, UpdateStatus},
update::Update,
user::action_user,
},
parsers::parse_key_value_list,
@@ -37,7 +34,6 @@ use mogh_auth_server::api::manage::api_key::{
use mogh_config::merge_objects;
use mogh_resolver::Resolve;
use tokio::fs;
use tokio_util::sync::CancellationToken;
use crate::{
alert::send_alerts,
@@ -50,7 +46,7 @@ use crate::{
},
permission::get_check_permissions,
resource::refresh_action_state_cache,
state::{action_cancel_cache, action_states, db_client},
state::{action_states, db_client},
};
use super::ExecuteArgs;
@@ -70,23 +66,18 @@ impl Resolve<ExecuteArgs> for BatchRunAction {
"BatchRunAction",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunAction>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchRunAction>(&self.pattern, user)
.await?,
)
}
}
@@ -96,7 +87,7 @@ impl Resolve<ExecuteArgs> for RunAction {
"RunAction",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
action = self.action,
@@ -104,11 +95,7 @@ impl Resolve<ExecuteArgs> for RunAction {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let mut action = get_check_permissions::<Action>(
&self.action,
@@ -125,7 +112,7 @@ impl Resolve<ExecuteArgs> for RunAction {
// This will set action state back to default when dropped.
// Will also check to ensure action not already busy before updating.
let action_guard = action_state.update_custom(
let _action_guard = action_state.update_custom(
|state| state.running += 1,
|state| state.running -= 1,
false,
@@ -206,20 +193,14 @@ impl Resolve<ExecuteArgs> for RunAction {
""
};
let cancel = CancellationToken::new();
action_cancel_cache()
.insert(update.id.clone(), cancel.clone())
.await;
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()
),
CommandOptions::default().cancel(cancel),
)
.await;
@@ -233,7 +214,7 @@ impl Resolve<ExecuteArgs> for RunAction {
update.logs.push(res);
update.finalize();
mogh_error::Ok(())
mogh_error::Ok(update)
}
.await;
@@ -246,13 +227,7 @@ impl Resolve<ExecuteArgs> for RunAction {
);
};
action_cancel_cache().remove(&update.id).await;
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
res?;
let update = res?;
// Need to manually update the update before cache refresh,
// and before broadcast with update_update.
@@ -480,65 +455,3 @@ fn parse_action_arguments(
.context("Failed to parse Json to action args"),
}
}
impl Resolve<ExecuteArgs> for CancelAction {
#[instrument(
"CancelAction",
skip_all,
fields(
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
action = self.action,
)
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let action = get_check_permissions::<Action>(
&self.action,
user,
PermissionLevel::Execute.into(),
)
.await?;
let update_id = if let Some(update_id) = self.update_id
&& !update_id.is_empty()
{
update_id
} else {
db_client()
.updates
.find_one(doc! {
"target.type": "Action",
"target.id": &action.id,
"operation": Operation::RunAction.as_ref(),
"status": UpdateStatus::InProgress.as_ref(),
})
.await?
.context("No active run to cancel found")?
.id
};
action_cancel_cache()
.get(&update_id)
.await
.context("Action run cancel token not found")?
.cancel();
let mut update = update.clone();
update.push_simple_log(
"Cancel Triggered",
"The action cancel has been triggered.",
);
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
+4 -14
View File
@@ -28,7 +28,7 @@ impl Resolve<ExecuteArgs> for TestAlerter {
"TestAlerter",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
alerter = self.alerter,
@@ -36,11 +36,7 @@ impl Resolve<ExecuteArgs> for TestAlerter {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let alerter = get_check_permissions::<Alerter>(
&self.alerter,
@@ -98,7 +94,7 @@ impl Resolve<ExecuteArgs> for SendAlert {
"SendAlert",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
request = format!("{self:?}"),
@@ -106,16 +102,10 @@ impl Resolve<ExecuteArgs> for SendAlert {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let alerters = list_full_for_user::<Alerter>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
+34 -55
View File
@@ -28,7 +28,7 @@ use komodo_client::{
alert::{Alert, AlertData, SeverityLevel},
all_logs_success,
build::{Build, BuildConfig},
builder::Builder,
builder::{Builder, BuilderConfig},
deployment::DeploymentState,
komodo_timestamp, optional_string,
permission::PermissionLevel,
@@ -75,23 +75,18 @@ impl Resolve<ExecuteArgs> for BatchRunBuild {
"BatchRunBuild",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunBuild>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchRunBuild>(&self.pattern, user)
.await?,
)
}
}
@@ -101,7 +96,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
"RunBuild",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
build = self.build,
@@ -109,11 +104,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let mut build = get_check_permissions::<Build>(
&self.build,
@@ -153,7 +144,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
// This will set action state back to default when dropped.
// Will also check to ensure build not already busy before updating.
let action_guard =
let _action_guard =
action_state.update(|state| state.building = true)?;
if build.config.auto_increment_version {
@@ -180,6 +171,9 @@ impl Resolve<ExecuteArgs> for RunBuild {
let builder =
resource::get::<Builder>(&build.config.builder_id).await?;
let is_server_builder =
matches!(&builder.config, BuilderConfig::Server(_));
tokio::spawn(async move {
let poll = async {
loop {
@@ -188,13 +182,19 @@ impl Resolve<ExecuteArgs> for RunBuild {
id = cancel_recv.recv() => id?
};
if incoming_build_id == build_id {
update.push_simple_log("Cancel acknowledged", "The build cancellation has been queued, it may still take some time.");
if is_server_builder {
update.push_error_log("Cancel acknowledged", "Build cancellation is not possible on server builders at this time. Use an AWS builder to enable this feature.");
} else {
update.push_simple_log("Cancel acknowledged", "The build cancellation has been queued, it may still take some time.");
}
update.finalize();
let id = update.id.clone();
if let Err(e) = update_update(update).await {
warn!("Failed to modify Update {id} on db | {e:#}");
}
cancel_clone.cancel();
if !is_server_builder {
cancel_clone.cancel();
}
return Ok(());
}
}
@@ -212,7 +212,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
build.name.clone(),
Some(build.config.version),
builder,
Some(&mut update),
&mut update,
)
.await
{
@@ -269,11 +269,11 @@ impl Resolve<ExecuteArgs> for RunBuild {
replacers: Default::default(),
}) => res,
_ = cancel.cancelled() => {
debug!("Build cancelled during repo clone, cleaning up builder");
update.push_error_log("Build cancelled", String::from("Build cancelled during repo clone"));
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;
debug!("Builder cleaned up");
info!("Builder cleaned up");
return handle_early_return(update, build.id, build.name, true).await
},
};
@@ -319,14 +319,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
}) => res.context("Failed at call to Periphery to build"),
_ = cancel.cancelled() => {
info!("Build cancelled during build, cleaning up builder");
if let Err(e) = periphery.request(api::build::CancelBuild {
id: build.id.clone()
})
.await
.context("Failed to cancel build execution on Server") {
update.push_error_log("Cancel Build", format_serror(&e.into()));
}
update.push_error_log("Build Cancelled", String::from("User cancelled build during image build step"));
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
@@ -376,10 +369,6 @@ impl Resolve<ExecuteArgs> for RunBuild {
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
// The Err case of to_document should be unreachable,
@@ -516,10 +505,10 @@ pub async fn validate_cancel_build(
)?;
match (latest_build, latest_cancel) {
(Some(build), Some(cancel))
if cancel.start_ts > build.start_ts =>
{
return Err(anyhow!("Build has already been cancelled"));
(Some(build), Some(cancel)) => {
if cancel.start_ts > build.start_ts {
return Err(anyhow!("Build has already been cancelled"));
}
}
(None, _) => return Err(anyhow!("No build in progress")),
_ => {}
@@ -533,7 +522,7 @@ impl Resolve<ExecuteArgs> for CancelBuild {
"CancelBuild",
skip(user, update),
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
build = self.build,
@@ -541,11 +530,7 @@ impl Resolve<ExecuteArgs> for CancelBuild {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let build = get_check_permissions::<Build>(
&self.build,
@@ -568,8 +553,8 @@ impl Resolve<ExecuteArgs> for CancelBuild {
let mut update = update.clone();
update.push_simple_log(
"Cancel Triggered",
"The build cancel has been triggered",
"cancel triggered",
"the build cancel has been triggered",
);
update_update(update.clone()).await?;
@@ -624,13 +609,7 @@ async fn handle_post_build_redeploy(build_id: &str) {
let state = get_deployment_state(&deployment.id)
.await
.unwrap_or_default();
if ![
DeploymentState::NotDeployed,
DeploymentState::Exited,
DeploymentState::Unknown,
]
.contains(&state)
{
if state == DeploymentState::Running {
let req = super::ExecuteRequest::Deploy(Deploy {
deployment: deployment.id.clone(),
stop_signal: None,
@@ -647,7 +626,7 @@ async fn handle_post_build_redeploy(build_id: &str) {
.resolve(&ExecuteArgs {
user,
update,
task_id: Uuid::new_v4(),
id: Uuid::new_v4(),
})
.await
}
+69 -164
View File
@@ -32,7 +32,7 @@ use crate::{
swarm::swarm_request,
update::update_update,
},
monitor::{refresh_server_cache, refresh_swarm_cache},
monitor::{update_cache_for_server, update_cache_for_swarm},
resource::{self, setup_deployment_execution},
state::action_states,
};
@@ -55,23 +55,18 @@ impl Resolve<ExecuteArgs> for BatchDeploy {
"BatchDeploy",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeploy>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchDeploy>(&self.pattern, user)
.await?,
)
}
}
@@ -81,7 +76,7 @@ impl Resolve<ExecuteArgs> for Deploy {
"Deploy",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -91,11 +86,7 @@ impl Resolve<ExecuteArgs> for Deploy {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (mut deployment, swarm_or_server) =
setup_deployment_execution(
@@ -115,28 +106,38 @@ impl Resolve<ExecuteArgs> for Deploy {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.deploying = true)?;
let mut update = update.clone();
// Send update after setting action state, this way UI gets correct state.
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
// This block resolves the attached Build to an actual versioned image
let (version, registry_token) = match &deployment.config.image {
DeploymentImage::Build { build_id, version } => {
let build = resource::get::<Build>(build_id).await?;
let image_names = build.get_image_names();
let image_name = image_names
.first()
.context("No image name could be created")
.context("Failed to create image name")?;
let version = if version.is_none() {
build.config.version
} else {
*version
};
let image_name = build.get_deployment_image_name();
let image_tag = build.get_deployment_image_tag(version);
let version_str = version.to_string();
// Potentially add the build image_tag postfix
let version_str = if build.config.image_tag.is_empty() {
version_str
} else {
format!("{version_str}-{}", build.config.image_tag)
};
// replace image with corresponding build image.
deployment.config.image = DeploymentImage::Image {
image: format!("{image_name}:{image_tag}"),
image: format!("{image_name}:{version_str}"),
};
let first_registry = build
.config
@@ -206,12 +207,6 @@ impl Resolve<ExecuteArgs> for Deploy {
update_update(update.clone()).await?;
let deployment_id = deployment.id.clone();
// Track the name the container / service is deployed under,
// so the Deployment stays matched to it even if the
// name configuration changes before the next deploy.
let fresh_name = deployment.custom_name().to_string();
let prev_deployed_name = deployment.info.deployed_name.clone();
let mut deployed = false;
match swarm_or_server {
SwarmOrServer::None => unreachable!(),
@@ -227,8 +222,7 @@ impl Resolve<ExecuteArgs> for Deploy {
.await
{
Ok(logs) => {
refresh_swarm_cache(&swarm, true).await;
deployed = logs.iter().all(|log| log.success);
update_cache_for_swarm(&swarm, true).await;
update.logs.extend(logs)
}
Err(e) => {
@@ -252,8 +246,7 @@ impl Resolve<ExecuteArgs> for Deploy {
.await
{
Ok(log) => {
refresh_server_cache(&server, true).await;
deployed = log.success;
update_cache_for_server(&server, true).await;
update.logs.push(log)
}
Err(e) => {
@@ -270,11 +263,6 @@ impl Resolve<ExecuteArgs> for Deploy {
&deployment_id,
&DeploymentInfo {
latest_image_digest: Default::default(),
deployed_name: if deployed {
fresh_name
} else {
prev_deployed_name
},
},
)
.await
@@ -286,11 +274,6 @@ impl Resolve<ExecuteArgs> for Deploy {
}
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -418,7 +401,7 @@ pub async fn pull_deployment_inner(
Err(e) => Log::error("Pull image", format_serror(&e.into())),
};
refresh_server_cache(server, true).await;
update_cache_for_server(server, true).await;
anyhow::Ok(log)
}
.await;
@@ -435,7 +418,7 @@ impl Resolve<ExecuteArgs> for PullDeployment {
"PullDeployment",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -443,11 +426,7 @@ impl Resolve<ExecuteArgs> for PullDeployment {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
@@ -471,21 +450,17 @@ impl Resolve<ExecuteArgs> for PullDeployment {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pulling = true)?;
let mut update = update.clone();
// Send update after setting action state, this way UI gets correct state.
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = pull_deployment_inner(deployment, &server).await?;
update.logs.push(log);
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -497,7 +472,7 @@ impl Resolve<ExecuteArgs> for StartDeployment {
"StartDeployment",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -505,11 +480,7 @@ impl Resolve<ExecuteArgs> for StartDeployment {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
@@ -533,18 +504,18 @@ impl Resolve<ExecuteArgs> for StartDeployment {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.starting = true)?;
let mut update = update.clone();
// Send update after setting action state, this way UI gets correct state.
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = match periphery_client(&server)
.await?
.request(api::container::StartContainer {
name: deployment.deployed_name().to_string(),
name: deployment.name,
})
.await
{
@@ -556,12 +527,8 @@ impl Resolve<ExecuteArgs> for StartDeployment {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -573,7 +540,7 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
"RestartDeployment",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -581,11 +548,7 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
@@ -609,18 +572,18 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.restarting = true)?;
let mut update = update.clone();
// Send update after setting action state, this way UI gets correct state.
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = match periphery_client(&server)
.await?
.request(api::container::RestartContainer {
name: deployment.deployed_name().to_string(),
name: deployment.name,
})
.await
{
@@ -634,12 +597,8 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -651,7 +610,7 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
"PauseDeployment",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -659,11 +618,7 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
@@ -687,18 +642,18 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pausing = true)?;
let mut update = update.clone();
// Send update after setting action state, this way UI gets correct state.
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = match periphery_client(&server)
.await?
.request(api::container::PauseContainer {
name: deployment.deployed_name().to_string(),
name: deployment.name,
})
.await
{
@@ -710,12 +665,8 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -727,7 +678,7 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
"UnpauseDeployment",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -735,11 +686,7 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
@@ -763,18 +710,18 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.unpausing = true)?;
let mut update = update.clone();
// Send update after setting action state, this way UI gets correct state.
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = match periphery_client(&server)
.await?
.request(api::container::UnpauseContainer {
name: deployment.deployed_name().to_string(),
name: deployment.name,
})
.await
{
@@ -788,12 +735,8 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -805,7 +748,7 @@ impl Resolve<ExecuteArgs> for StopDeployment {
"StopDeployment",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -815,11 +758,7 @@ impl Resolve<ExecuteArgs> for StopDeployment {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
@@ -843,18 +782,18 @@ impl Resolve<ExecuteArgs> for StopDeployment {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.stopping = true)?;
let mut update = update.clone();
// Send update after setting action state, this way UI gets correct state.
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = match periphery_client(&server)
.await?
.request(api::container::StopContainer {
name: deployment.deployed_name().to_string(),
name: deployment.name,
signal: self
.signal
.unwrap_or(deployment.config.termination_signal)
@@ -874,12 +813,8 @@ impl Resolve<ExecuteArgs> for StopDeployment {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -902,20 +837,18 @@ impl Resolve<ExecuteArgs> for BatchDestroyDeployment {
"BatchDestroyDeployment",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDestroyDeployment>(
&self.pattern,
self.tags,
user,
)
.await?,
@@ -928,7 +861,7 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
"DestroyDeployment",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -938,11 +871,7 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
@@ -961,12 +890,12 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.destroying = true)?;
let mut update = update.clone();
// Send update after setting action state, this way UI gets correct state.
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = match swarm_or_server {
@@ -975,13 +904,13 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
match swarm_request(
&swarm.config.server_ids,
api::swarm::RemoveSwarmServices {
services: vec![deployment.deployed_name().to_string()],
services: vec![deployment.name],
},
)
.await
{
Ok(log) => {
refresh_swarm_cache(&swarm, true).await;
update_cache_for_swarm(&swarm, true).await;
log
}
Err(e) => Log::error(
@@ -996,7 +925,7 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
match periphery_client(&server)
.await?
.request(api::container::RemoveContainer {
name: deployment.deployed_name().to_string(),
name: deployment.name,
signal: self
.signal
.unwrap_or(deployment.config.termination_signal)
@@ -1009,7 +938,7 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
.await
{
Ok(log) => {
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
log
}
Err(e) => Log::error(
@@ -1022,33 +951,9 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
}
};
// Clear the tracked deployed name once the container / service
// is confirmed removed, so the next deploy uses the fresh name.
if log.success
&& let Err(e) = resource::update_info::<Deployment>(
&deployment.id,
&DeploymentInfo {
latest_image_digest: deployment
.info
.latest_image_digest
.clone(),
deployed_name: Default::default(),
},
)
.await
{
warn!(
"Failed to clear deployment {} ({}) deployed name after destroy | {e:#}",
deployment.name, deployment.id,
);
}
update.logs.push(log);
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
+51 -114
View File
@@ -1,7 +1,7 @@
use std::{fmt::Write as _, sync::OnceLock};
use anyhow::{Context, anyhow};
use command::{CommandOptions, run_komodo_standard_command};
use command::run_komodo_standard_command;
use database::{
bson::{Document, doc},
mungos::find::find_collect,
@@ -15,7 +15,7 @@ use komodo_client::{
},
entities::{
SwarmOrServer, deployment::DeploymentState, server::ServerState,
stack::StackState, swarm::SwarmState,
stack::StackState,
},
};
use mogh_error::AddStatusCodeError;
@@ -39,7 +39,7 @@ use crate::{
resource::rotate_server_keys,
state::{
db_client, deployment_status_cache, server_status_cache,
stack_status_cache, swarm_status_cache,
stack_status_cache,
},
};
@@ -54,18 +54,14 @@ impl Resolve<ExecuteArgs> for ClearRepoCache {
"ClearRepoCache",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id
)
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
if !user.admin {
return Err(
@@ -137,18 +133,14 @@ impl Resolve<ExecuteArgs> for BackupCoreDatabase {
"BackupCoreDatabase",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
)
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
if !user.admin {
return Err(
@@ -167,8 +159,8 @@ impl Resolve<ExecuteArgs> for BackupCoreDatabase {
let res = run_komodo_standard_command(
"Backup Core Database",
None,
"km database backup --yes",
CommandOptions::default(),
)
.await;
@@ -194,18 +186,14 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
"GlobalAutoUpdate",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
)
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
if !user.admin {
return Err(
@@ -244,7 +232,6 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
.context("Failed to query for stacks from database")?;
let server_status_cache = server_status_cache();
let swarm_status_cache = swarm_status_cache();
let stack_status_cache = stack_status_cache();
// Will be edited later at update.logs[0]
@@ -256,7 +243,7 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
continue;
};
// Only check running stacks.
// Only pull running stacks.
if !matches!(status.curr.state, StackState::Running) {
continue;
}
@@ -268,58 +255,43 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
&servers,
)?;
// Ensure server / swarm is reachable
match &swarm_or_server {
SwarmOrServer::None => continue,
SwarmOrServer::Server(server) => {
if !server_status_cache
.get(&server.id)
.await
.map(|s| matches!(s.state, ServerState::Ok))
.unwrap_or_default()
{
continue;
}
}
SwarmOrServer::Swarm(swarm) => {
if !swarm_status_cache
.get(&swarm.id)
.await
.map(|s| {
matches!(
s.state,
SwarmState::Healthy | SwarmState::Unhealthy
)
})
.unwrap_or_default()
{
continue;
}
}
if let SwarmOrServer::None = &swarm_or_server {
continue;
}
if let Err(e) = check_stack_for_update_inner(
stack.id,
&swarm_or_server,
self.skip_auto_update,
true,
false,
)
.await
if let Some(server) =
servers.iter().find(|s| s.id == stack.config.server_id)
// This check is probably redundant along with running check
// but shouldn't hurt
&& server_status_cache
.get(&server.id)
.await
.map(|s| matches!(s.state, ServerState::Ok))
.unwrap_or_default()
{
update.push_error_log(
&format!("Check Stack {}", stack.name),
format_serror(&e.into()),
);
} else {
if !update.logs[0].stdout.is_empty() {
update.logs[0].stdout.push('\n');
}
if let Err(e) = check_stack_for_update_inner(
stack.id,
&swarm_or_server,
self.skip_auto_update,
true,
false,
)
.await
{
update.push_error_log(
&format!("Check Stack {}", stack.name),
format_serror(&e.into()),
);
} else {
if !update.logs[0].stdout.is_empty() {
update.logs[0].stdout.push('\n');
}
update.logs[0].stdout.push_str(&format!(
"Checked Stack {} ✅",
bold(&stack.name)
));
update.logs[0].stdout.push_str(&format!(
"Checked Stack {} ✅",
bold(&stack.name)
));
}
}
}
@@ -336,7 +308,7 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
continue;
};
// Only check running deployments.
// Only pull running deployments.
if !matches!(status.curr.state, DeploymentState::Running) {
continue;
}
@@ -348,38 +320,11 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
&servers,
)?;
// Ensure server / swarm is reachable
match &swarm_or_server {
SwarmOrServer::None => continue,
SwarmOrServer::Server(server) => {
if !server_status_cache
.get(&server.id)
.await
.map(|s| matches!(s.state, ServerState::Ok))
.unwrap_or_default()
{
continue;
}
}
SwarmOrServer::Swarm(swarm) => {
if !swarm_status_cache
.get(&swarm.id)
.await
.map(|s| {
matches!(
s.state,
SwarmState::Healthy | SwarmState::Unhealthy
)
})
.unwrap_or_default()
{
continue;
}
}
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,
@@ -422,18 +367,14 @@ impl Resolve<ExecuteArgs> for RotateAllServerKeys {
"RotateAllServerKeys",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
)
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
if !user.admin {
return Err(
@@ -540,7 +481,7 @@ impl Resolve<ExecuteArgs> for RotateCoreKeys {
"RotateCoreKeys",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
force = self.force,
@@ -548,11 +489,7 @@ impl Resolve<ExecuteArgs> for RotateCoreKeys {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
if !user.admin {
return Err(
+24 -73
View File
@@ -13,7 +13,6 @@ use komodo_client::{
entities::{
Operation,
permission::PermissionLevel,
resource::ResourceQuery,
update::{Log, Update},
user::User,
},
@@ -30,10 +29,7 @@ use uuid::Uuid;
use crate::{
auth::KomodoAuthImpl,
helpers::{
query::get_all_tags,
update::{init_execution_update, update_update},
},
helpers::update::{init_execution_update, update_update},
resource::{KomodoResource, list_full_for_user_using_pattern},
state::db_client,
};
@@ -53,9 +49,9 @@ mod sync;
use super::Variant;
pub struct ExecuteArgs {
/// The task id.
/// Unique for every '/execute' call.
pub task_id: Uuid,
/// The execution id.
/// Unique for every /execute call.
pub id: Uuid,
pub user: User,
pub update: Update,
}
@@ -64,7 +60,7 @@ pub struct ExecuteArgs {
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants,
)]
#[strum_discriminants(name(ExecuteRequestMethod), derive(Display))]
#[strum_discriminants(name(ExecuteRequestVariant), derive(Display))]
#[args(ExecuteArgs)]
#[response(JsonString)]
#[error(mogh_error::Error)]
@@ -115,12 +111,10 @@ pub enum ExecuteRequest {
// ==== PROCEDURE ====
RunProcedure(RunProcedure),
BatchRunProcedure(BatchRunProcedure),
CancelProcedure(CancelProcedure),
// ==== ACTION ====
RunAction(RunAction),
BatchRunAction(BatchRunAction),
CancelAction(CancelAction),
// ==== SYNC ====
RunSync(RunSync),
@@ -154,7 +148,6 @@ pub enum ExecuteRequest {
// ==== SWARM ====
RemoveSwarmNodes(RemoveSwarmNodes),
UpdateSwarmNode(UpdateSwarmNode),
RemoveSwarmStacks(RemoveSwarmStacks),
RemoveSwarmServices(RemoveSwarmServices),
CreateSwarmConfig(CreateSwarmConfig),
@@ -256,19 +249,11 @@ pub fn inner_handler(
async move {
let log = match handle.await {
Ok(Err(e)) => {
warn!(
api = "Execute",
task_id = task_id.to_string(),
"/execute request task error: {e:#}",
);
warn!("/execute request {task_id} task error: {e:#}",);
Log::error("Task Error", format_serror(&e.into()))
}
Err(e) => {
warn!(
api = "Execute",
task_id = task_id.to_string(),
"/execute request spawn error: {e:?}",
);
warn!("/execute request {task_id} spawn error: {e:?}",);
Log::error("Spawn Error", format!("{e:#?}"))
}
_ => return,
@@ -283,7 +268,7 @@ pub fn inner_handler(
find_one_by_id(&db_client().updates, &update_id)
.await
.context("Failed to query to db")?
.context("No Update exists with given id")?;
.context("No update exists with given id")?;
update.logs.push(log);
update.finalize();
update_update(update).await
@@ -292,10 +277,7 @@ pub fn inner_handler(
if let Err(e) = res {
warn!(
api = "Execute",
task_id = task_id.to_string(),
update_id,
"Failed to modify Update with task error log | {e:#}"
"Failed to update update with task error log | {e:#}"
);
}
}
@@ -311,44 +293,25 @@ async fn task(
user: User,
update: Update,
) -> anyhow::Result<String> {
let method: ExecuteRequestMethod = (&request).into();
let user_id = user.id.clone();
let username = user.username.clone();
let variant: ExecuteRequestVariant = (&request).into();
info!(
api = "Execute",
task_id = id.to_string(),
method = method.to_string(),
user_id,
username,
"EXECUTE REQUEST",
"EXECUTE REQUEST {id} | METHOD: {variant} | USER: {} ({})",
user.username, user.id
);
let res = match request
.resolve(&ExecuteArgs {
user,
update,
task_id: 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),
};
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!(
api = "Execute",
task_id = id.to_string(),
method = method.to_string(),
user_id,
username,
"EXECUTE REQUEST | ERROR: {e:#}"
);
warn!("EXECUTE REQUEST {id} | METHOD: {variant} | ERROR: {e:#}");
}
res
@@ -362,26 +325,14 @@ trait BatchExecute {
#[instrument("BatchExecute", skip(user))]
async fn batch_execute<E: BatchExecute>(
pattern: &str,
tags: Vec<String>,
user: &User,
) -> anyhow::Result<BatchExecutionResponse> {
let all_tags = if tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let resources = list_full_for_user_using_pattern::<E::Resource>(
pattern,
ResourceQuery {
tags,
..Default::default()
},
None,
None,
Default::default(),
user,
PermissionLevel::Execute.into(),
&all_tags,
&[],
)
.await?;
+9 -76
View File
@@ -1,14 +1,12 @@
use std::pin::Pin;
use anyhow::Context as _;
use database::mungos::{
by_id::update_one_by_id, mongodb::bson::to_document,
};
use formatting::{Color, bold, colored, format_serror, muted};
use komodo_client::{
api::execute::{
BatchExecutionResponse, BatchRunProcedure, CancelProcedure,
RunProcedure,
BatchExecutionResponse, BatchRunProcedure, RunProcedure,
},
entities::{
alert::{Alert, AlertData, SeverityLevel},
@@ -21,14 +19,13 @@ use komodo_client::{
};
use mogh_resolver::Resolve;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use crate::{
alert::send_alerts,
helpers::{procedure::execute_procedure, update::update_update},
permission::get_check_permissions,
resource::refresh_procedure_state_cache,
state::{action_states, db_client, procedure_cancel_cache},
state::{action_states, db_client},
};
use super::{ExecuteArgs, ExecuteRequest};
@@ -51,12 +48,8 @@ impl Resolve<ExecuteArgs> for BatchRunProcedure {
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunProcedure>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchRunProcedure>(&self.pattern, user)
.await?,
)
}
}
@@ -66,7 +59,7 @@ impl Resolve<ExecuteArgs> for RunProcedure {
"RunProcedure",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
procedure = self.procedure,
@@ -74,11 +67,7 @@ impl Resolve<ExecuteArgs> for RunProcedure {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
Ok(
resolve_inner(self.procedure, user.clone(), update.clone())
@@ -124,22 +113,14 @@ fn resolve_inner(
// This will set action state back to default when dropped.
// Will also check to ensure procedure not already busy before updating.
let action_guard =
let _action_guard =
action_state.update(|state| state.running = true)?;
update_update(update.clone()).await?;
let cancel = CancellationToken::new();
procedure_cancel_cache()
.insert(procedure.id.clone(), cancel.clone())
.await;
let update = Mutex::new(update);
let res = execute_procedure(&procedure, &update, cancel).await;
procedure_cancel_cache().remove(&procedure.id).await;
let res = execute_procedure(&procedure, &update).await;
let mut update = update.into_inner();
@@ -155,15 +136,11 @@ fn resolve_inner(
);
}
Err(e) => update
.push_error_log("Execution error", format_serror(&e.into())),
.push_error_log("execution error", format_serror(&e.into())),
}
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
// The Err case of to_document should be unreachable,
@@ -203,47 +180,3 @@ fn resolve_inner(
Ok(update)
})
}
impl Resolve<ExecuteArgs> for CancelProcedure {
#[instrument(
"CancelProcedure",
skip_all,
fields(
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
procedure = self.procedure,
)
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let procedure = get_check_permissions::<Procedure>(
&self.procedure,
user,
PermissionLevel::Execute.into(),
)
.await?;
procedure_cancel_cache()
.get(&procedure.id)
.await
.context("Procedure run cancel token not found")?
.cancel();
let mut update = update.clone();
update.push_simple_log(
"Cancel Triggered",
"Procedure cancel has been triggered. The procedure will exit after the currently running stage is complete.",
);
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
+31 -79
View File
@@ -55,23 +55,18 @@ impl Resolve<ExecuteArgs> for BatchCloneRepo {
"BatchCloneRepo",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchCloneRepo>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchCloneRepo>(&self.pattern, user)
.await?,
)
}
}
@@ -81,7 +76,7 @@ impl Resolve<ExecuteArgs> for CloneRepo {
"CloneRepo",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
repo = self.repo,
@@ -89,11 +84,7 @@ impl Resolve<ExecuteArgs> for CloneRepo {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let mut repo = get_check_permissions::<Repo>(
&self.repo,
@@ -108,7 +99,7 @@ impl Resolve<ExecuteArgs> for CloneRepo {
// This will set action state back to default when dropped.
// Will also check to ensure repo not already busy before updating.
let action_guard =
let _action_guard =
action_state.update(|state| state.cloning = true)?;
let mut update = update.clone();
@@ -179,10 +170,6 @@ impl Resolve<ExecuteArgs> for CloneRepo {
);
};
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
handle_repo_update_return(update).await
}
}
@@ -199,23 +186,18 @@ impl Resolve<ExecuteArgs> for BatchPullRepo {
"BatchPullRepo",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
pattern = self.pattern
)
)]
async fn resolve(
self,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchPullRepo>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchPullRepo>(&self.pattern, user)
.await?,
)
}
}
@@ -225,7 +207,7 @@ impl Resolve<ExecuteArgs> for PullRepo {
"PullRepo",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
repo = self.repo,
@@ -233,11 +215,7 @@ impl Resolve<ExecuteArgs> for PullRepo {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let mut repo = get_check_permissions::<Repo>(
&self.repo,
@@ -252,7 +230,7 @@ impl Resolve<ExecuteArgs> for PullRepo {
// This will set action state back to default when dropped.
// Will also check to ensure repo not already busy before updating.
let action_guard =
let _action_guard =
action_state.update(|state| state.pulling = true)?;
let mut update = update.clone();
@@ -327,10 +305,6 @@ impl Resolve<ExecuteArgs> for PullRepo {
);
};
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
handle_repo_update_return(update).await
}
}
@@ -389,23 +363,18 @@ impl Resolve<ExecuteArgs> for BatchBuildRepo {
"BatchBuildRepo",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchBuildRepo>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchBuildRepo>(&self.pattern, user)
.await?,
)
}
}
@@ -415,7 +384,7 @@ impl Resolve<ExecuteArgs> for BuildRepo {
"BuildRepo",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
repo = self.repo,
@@ -423,11 +392,7 @@ impl Resolve<ExecuteArgs> for BuildRepo {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let mut repo = get_check_permissions::<Repo>(
&self.repo,
@@ -446,7 +411,7 @@ impl Resolve<ExecuteArgs> for BuildRepo {
// This will set action state back to default when dropped.
// Will also check to ensure repo not already busy before updating.
let action_guard =
let _action_guard =
action_state.update(|state| state.building = true)?;
let mut update = update.clone();
@@ -513,7 +478,7 @@ impl Resolve<ExecuteArgs> for BuildRepo {
repo.name.clone(),
None,
builder,
Some(&mut update),
&mut update,
)
.await
{
@@ -524,9 +489,6 @@ impl Resolve<ExecuteArgs> for BuildRepo {
"get builder",
format_serror(&e.context("failed to get builder").into()),
));
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
return handle_builder_early_return(
update, repo.id, repo.name, false,
)
@@ -559,9 +521,6 @@ impl Resolve<ExecuteArgs> for BuildRepo {
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
info!("builder cleaned up");
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
return handle_builder_early_return(update, repo.id, repo.name, true).await
},
};
@@ -609,10 +568,6 @@ impl Resolve<ExecuteArgs> for BuildRepo {
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
// The Err case of to_document should be unreachable,
@@ -731,12 +686,13 @@ pub async fn validate_cancel_repo_build(
)?;
match (latest_build, latest_cancel) {
(Some(build), Some(cancel))
if cancel.start_ts > build.start_ts =>
{
return Err(anyhow!("Repo build has already been cancelled"));
(Some(build), Some(cancel)) => {
if cancel.start_ts > build.start_ts {
return Err(anyhow!(
"Repo build has already been cancelled"
));
}
}
(None, _) => return Err(anyhow!("No repo build in progress")),
_ => {}
};
@@ -749,7 +705,7 @@ impl Resolve<ExecuteArgs> for CancelRepoBuild {
"CancelRepoBuild",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
repo = self.repo,
@@ -757,11 +713,7 @@ impl Resolve<ExecuteArgs> for CancelRepoBuild {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let repo = get_check_permissions::<Repo>(
&self.repo,
+99 -288
View File
@@ -14,7 +14,7 @@ use periphery_client::api;
use crate::{
helpers::{periphery_client, update::update_update},
monitor::refresh_server_cache,
monitor::update_cache_for_server,
permission::get_check_permissions,
state::action_states,
};
@@ -26,7 +26,7 @@ impl Resolve<ExecuteArgs> for StartContainer {
"StartContainer",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -35,11 +35,7 @@ impl Resolve<ExecuteArgs> for StartContainer {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -56,15 +52,12 @@ impl Resolve<ExecuteArgs> for StartContainer {
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.starting_containers += 1,
|state| state.starting_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.starting_containers = true)?;
let mut update = update.clone();
// Send update after setting action state, this way UI gets correct state.
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server).await?;
@@ -83,13 +76,9 @@ impl Resolve<ExecuteArgs> for StartContainer {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -101,7 +90,7 @@ impl Resolve<ExecuteArgs> for RestartContainer {
"RestartContainer",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -110,11 +99,7 @@ impl Resolve<ExecuteArgs> for RestartContainer {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -131,15 +116,12 @@ impl Resolve<ExecuteArgs> for RestartContainer {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.restarting_containers += 1,
|state| state.restarting_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.restarting_containers = true)?;
let mut update = update.clone();
// Send update after setting action state, this way UI gets correct state.
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server).await?;
@@ -160,13 +142,9 @@ impl Resolve<ExecuteArgs> for RestartContainer {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -178,7 +156,7 @@ impl Resolve<ExecuteArgs> for PauseContainer {
"PauseContainer",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -187,11 +165,7 @@ impl Resolve<ExecuteArgs> for PauseContainer {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -208,15 +182,12 @@ impl Resolve<ExecuteArgs> for PauseContainer {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.pausing_containers += 1,
|state| state.pausing_containers -= 1,
false,
)?;
let _action_guard =
action_state.update(|state| state.pausing_containers = true)?;
let mut update = update.clone();
// Send update after setting action state, this way UI gets correct state.
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server).await?;
@@ -235,13 +206,9 @@ impl Resolve<ExecuteArgs> for PauseContainer {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -253,7 +220,7 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
"UnpauseContainer",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -262,11 +229,7 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -283,15 +246,12 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.unpausing_containers += 1,
|state| state.unpausing_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.unpausing_containers = true)?;
let mut update = update.clone();
// Send update after setting action state, this way UI gets correct state.
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server).await?;
@@ -312,13 +272,9 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -330,7 +286,7 @@ impl Resolve<ExecuteArgs> for StopContainer {
"StopContainer",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -341,11 +297,7 @@ impl Resolve<ExecuteArgs> for StopContainer {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -362,15 +314,12 @@ impl Resolve<ExecuteArgs> for StopContainer {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.stopping_containers += 1,
|state| state.stopping_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.stopping_containers = true)?;
let mut update = update.clone();
// Send update after setting action state, this way UI gets correct state.
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server).await?;
@@ -391,13 +340,9 @@ impl Resolve<ExecuteArgs> for StopContainer {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -409,7 +354,7 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
"DestroyContainer",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -420,11 +365,7 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let DestroyContainer {
server,
@@ -447,15 +388,12 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.destroying_containers += 1,
|state| state.destroying_containers -= 1,
false,
)?;
let _action_guard =
action_state.update(|state| state.pruning_containers = true)?;
let mut update = update.clone();
// Send update after setting action state, this way UI gets correct state.
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server).await?;
@@ -478,13 +416,9 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -496,7 +430,7 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
"StartAllContainers",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -504,11 +438,7 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -525,11 +455,8 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.starting_containers += 1,
|state| state.starting_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.starting_containers = true)?;
let mut update = update.clone();
@@ -550,12 +477,8 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
);
}
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -567,7 +490,7 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
"RestartAllContainers",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -575,11 +498,7 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -596,11 +515,8 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.restarting_containers += 1,
|state| state.restarting_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.restarting_containers = true)?;
let mut update = update.clone();
@@ -623,12 +539,8 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
);
}
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -640,7 +552,7 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
"PauseAllContainers",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -648,11 +560,7 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -669,11 +577,8 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.pausing_containers += 1,
|state| state.pausing_containers -= 1,
false,
)?;
let _action_guard =
action_state.update(|state| state.pausing_containers = true)?;
let mut update = update.clone();
@@ -694,12 +599,8 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
);
}
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -711,7 +612,7 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
"UnpauseAllContainers",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -719,11 +620,7 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -740,11 +637,8 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.unpausing_containers += 1,
|state| state.unpausing_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.unpausing_containers = true)?;
let mut update = update.clone();
@@ -767,12 +661,8 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
);
}
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -784,7 +674,7 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
"StopAllContainers",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -792,11 +682,7 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -813,11 +699,8 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard = action_state.update_custom(
|state| state.stopping_containers += 1,
|state| state.stopping_containers -= 1,
false,
)?;
let _action_guard = action_state
.update(|state| state.stopping_containers = true)?;
let mut update = update.clone();
@@ -838,12 +721,8 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
);
}
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -855,7 +734,7 @@ impl Resolve<ExecuteArgs> for PruneContainers {
"PruneContainers",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -863,11 +742,7 @@ impl Resolve<ExecuteArgs> for PruneContainers {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -884,7 +759,7 @@ impl Resolve<ExecuteArgs> for PruneContainers {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pruning_containers = true)?;
let mut update = update.clone();
@@ -910,13 +785,9 @@ impl Resolve<ExecuteArgs> for PruneContainers {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -928,7 +799,7 @@ impl Resolve<ExecuteArgs> for DeleteNetwork {
"DeleteNetwork",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -937,11 +808,7 @@ impl Resolve<ExecuteArgs> for DeleteNetwork {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -979,7 +846,7 @@ impl Resolve<ExecuteArgs> for DeleteNetwork {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
update_update(update.clone()).await?;
@@ -993,7 +860,7 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
"PruneNetworks",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -1001,11 +868,7 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -1022,7 +885,7 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pruning_networks = true)?;
let mut update = update.clone();
@@ -1046,13 +909,9 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -1064,7 +923,7 @@ impl Resolve<ExecuteArgs> for DeleteImage {
"DeleteImage",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -1073,11 +932,7 @@ impl Resolve<ExecuteArgs> for DeleteImage {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -1112,7 +967,7 @@ impl Resolve<ExecuteArgs> for DeleteImage {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
update_update(update.clone()).await?;
@@ -1126,7 +981,7 @@ impl Resolve<ExecuteArgs> for PruneImages {
"PruneImages",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -1134,11 +989,7 @@ impl Resolve<ExecuteArgs> for PruneImages {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -1155,7 +1006,7 @@ impl Resolve<ExecuteArgs> for PruneImages {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pruning_images = true)?;
let mut update = update.clone();
@@ -1177,13 +1028,9 @@ impl Resolve<ExecuteArgs> for PruneImages {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -1195,7 +1042,7 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
"DeleteVolume",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -1204,11 +1051,7 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -1246,7 +1089,7 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
update_update(update.clone()).await?;
@@ -1260,7 +1103,7 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
"PruneVolumes",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -1268,11 +1111,7 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -1289,7 +1128,7 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pruning_volumes = true)?;
let mut update = update.clone();
@@ -1311,13 +1150,9 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -1329,7 +1164,7 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
"PruneDockerBuilders",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -1337,11 +1172,7 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -1358,7 +1189,7 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pruning_builders = true)?;
let mut update = update.clone();
@@ -1380,13 +1211,9 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -1398,7 +1225,7 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
"PruneBuildx",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -1406,11 +1233,7 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -1427,7 +1250,7 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pruning_buildx = true)?;
let mut update = update.clone();
@@ -1449,13 +1272,9 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -1467,7 +1286,7 @@ impl Resolve<ExecuteArgs> for PruneSystem {
"PruneSystem",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -1475,11 +1294,7 @@ impl Resolve<ExecuteArgs> for PruneSystem {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -1496,7 +1311,7 @@ impl Resolve<ExecuteArgs> for PruneSystem {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let action_guard =
let _action_guard =
action_state.update(|state| state.pruning_system = true)?;
let mut update = update.clone();
@@ -1517,13 +1332,9 @@ impl Resolve<ExecuteArgs> for PruneSystem {
};
update.logs.push(log);
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
+48 -121
View File
@@ -39,7 +39,7 @@ use crate::{
add_update_without_send, init_execution_update, update_update,
},
},
monitor::{refresh_server_cache, refresh_swarm_cache},
monitor::{update_cache_for_server, update_cache_for_swarm},
permission::get_check_permissions,
resource,
stack::{
@@ -69,23 +69,18 @@ impl Resolve<ExecuteArgs> for BatchDeployStack {
"BatchDeployStack",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeployStack>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchDeployStack>(&self.pattern, user)
.await?,
)
}
}
@@ -95,7 +90,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
"DeployStack",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -105,11 +100,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (mut stack, swarm_or_server) = setup_stack_execution(
&self.stack,
@@ -136,7 +127,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
// 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 =
let _action_guard =
action_state.update(|state| state.deploying = true)?;
let mut update = update.clone();
@@ -322,18 +313,14 @@ impl Resolve<ExecuteArgs> for DeployStack {
match swarm_or_server {
SwarmOrServer::None => unreachable!(),
SwarmOrServer::Swarm(swarm) => {
refresh_swarm_cache(&swarm, true).await;
update_cache_for_swarm(&swarm, true).await;
}
SwarmOrServer::Server(server) => {
refresh_server_cache(&server, true).await;
update_cache_for_server(&server, true).await;
}
}
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -355,20 +342,18 @@ impl Resolve<ExecuteArgs> for BatchDeployStackIfChanged {
"BatchDeployStackIfChanged",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeployStackIfChanged>(
&self.pattern,
self.tags,
user,
)
.await?,
@@ -381,7 +366,7 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
"DeployStackIfChanged",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -390,11 +375,7 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let stack = get_check_permissions::<Stack>(
&self.stack,
@@ -452,7 +433,7 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
.resolve(&ExecuteArgs {
user: user.clone(),
update,
task_id: *task_id,
id: *id,
})
.await
}
@@ -591,7 +572,7 @@ async fn deploy_services(
.resolve(&ExecuteArgs {
user: user.clone(),
update,
task_id: Uuid::new_v4(),
id: Uuid::new_v4(),
})
.await
}
@@ -622,7 +603,7 @@ async fn restart_services(
.resolve(&ExecuteArgs {
user: user.clone(),
update,
task_id: Uuid::new_v4(),
id: Uuid::new_v4(),
})
.await
}
@@ -785,23 +766,18 @@ impl Resolve<ExecuteArgs> for BatchPullStack {
"BatchPullStack",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchPullStack>(
&self.pattern,
self.tags,
user,
)
.await?,
super::batch_execute::<BatchPullStack>(&self.pattern, user)
.await?,
)
}
}
@@ -902,7 +878,7 @@ pub async fn pull_stack_inner(
.await?;
// Ensure cached stack state up to date by updating server cache
refresh_server_cache(server, true).await;
update_cache_for_server(server, true).await;
Ok(res)
}
@@ -912,7 +888,7 @@ impl Resolve<ExecuteArgs> for PullStack {
"PullStack",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -921,11 +897,7 @@ impl Resolve<ExecuteArgs> for PullStack {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (stack, swarm_or_server) = setup_stack_execution(
&self.stack,
@@ -959,7 +931,7 @@ impl Resolve<ExecuteArgs> for PullStack {
// 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 =
let _action_guard =
action_state.update(|state| state.pulling = true)?;
let mut update = update.clone();
@@ -976,10 +948,6 @@ impl Resolve<ExecuteArgs> for PullStack {
update.logs.extend(res.logs);
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -991,7 +959,7 @@ impl Resolve<ExecuteArgs> for StartStack {
"StartStack",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -1000,11 +968,7 @@ impl Resolve<ExecuteArgs> for StartStack {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
execute_compose::<StartStack>(
&self.stack,
@@ -1024,7 +988,7 @@ impl Resolve<ExecuteArgs> for RestartStack {
"RestartStack",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -1033,11 +997,7 @@ impl Resolve<ExecuteArgs> for RestartStack {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
execute_compose::<RestartStack>(
&self.stack,
@@ -1059,7 +1019,7 @@ impl Resolve<ExecuteArgs> for PauseStack {
"PauseStack",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -1068,11 +1028,7 @@ impl Resolve<ExecuteArgs> for PauseStack {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
execute_compose::<PauseStack>(
&self.stack,
@@ -1092,7 +1048,7 @@ impl Resolve<ExecuteArgs> for UnpauseStack {
"UnpauseStack",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -1101,11 +1057,7 @@ impl Resolve<ExecuteArgs> for UnpauseStack {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
execute_compose::<UnpauseStack>(
&self.stack,
@@ -1125,7 +1077,7 @@ impl Resolve<ExecuteArgs> for StopStack {
"StopStack",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -1134,11 +1086,7 @@ impl Resolve<ExecuteArgs> for StopStack {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
execute_compose::<StopStack>(
&self.stack,
@@ -1170,23 +1118,18 @@ impl Resolve<ExecuteArgs> for BatchDestroyStack {
"BatchDestroyStack",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDestroyStack>(
&self.pattern,
self.tags,
user,
)
.await
.map_err(Into::into)
super::batch_execute::<BatchDestroyStack>(&self.pattern, user)
.await
.map_err(Into::into)
}
}
@@ -1195,7 +1138,7 @@ impl Resolve<ExecuteArgs> for DestroyStack {
"DestroyStack",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -1206,11 +1149,7 @@ impl Resolve<ExecuteArgs> for DestroyStack {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (stack, swarm_or_server) = setup_stack_execution(
&self.stack,
@@ -1239,11 +1178,11 @@ impl Resolve<ExecuteArgs> for DestroyStack {
// 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 =
let _action_guard =
action_state.update(|state| state.destroying = true)?;
let mut update = update.clone();
// Send update here for UI to recheck action state
// Send update here for frontend to recheck action state
update_update(update.clone()).await?;
match swarm_request(
@@ -1266,13 +1205,9 @@ impl Resolve<ExecuteArgs> for DestroyStack {
}
// Ensure cached stack state up to date by updating swarm cache
refresh_swarm_cache(&swarm, true).await;
update_cache_for_swarm(&swarm, true).await;
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -1298,7 +1233,7 @@ impl Resolve<ExecuteArgs> for RunStackService {
"RunStackService",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -1308,11 +1243,7 @@ impl Resolve<ExecuteArgs> for RunStackService {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (mut stack, swarm_or_server) = setup_stack_execution(
&self.stack,
@@ -1343,7 +1274,7 @@ impl Resolve<ExecuteArgs> for RunStackService {
let action_state =
action_states().stack.get_or_insert_default(&stack.id).await;
let action_guard =
let _action_guard =
action_state.update(|state| state.deploying = true)?;
let mut update = update.clone();
@@ -1403,10 +1334,6 @@ impl Resolve<ExecuteArgs> for RunStackService {
update.logs.push(log);
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
+33 -131
View File
@@ -1,6 +1,10 @@
use formatting::format_serror;
use komodo_client::{
api::execute::*,
api::execute::{
CreateSwarmConfig, CreateSwarmSecret, RemoveSwarmConfigs,
RemoveSwarmNodes, RemoveSwarmSecrets, RemoveSwarmServices,
RemoveSwarmStacks, RotateSwarmConfig, RotateSwarmSecret,
},
entities::{permission::PermissionLevel, swarm::Swarm},
};
use mogh_resolver::Resolve;
@@ -8,7 +12,7 @@ use mogh_resolver::Resolve;
use crate::{
api::execute::ExecuteArgs,
helpers::{swarm::swarm_request, update::update_update},
monitor::refresh_swarm_cache,
monitor::update_cache_for_swarm,
permission::get_check_permissions,
};
@@ -17,7 +21,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmNodes {
"RemoveSwarmNodes",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -27,11 +31,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmNodes {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -55,7 +55,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmNodes {
{
Ok(log) => {
update.logs.push(log);
refresh_swarm_cache(&swarm, true).await;
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Remove Swarm Nodes",
@@ -72,78 +72,12 @@ impl Resolve<ExecuteArgs> for RemoveSwarmNodes {
}
}
impl Resolve<ExecuteArgs> for UpdateSwarmNode {
#[instrument(
"UpdateSwarmNode",
skip_all,
fields(
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
node = self.node,
availability = format!("{:?}", self.availability),
label_add = format!("{:?}", self.label_add),
label_rm = format!("{:?}", self.label_rm),
role = format!("{:?}", self.role),
)
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_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::UpdateSwarmNode {
node: self.node,
availability: self.availability,
label_add: self.label_add,
label_rm: self.label_rm,
role: self.role,
},
)
.await
{
Ok(log) => {
update.logs.push(log);
refresh_swarm_cache(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Update Swarm Node",
format_serror(
&e.context("Failed to update swarm node").into(),
),
),
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for RemoveSwarmStacks {
#[instrument(
"RemoveSwarmStacks",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -153,11 +87,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmStacks {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -181,7 +111,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmStacks {
{
Ok(log) => {
update.logs.push(log);
refresh_swarm_cache(&swarm, true).await;
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Remove Swarm Stacks",
@@ -203,7 +133,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmServices {
"RemoveSwarmServices",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -212,11 +142,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmServices {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -239,7 +165,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmServices {
{
Ok(log) => {
update.logs.push(log);
refresh_swarm_cache(&swarm, true).await;
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Remove Swarm Services",
@@ -261,7 +187,7 @@ impl Resolve<ExecuteArgs> for CreateSwarmConfig {
"CreateSwarmConfig",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -270,11 +196,7 @@ impl Resolve<ExecuteArgs> for CreateSwarmConfig {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -300,7 +222,7 @@ impl Resolve<ExecuteArgs> for CreateSwarmConfig {
{
Ok(log) => {
update.logs.push(log);
refresh_swarm_cache(&swarm, true).await;
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Create Swarm Config",
@@ -322,7 +244,7 @@ impl Resolve<ExecuteArgs> for RotateSwarmConfig {
"RotateSwarmConfig",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -331,11 +253,7 @@ impl Resolve<ExecuteArgs> for RotateSwarmConfig {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -359,7 +277,7 @@ impl Resolve<ExecuteArgs> for RotateSwarmConfig {
{
Ok(logs) => {
update.logs.extend(logs);
refresh_swarm_cache(&swarm, true).await;
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Rotate Swarm Config",
@@ -381,7 +299,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmConfigs {
"RemoveSwarmConfigs",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -390,11 +308,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmConfigs {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -417,7 +331,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmConfigs {
{
Ok(log) => {
update.logs.push(log);
refresh_swarm_cache(&swarm, true).await;
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Remove Swarm Configs",
@@ -439,7 +353,7 @@ impl Resolve<ExecuteArgs> for CreateSwarmSecret {
"CreateSwarmSecret",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -448,11 +362,7 @@ impl Resolve<ExecuteArgs> for CreateSwarmSecret {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -479,7 +389,7 @@ impl Resolve<ExecuteArgs> for CreateSwarmSecret {
{
Ok(log) => {
update.logs.push(log);
refresh_swarm_cache(&swarm, true).await;
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Create Swarm Secret",
@@ -501,7 +411,7 @@ impl Resolve<ExecuteArgs> for RotateSwarmSecret {
"RotateSwarmSecret",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -510,11 +420,7 @@ impl Resolve<ExecuteArgs> for RotateSwarmSecret {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -538,7 +444,7 @@ impl Resolve<ExecuteArgs> for RotateSwarmSecret {
{
Ok(logs) => {
update.logs.extend(logs);
refresh_swarm_cache(&swarm, true).await;
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Rotate Swarm Secret",
@@ -560,7 +466,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmSecrets {
"RemoveSwarmSecrets",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -569,11 +475,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmSecrets {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -596,7 +498,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmSecrets {
{
Ok(log) => {
update.logs.push(log);
refresh_swarm_cache(&swarm, true).await;
update_cache_for_swarm(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Remove Swarm Secrets",
+198 -106
View File
@@ -21,7 +21,6 @@ use komodo_client::{
repo::Repo,
server::Server,
stack::Stack,
swarm::Swarm,
sync::ResourceSync,
update::{Log, Update},
user::sync_user,
@@ -54,7 +53,7 @@ impl Resolve<ExecuteArgs> for RunSync {
"RunSync",
skip_all,
fields(
task_id = task_id.to_string(),
id = id.to_string(),
operator = user.id,
update_id = update.id,
sync = self.sync,
@@ -64,11 +63,7 @@ impl Resolve<ExecuteArgs> for RunSync {
)]
async fn resolve(
self,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
ExecuteArgs { user, update, id }: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let RunSync {
sync,
@@ -98,7 +93,7 @@ impl Resolve<ExecuteArgs> for RunSync {
// This will set action state back to default when dropped.
// Will also check to ensure sync not already busy before updating.
let action_guard =
let _action_guard =
action_state.update(|state| state.syncing = true)?;
let mut update = update.clone();
@@ -139,36 +134,56 @@ impl Resolve<ExecuteArgs> for RunSync {
let Some(resource_type) = match_resource_type else {
return Some(name_or_id);
};
macro_rules! resolve_id_to_name {
($(($Variant:ident, $field:ident)),* $(,)?) => {
match ObjectId::from_str(&name_or_id) {
Ok(_) => match resource_type {
$(
ResourceTargetVariant::$Variant => all_resources
.$field
.get(&name_or_id)
.map(|r| r.name.clone()),
)*
ResourceTargetVariant::System => None,
},
Err(_) => Some(name_or_id),
}
};
match ObjectId::from_str(&name_or_id) {
Ok(_) => match resource_type {
ResourceTargetVariant::Swarm => all_resources
.swarms
.get(&name_or_id)
.map(|s| s.name.clone()),
ResourceTargetVariant::Server => all_resources
.servers
.get(&name_or_id)
.map(|s| s.name.clone()),
ResourceTargetVariant::Stack => all_resources
.stacks
.get(&name_or_id)
.map(|s| s.name.clone()),
ResourceTargetVariant::Deployment => all_resources
.deployments
.get(&name_or_id)
.map(|d| d.name.clone()),
ResourceTargetVariant::Build => all_resources
.builds
.get(&name_or_id)
.map(|b| b.name.clone()),
ResourceTargetVariant::Repo => all_resources
.repos
.get(&name_or_id)
.map(|r| r.name.clone()),
ResourceTargetVariant::Procedure => all_resources
.procedures
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::Action => all_resources
.actions
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::ResourceSync => all_resources
.syncs
.get(&name_or_id)
.map(|s| s.name.clone()),
ResourceTargetVariant::Builder => all_resources
.builders
.get(&name_or_id)
.map(|b| b.name.clone()),
ResourceTargetVariant::Alerter => all_resources
.alerters
.get(&name_or_id)
.map(|a| a.name.clone()),
ResourceTargetVariant::System => None,
},
Err(_) => Some(name_or_id),
}
// New resource types need to be added here manually.
resolve_id_to_name!(
(Server, servers),
(Swarm, swarms),
(Stack, stacks),
(Deployment, deployments),
(Build, builds),
(Repo, repos),
(Procedure, procedures),
(Action, actions),
(ResourceSync, syncs),
(Builder, builders),
(Alerter, alerters),
)
})
.collect::<Vec<_>>()
});
@@ -216,39 +231,136 @@ impl Resolve<ExecuteArgs> for RunSync {
let delete = sync.config.managed || sync.config.delete;
macro_rules! get_deltas {
($(($var:ident, $Type:ident, $field:ident)),* $(,)?) => {
$(
let $var = if sync.config.include_resources {
get_updates_for_execution::<$Type>(
resources.$field,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
)*
};
}
// New resource types need to be added here manually.
get_deltas!(
(server_deltas, Server, servers),
(swarm_deltas, Swarm, swarms),
(stack_deltas, Stack, stacks),
(deployment_deltas, Deployment, deployments),
(build_deltas, Build, builds),
(repo_deltas, Repo, repos),
(procedure_deltas, Procedure, procedures),
(action_deltas, Action, actions),
(builder_deltas, Builder, builders),
(alerter_deltas, Alerter, alerters),
(resource_sync_deltas, ResourceSync, resource_syncs),
);
let server_deltas = if sync.config.include_resources {
get_updates_for_execution::<Server>(
resources.servers,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let stack_deltas = if sync.config.include_resources {
get_updates_for_execution::<Stack>(
resources.stacks,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let deployment_deltas = if sync.config.include_resources {
get_updates_for_execution::<Deployment>(
resources.deployments,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let build_deltas = if sync.config.include_resources {
get_updates_for_execution::<Build>(
resources.builds,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let repo_deltas = if sync.config.include_resources {
get_updates_for_execution::<Repo>(
resources.repos,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let procedure_deltas = if sync.config.include_resources {
get_updates_for_execution::<Procedure>(
resources.procedures,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let action_deltas = if sync.config.include_resources {
get_updates_for_execution::<Action>(
resources.actions,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let builder_deltas = if sync.config.include_resources {
get_updates_for_execution::<Builder>(
resources.builders,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let alerter_deltas = if sync.config.include_resources {
get_updates_for_execution::<Alerter>(
resources.alerters,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let resource_sync_deltas = if sync.config.include_resources {
get_updates_for_execution::<entities::sync::ResourceSync>(
resources.resource_syncs,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let (
variables_to_create,
@@ -283,11 +395,9 @@ impl Resolve<ExecuteArgs> for RunSync {
Default::default()
};
// New resource types need to be added here manually.
if deploy_cache.is_empty()
&& resource_sync_deltas.no_changes()
&& server_deltas.no_changes()
&& swarm_deltas.no_changes()
&& deployment_deltas.no_changes()
&& stack_deltas.no_changes()
&& build_deltas.no_changes()
@@ -311,19 +421,11 @@ impl Resolve<ExecuteArgs> for RunSync {
),
);
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
return Ok(update);
}
// =====================================================
// The ordering these are executed does matter, since
// latter resources may depend on prior synced resources
// already being updated with the declared state.
// =====================================================
// =================
// No deps
maybe_extend(
@@ -344,7 +446,10 @@ impl Resolve<ExecuteArgs> for RunSync {
)
.await,
);
maybe_extend(
&mut update.logs,
ResourceSync::execute_sync_updates(resource_sync_deltas).await,
);
maybe_extend(
&mut update.logs,
Server::execute_sync_updates(server_deltas).await,
@@ -358,43 +463,34 @@ impl Resolve<ExecuteArgs> for RunSync {
Action::execute_sync_updates(action_deltas).await,
);
// Depends on server
maybe_extend(
&mut update.logs,
Swarm::execute_sync_updates(swarm_deltas).await,
);
// Depends on server
// Dependent on server
maybe_extend(
&mut update.logs,
Builder::execute_sync_updates(builder_deltas).await,
);
// Depends on server / builder
maybe_extend(
&mut update.logs,
Repo::execute_sync_updates(repo_deltas).await,
);
// Depends on builder / repo
// Dependant on builder
maybe_extend(
&mut update.logs,
Build::execute_sync_updates(build_deltas).await,
);
// Depends on server / repo
maybe_extend(
&mut update.logs,
Stack::execute_sync_updates(stack_deltas).await,
);
// Depends on repo
maybe_extend(
&mut update.logs,
ResourceSync::execute_sync_updates(resource_sync_deltas).await,
);
// Depends on server / build
// Dependant on server / build
maybe_extend(
&mut update.logs,
Deployment::execute_sync_updates(deployment_deltas).await,
);
// Depends on everything
// stack only depends on server, but maybe will depend on build later.
maybe_extend(
&mut update.logs,
Stack::execute_sync_updates(stack_deltas).await,
);
// Dependant on everything
maybe_extend(
&mut update.logs,
Procedure::execute_sync_updates(procedure_deltas).await,
@@ -446,10 +542,6 @@ impl Resolve<ExecuteArgs> for RunSync {
}
update.finalize();
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
update_update(update.clone()).await?;
Ok(update)
@@ -1,7 +1,7 @@
use anyhow::{Context, anyhow};
use axum::http::HeaderMap;
use hex::ToHex;
use hmac::{Hmac, KeyInit as _, Mac};
use hmac::{Hmac, Mac};
use serde::Deserialize;
use sha2::Sha256;
+4 -12
View File
@@ -1,5 +1,6 @@
use std::sync::Arc;
use anyhow::anyhow;
use axum::{Router, http::HeaderMap};
use komodo_client::entities::resource::Resource;
use mogh_cache::CloneCache;
@@ -40,21 +41,12 @@ trait VerifySecret {
/// Implemented on the integration struct, eg [integrations::github::Github]
trait ExtractBranch {
fn extract_branch(body: &str) -> anyhow::Result<String>;
/// Whether the webhook body's branch matches `expected`.
/// A mismatch is routine and only logged at debug; errors
/// only when the branch cannot be extracted from the body.
fn branch_matches(
body: &str,
expected: &str,
) -> anyhow::Result<bool> {
fn verify_branch(body: &str, expected: &str) -> anyhow::Result<()> {
let branch = Self::extract_branch(body)?;
if branch == expected {
Ok(true)
Ok(())
} else {
debug!(
"Ignoring webhook | push to branch '{branch}' does not match expected branch '{expected}'"
);
Ok(false)
Err(anyhow!("request branch does not match expected"))
}
}
}
+32 -90
View File
@@ -1,4 +1,4 @@
use std::{str::FromStr, sync::OnceLock, time::Duration};
use std::{str::FromStr, sync::OnceLock};
use anyhow::{Context, anyhow};
use komodo_client::{
@@ -23,7 +23,6 @@ use crate::{
},
helpers::update::init_execution_update,
resource,
state::action_states,
};
use super::{ANY_BRANCH, ListenerLockCache};
@@ -38,6 +37,11 @@ impl super::CustomSecret for Build {
}
}
fn build_locks() -> &'static ListenerLockCache {
static BUILD_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
BUILD_LOCKS.get_or_init(Default::default)
}
pub async fn handle_build_webhook<B: super::ExtractBranch>(
build: Build,
body: String,
@@ -46,6 +50,12 @@ pub async fn handle_build_webhook<B: super::ExtractBranch>(
return Ok(());
}
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = build_locks().get_or_insert_default(&build.id).await;
let _lock = lock.lock().await;
// Use the correct target branch when using linked repo.
let branch = if build.config.linked_repo.is_empty() {
build.config.branch
@@ -57,82 +67,25 @@ pub async fn handle_build_webhook<B: super::ExtractBranch>(
.branch
};
if !B::branch_matches(&body, &branch)? {
return Ok(());
}
// Cancel if currently building
if action_states()
.build
.get(&build.id)
.await
.and_then(|states| {
states.get().ok().map(|states| states.building)
})
.unwrap_or_default()
{
let user = git_webhook_user().to_owned();
let cancel = ExecuteRequest::CancelBuild(CancelBuild {
build: build.id.clone(),
});
let update = init_execution_update(&cancel, &user).await?;
let ExecuteRequest::CancelBuild(cancel) = cancel else {
unreachable!()
};
cancel
.resolve(&ExecuteArgs {
user,
update,
task_id: Uuid::new_v4(),
})
.await
.ok();
poll_build_until_cancelled(&build.id).await?;
}
B::verify_branch(&body, &branch)?;
let user = git_webhook_user().to_owned();
let run = ExecuteRequest::RunBuild(RunBuild { build: build.id });
let update = init_execution_update(&run, &user).await?;
let ExecuteRequest::RunBuild(run) = run else {
let req = ExecuteRequest::RunBuild(RunBuild { build: build.id });
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunBuild(req) = req else {
unreachable!()
};
run
req
.resolve(&ExecuteArgs {
user,
update,
task_id: Uuid::new_v4(),
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
Ok(())
}
async fn poll_build_until_cancelled(
build_id: &String,
) -> anyhow::Result<()> {
let action_states = action_states();
// Poll to ensure cancelled
for _ in 0..10 {
if !action_states
.build
.get(build_id)
.await
.and_then(|states| {
states.get().ok().map(|states| states.building)
})
.unwrap_or_default()
{
return Ok(());
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
Err(anyhow!("Build still running after cancel"))
}
// ======
// REPO
// ======
@@ -168,7 +121,7 @@ impl RepoExecution for CloneRepo {
.resolve(&ExecuteArgs {
user,
update,
task_id: Uuid::new_v4(),
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
@@ -192,7 +145,7 @@ impl RepoExecution for PullRepo {
.resolve(&ExecuteArgs {
user,
update,
task_id: Uuid::new_v4(),
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
@@ -216,7 +169,7 @@ impl RepoExecution for BuildRepo {
.resolve(&ExecuteArgs {
user,
update,
task_id: Uuid::new_v4(),
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
@@ -267,9 +220,7 @@ async fn handle_repo_webhook_inner<
let lock = repo_locks().get_or_insert_default(&repo.id).await;
let _lock = lock.lock().await;
if !B::branch_matches(&body, &repo.config.branch)? {
return Ok(());
}
B::verify_branch(&body, &repo.config.branch)?;
E::resolve(repo).await
}
@@ -321,7 +272,7 @@ impl StackExecution for DeployStack {
.resolve(&ExecuteArgs {
user,
update,
task_id: Uuid::new_v4(),
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
@@ -339,7 +290,7 @@ impl StackExecution for DeployStack {
.resolve(&ExecuteArgs {
user,
update,
task_id: Uuid::new_v4(),
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
@@ -400,9 +351,7 @@ pub async fn handle_stack_webhook_inner<
.branch
};
if !B::branch_matches(&body, &branch)? {
return Ok(());
}
B::verify_branch(&body, &branch)?;
E::resolve(stack).await.map_err(|e| e.error)
}
@@ -454,7 +403,7 @@ impl SyncExecution for RunSync {
.resolve(&ExecuteArgs {
user,
update,
task_id: Uuid::new_v4(),
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
@@ -515,9 +464,7 @@ async fn handle_sync_webhook_inner<
.branch
};
if !B::branch_matches(&body, &branch)? {
return Ok(());
}
B::verify_branch(&body, &branch)?;
E::resolve(sync).await
}
@@ -554,10 +501,8 @@ pub async fn handle_procedure_webhook<B: super::ExtractBranch>(
procedure_locks().get_or_insert_default(&procedure.id).await;
let _lock = lock.lock().await;
if target_branch != ANY_BRANCH
&& !B::branch_matches(&body, target_branch)?
{
return Ok(());
if target_branch != ANY_BRANCH {
B::verify_branch(&body, target_branch)?;
}
let user = git_webhook_user().to_owned();
@@ -572,7 +517,7 @@ pub async fn handle_procedure_webhook<B: super::ExtractBranch>(
.resolve(&ExecuteArgs {
user,
update,
task_id: Uuid::new_v4(),
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
@@ -612,10 +557,7 @@ pub async fn handle_action_webhook<B: super::ExtractBranch>(
let branch = B::extract_branch(&body)?;
if target_branch != ANY_BRANCH && branch != target_branch {
debug!(
"Ignoring webhook | push to branch '{branch}' does not match expected branch '{target_branch}'"
);
return Ok(());
return Err(anyhow!("request branch does not match expected"));
}
let user = git_webhook_user().to_owned();
@@ -641,7 +583,7 @@ pub async fn handle_action_webhook<B: super::ExtractBranch>(
.resolve(&ExecuteArgs {
user,
update,
task_id: Uuid::new_v4(),
id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
-23
View File
@@ -222,31 +222,8 @@ where
.status_code(StatusCode::BAD_REQUEST)?;
P::verify_secret(headers, body, R::custom_secret(&resource))
.status_code(StatusCode::UNAUTHORIZED)?;
info!(
resource_type = R::resource_type().to_string(),
resource_id = id,
source_ip = ip.to_string(),
"Successfully authenticated incoming webhook"
);
debug!(
resource_type = R::resource_type().to_string(),
resource_id = id,
source_ip = ip.to_string(),
"Webhook body: {body}"
);
mogh_error::Result::Ok(resource)
}
.with_failure_rate_limit_using_ip(&GENERAL_RATE_LIMITER, &ip)
.await
.inspect_err(|e| {
warn!(
resource_id = id,
source_ip = ip.to_string(),
"Incoming webhook failed | ERROR: {:#}",
e.error
);
})
}
+1 -4
View File
@@ -38,10 +38,7 @@ pub fn app() -> Router {
.nest("/ws", ws::router())
.nest("/client", ts_client::router())
.layer(memory_session_layer(config))
.fallback_service(serve_static_ui(
&config.ui_path,
config.ui_index_force_no_cache,
))
.fallback_service(serve_static_ui(&config.frontend_path))
.layer(cors_layer(config))
}
+12 -61
View File
@@ -3,8 +3,7 @@ use komodo_client::{
api::read::*,
entities::{
action::{
Action, ActionActionState, ActionListItem, ActionSortBy,
ActionState,
Action, ActionActionState, ActionListItem, ActionState,
},
permission::PermissionLevel,
},
@@ -12,13 +11,13 @@ use komodo_client::{
use mogh_resolver::Resolve;
use crate::{
helpers::query::{get_action_state, get_all_tags},
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{action_state_cache, action_states},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetAction {
async fn resolve(
@@ -46,45 +45,15 @@ impl Resolve<ReadArgs> for ListActions {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<ActionListItem> =
match self.sort_by {
ActionSortBy::Name => resource::ListItemSort::Name,
ActionSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| a.name.cmp(&b.name))
}))
}
ActionSortBy::NextRun => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.next_scheduled_run
.cmp(&b.info.next_scheduled_run)
.then_with(|| a.name.cmp(&b.name))
}))
}
};
let actions = resource::list_items_for_user::<Action>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|action| {
states.is_empty() || states.contains(&action.info.state)
},
Ok(
resource::list_for_user::<Action>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
.await?;
Ok(actions)
}
}
@@ -98,28 +67,12 @@ impl Resolve<ReadArgs> for ListFullActions {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user_filtered::<Action, _>(
resource::list_full_for_user::<Action>(
self.query,
limit,
self.page,
user,
PermissionLevel::Read.into(),
&all_tags,
|action| {
let states = states.clone();
async move {
if states.is_empty()
|| states.contains(&get_action_state(&action.id).await)
{
Some(action)
} else {
None
}
}
},
)
.await?,
)
@@ -154,8 +107,6 @@ impl Resolve<ReadArgs> for GetActionsSummary {
) -> mogh_error::Result<GetActionsSummaryResponse> {
let actions = resource::list_full_for_user::<Action>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
+1 -1
View File
@@ -40,7 +40,7 @@ impl Resolve<ReadArgs> for ListAlerts {
FindOptions::builder()
.sort(doc! { "ts": -1 })
.limit(NUM_ALERTS_PER_PAGE as i64)
.skip(self.page.saturating_mul(NUM_ALERTS_PER_PAGE))
.skip(self.page * NUM_ALERTS_PER_PAGE)
.build(),
)
.await
+3 -26
View File
@@ -4,7 +4,7 @@ use database::mungos::mongodb::bson::doc;
use komodo_client::{
api::read::*,
entities::{
alerter::{Alerter, AlerterListItem, AlerterSortBy},
alerter::{Alerter, AlerterListItem},
permission::PermissionLevel,
},
};
@@ -17,7 +17,7 @@ use crate::{
state::db_client,
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetAlerter {
async fn resolve(
@@ -45,30 +45,12 @@ impl Resolve<ReadArgs> for ListAlerters {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<AlerterListItem> =
match self.sort_by {
AlerterSortBy::Name => resource::ListItemSort::Name,
AlerterSortBy::Type => {
resource::ListItemSort::DbField("config.endpoint.type")
}
AlerterSortBy::Enabled => {
resource::ListItemSort::DbField("config.enabled")
}
};
Ok(
resource::list_items_for_user::<Alerter>(
resource::list_for_user::<Alerter>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|_| true,
)
.await?,
)
@@ -85,12 +67,9 @@ impl Resolve<ReadArgs> for ListFullAlerters {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user::<Alerter>(
self.query,
limit as i64,
self.page.saturating_mul(limit),
user,
PermissionLevel::Read.into(),
&all_tags,
@@ -106,8 +85,6 @@ impl Resolve<ReadArgs> for GetAlertersSummary {
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetAlertersSummaryResponse> {
let query = match list_resource_ids_for_user::<Alerter>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
+12 -68
View File
@@ -11,9 +11,7 @@ use komodo_client::{
api::read::*,
entities::{
Operation,
build::{
Build, BuildActionState, BuildListItem, BuildSortBy, BuildState,
},
build::{Build, BuildActionState, BuildListItem, BuildState},
permission::PermissionLevel,
update::UpdateStatus,
},
@@ -27,7 +25,7 @@ use crate::{
state::{action_states, build_state_cache, db_client},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetBuild {
async fn resolve(
@@ -55,47 +53,15 @@ impl Resolve<ReadArgs> for ListBuilds {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<BuildListItem> =
match self.sort_by {
BuildSortBy::Name => resource::ListItemSort::Name,
BuildSortBy::Source => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.files_on_host
.cmp(&b.info.files_on_host)
.then_with(|| {
a.info.linked_repo_name.cmp(&b.info.linked_repo_name)
})
.then_with(|| a.info.repo.cmp(&b.info.repo))
.then_with(|| a.name.cmp(&b.name))
}))
}
BuildSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| a.name.cmp(&b.name))
}))
}
};
let builds = resource::list_items_for_user::<Build>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|build| states.is_empty() || states.contains(&build.info.state),
Ok(
resource::list_for_user::<Build>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
.await?;
Ok(builds)
}
}
@@ -109,29 +75,12 @@ impl Resolve<ReadArgs> for ListFullBuilds {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user_filtered::<Build, _>(
resource::list_full_for_user::<Build>(
self.query,
limit,
self.page,
user,
PermissionLevel::Read.into(),
&all_tags,
|build| {
let states = states.clone();
async move {
if states.is_empty()
|| states
.contains(&resource::get_build_state(&build.id).await)
{
Some(build)
} else {
None
}
}
},
)
.await?,
)
@@ -166,8 +115,6 @@ impl Resolve<ReadArgs> for GetBuildsSummary {
) -> mogh_error::Result<GetBuildsSummaryResponse> {
let builds = resource::list_full_for_user::<Build>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
@@ -217,8 +164,7 @@ impl Resolve<ReadArgs> for GetBuildMonthlyStats {
let curr_ts = unix_timestamp_ms() as i64;
let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS;
let close_ts =
next_day - (self.page as i64).saturating_mul(30 * ONE_DAY_MS);
let close_ts = next_day - self.page as i64 * 30 * ONE_DAY_MS;
let open_ts = close_ts - 30 * ONE_DAY_MS;
let mut build_updates = db_client()
@@ -335,8 +281,6 @@ impl Resolve<ReadArgs> for ListCommonBuildExtraArgs {
};
let builds = resource::list_full_for_user::<Build>(
self.query,
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
+3 -31
View File
@@ -4,7 +4,7 @@ use database::mungos::mongodb::bson::doc;
use komodo_client::{
api::read::*,
entities::{
builder::{Builder, BuilderListItem, BuilderSortBy},
builder::{Builder, BuilderListItem},
permission::PermissionLevel,
},
};
@@ -17,7 +17,7 @@ use crate::{
state::db_client,
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetBuilder {
async fn resolve(
@@ -45,35 +45,12 @@ impl Resolve<ReadArgs> for ListBuilders {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<BuilderListItem> =
match self.sort_by {
BuilderSortBy::Name => resource::ListItemSort::Name,
BuilderSortBy::Provider => {
resource::ListItemSort::DbField("config.type")
}
BuilderSortBy::InstanceType => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.instance_type
.cmp(&b.info.instance_type)
.then_with(|| a.name.cmp(&b.name))
}))
}
};
Ok(
resource::list_items_for_user::<Builder>(
resource::list_for_user::<Builder>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|_| true,
)
.await?,
)
@@ -90,12 +67,9 @@ impl Resolve<ReadArgs> for ListFullBuilders {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user::<Builder>(
self.query,
limit as i64,
self.page.saturating_mul(limit),
user,
PermissionLevel::Read.into(),
&all_tags,
@@ -111,8 +85,6 @@ impl Resolve<ReadArgs> for GetBuildersSummary {
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetBuildersSummaryResponse> {
let query = match list_resource_ids_for_user::<Builder>(
None,
None,
None,
user,
PermissionLevel::Read.into(),
+29 -99
View File
@@ -6,8 +6,8 @@ use komodo_client::{
entities::{
SwarmOrServer,
deployment::{
Deployment, DeploymentActionState, DeploymentListItem,
DeploymentSortBy, DeploymentState,
Deployment, DeploymentActionState, DeploymentConfig,
DeploymentListItem, DeploymentState,
},
docker::{
container::{Container, ContainerStats},
@@ -25,9 +25,7 @@ use reqwest::StatusCode;
use crate::{
helpers::{
periphery_client,
query::{get_all_tags, get_deployment_state},
swarm::swarm_request,
periphery_client, query::get_all_tags, swarm::swarm_request,
},
permission::get_check_permissions,
resource::{self, setup_deployment_execution},
@@ -36,7 +34,7 @@ use crate::{
},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetDeployment {
async fn resolve(
@@ -65,66 +63,21 @@ impl Resolve<ReadArgs> for ListDeployments {
get_all_tags(None).await?
};
let only_update_available = self.query.specific.update_available;
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<DeploymentListItem> =
match self.sort_by {
DeploymentSortBy::Name => resource::ListItemSort::Name,
DeploymentSortBy::Image => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.image
.cmp(&b.info.image)
.then_with(|| a.name.cmp(&b.name))
}))
}
DeploymentSortBy::Host => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
let host_a = if a.info.swarm_id.is_empty() {
&a.info.server_name
} else {
&a.info.swarm_name
};
let host_b = if b.info.swarm_id.is_empty() {
&b.info.server_name
} else {
&b.info.swarm_name
};
host_a.cmp(host_b).then_with(|| a.name.cmp(&b.name))
}))
}
DeploymentSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| {
// Use ! with update available to order 'true' first
(!a.info.update_available)
.cmp(&!b.info.update_available)
})
.then_with(|| a.name.cmp(&b.name))
}))
}
};
let deployments = resource::list_items_for_user::<Deployment>(
let deployments = resource::list_for_user::<Deployment>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|deployment| {
(!only_update_available || deployment.info.update_available)
&& (states.is_empty()
|| states.contains(&deployment.info.state))
},
)
.await?;
let deployments = if only_update_available {
deployments
.into_iter()
.filter(|deployment| deployment.info.update_available)
.collect()
} else {
deployments
};
Ok(deployments)
}
}
@@ -139,32 +92,12 @@ impl Resolve<ReadArgs> for ListFullDeployments {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user_filtered::<Deployment, _>(
resource::list_full_for_user::<Deployment>(
self.query,
limit,
self.page,
user,
PermissionLevel::Read.into(),
&all_tags,
|deployment| {
let states = states.clone();
async move {
if states.is_empty()
|| states.contains(
&get_deployment_state(&deployment.id)
.await
.unwrap_or_default(),
)
{
Some(deployment)
} else {
None
}
}
},
)
.await?,
)
@@ -221,7 +154,7 @@ impl Resolve<ReadArgs> for GetDeploymentLog {
SwarmOrServer::Swarm(swarm) => swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::GetSwarmServiceLog {
service: deployment.deployed_name().to_string(),
service: deployment.name,
tail,
timestamps,
no_task_ids: false,
@@ -234,7 +167,7 @@ impl Resolve<ReadArgs> for GetDeploymentLog {
SwarmOrServer::Server(server) => periphery_client(&server)
.await?
.request(api::container::GetContainerLog {
name: deployment.deployed_name().to_string(),
name: deployment.name,
tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps,
})
@@ -273,7 +206,7 @@ impl Resolve<ReadArgs> for SearchDeploymentLog {
SwarmOrServer::Swarm(swarm) => swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::GetSwarmServiceLogSearch {
service: deployment.deployed_name().to_string(),
service: deployment.name,
terms,
combinator,
invert,
@@ -288,7 +221,7 @@ impl Resolve<ReadArgs> for SearchDeploymentLog {
SwarmOrServer::Server(server) => periphery_client(&server)
.await?
.request(api::container::GetContainerLogSearch {
name: deployment.deployed_name().to_string(),
name: deployment.name,
terms,
combinator,
invert,
@@ -341,7 +274,7 @@ impl Resolve<ReadArgs> for InspectDeploymentContainer {
periphery_client(&server)
.await?
.request(InspectContainer {
name: deployment.deployed_name().to_string(),
name: deployment.name,
})
.await
.context("Failed to inspect container on server")
@@ -374,7 +307,7 @@ impl Resolve<ReadArgs> for InspectDeploymentSwarmService {
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmService {
service: deployment.deployed_name().to_string(),
service: deployment.name,
},
)
.await
@@ -388,24 +321,25 @@ impl Resolve<ReadArgs> for GetDeploymentStats {
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ContainerStats> {
let deployment = get_check_permissions::<Deployment>(
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read.into(),
)
.await?;
if deployment.config.server_id.is_empty() {
if server_id.is_empty() {
return Err(
anyhow!("Deployment has no Server attached").into(),
anyhow!("deployment has no server attached").into(),
);
}
let server =
resource::get::<Server>(&deployment.config.server_id).await?;
let server = resource::get::<Server>(&server_id).await?;
let res = periphery_client(&server)
.await?
.request(api::container::GetContainerStats {
name: deployment.deployed_name().to_string(),
})
.request(api::container::GetContainerStats { name })
.await
.context("failed to get stats from periphery")?;
Ok(res)
@@ -440,8 +374,6 @@ impl Resolve<ReadArgs> for GetDeploymentsSummary {
) -> mogh_error::Result<GetDeploymentsSummaryResponse> {
let deployments = resource::list_full_for_user::<Deployment>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
@@ -490,8 +422,6 @@ impl Resolve<ReadArgs> for ListCommonDeploymentExtraArgs {
};
let deployments = resource::list_full_for_user::<Deployment>(
self.query,
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
-590
View File
@@ -1,590 +0,0 @@
use std::cmp;
use anyhow::{Context as _, anyhow};
use database::bson::doc;
use komodo_client::{
api::read::*,
entities::{
ResourceTarget,
deployment::Deployment,
docker::{
container::{
Container, ContainerListItem, ContainerSortBy,
ContainerStateStatusEnum,
},
image::{Image, ImageHistoryResponseItem},
network::Network,
volume::Volume,
},
permission::PermissionLevel,
server::{Server, ServerQuery, ServerState},
stack::{Stack, StackServiceNames},
update::Log,
},
};
use mogh_resolver::Resolve;
use periphery_client::api as periphery;
use crate::{
api::read::{ReadArgs, list_limit},
helpers::{periphery_client, query::get_all_tags},
permission::{get_check_permissions, list_resources_for_user},
resource,
stack::compose_container_match_regex,
state::{db_client, server_status_cache},
};
impl Resolve<ReadArgs> for GetContainersSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetContainersSummaryResponse> {
let servers = resource::list_full_for_user::<Server>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
)
.await
.context("failed to get servers from db")?;
let mut res = GetContainersSummaryResponse::default();
for server in servers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
for container in &docker.containers {
res.total += 1;
match container.state {
ContainerStateStatusEnum::Created
| ContainerStateStatusEnum::Paused
| ContainerStateStatusEnum::Exited => res.stopped += 1,
ContainerStateStatusEnum::Running => res.running += 1,
ContainerStateStatusEnum::Empty => res.unknown += 1,
_ => res.unhealthy += 1,
}
}
}
}
Ok(res)
}
}
impl Resolve<ReadArgs> for ListAllContainers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListAllContainersResponse> {
let all_tags = if self.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let servers = resource::list_for_user::<Server>(
ServerQuery::builder()
.names(self.servers.clone())
.tags(self.tags)
.build(),
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?;
let mut containers = Vec::<ContainerListItem>::new();
let limit = list_limit(self.limit);
// Match terms case insensitively.
let terms = self
.terms
.iter()
.map(|term| term.to_lowercase())
.collect::<Vec<_>>();
for server in servers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
let Some(docker) = &cache.docker else {
continue;
};
containers.extend(
docker
.containers
.iter()
.filter(|container| {
// Apply state filter if defined.
(self.state.is_empty() || self.state.contains(&container.state)) &&
// Apply terms filter if defined
(terms.is_empty()
// Match when all terms contained within a name.
|| {
let name = container.name.to_lowercase();
terms.iter().all(|term| name.contains(term))
})
})
.cloned(),
);
}
// The containers all come from the in memory status cache,
// so all matching containers are collected and sorted
// before applying pagination.
let compare = |a: &ContainerListItem, b: &ContainerListItem| {
match self.sort_by {
ContainerSortBy::Name => a.name.cmp(&b.name),
ContainerSortBy::Server => a.server_name.cmp(&b.server_name),
ContainerSortBy::State => a.state.cmp(&b.state),
ContainerSortBy::Image => a.image.cmp(&b.image),
ContainerSortBy::Networks => {
a.networks.first().cmp(&b.networks.first())
}
ContainerSortBy::Ports => a
.ports
.first()
.map(|port| port.private_port)
.cmp(&b.ports.first().map(|port| port.private_port)),
ContainerSortBy::Volumes => {
a.volumes.first().cmp(&b.volumes.first())
}
}
// Fall back to name based sorting for equal sort keys.
// Inside `compare`, so descending sorts are fully descending,
// matching the List<Resource> apis.
.then_with(|| a.name.cmp(&b.name))
};
if self.sort_desc {
containers.sort_by(|a, b| compare(b, a));
} else {
containers.sort_by(|a, b| compare(a, b));
}
let skip = limit.saturating_mul(self.page) as usize;
let take = if limit == 0 {
usize::MAX
} else {
limit as usize
};
Ok(containers.into_iter().skip(skip).take(take).collect())
}
}
impl Resolve<ReadArgs> for ListContainers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListContainersResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.containers.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Container> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.inspect(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(periphery::container::InspectContainer {
name: self.container,
})
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for GetResourceMatchingContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetResourceMatchingContainerResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
// First check deployments with a matching deployed / custom container name.
// The empty check is required to avoid matching
// deployments with no custom name configured.
if !self.container.is_empty()
&& let Ok(Some(deployment)) = db_client()
.deployments
.find_one(doc! {
"$or": [
{ "info.deployed_name": &self.container },
{ "config.custom_name": &self.container },
]
})
.await
{
return Ok(GetResourceMatchingContainerResponse {
resource: ResourceTarget::Deployment(deployment.id).into(),
});
}
// Then check deployments matching by name
if let Ok(deployment) =
resource::get::<Deployment>(&self.container).await
&& deployment.custom_name() == self.container
{
return Ok(GetResourceMatchingContainerResponse {
resource: ResourceTarget::Deployment(deployment.id).into(),
});
}
// then check stacks
let stacks = list_resources_for_user::<Stack>(
doc! { "config.server_id": &server.id },
None,
None,
user,
PermissionLevel::Read.into(),
)
.await?;
// check matching stack
for stack in stacks {
for StackServiceNames {
service_name,
container_name,
..
} in stack
.info
.deployed_services
.unwrap_or(stack.info.latest_services)
{
let is_match = match compose_container_match_regex(&container_name)
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
{
Ok(regex) => regex,
Err(e) => {
warn!("{e:#}");
continue;
}
}.is_match(&self.container);
if is_match {
return Ok(GetResourceMatchingContainerResponse {
resource: ResourceTarget::Stack(stack.id).into(),
});
}
}
}
Ok(GetResourceMatchingContainerResponse { resource: None })
}
}
const MAX_LOG_LENGTH: u64 = 5000;
impl Resolve<ReadArgs> for GetContainerLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Log> {
let GetContainerLog {
server,
container,
tail,
timestamps,
} = self;
let server = get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read.logs(),
)
.await?;
let res = periphery_client(&server)
.await?
.request(periphery::container::GetContainerLog {
name: container,
tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps,
})
.await
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for SearchContainerLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Log> {
let SearchContainerLog {
server,
container,
terms,
combinator,
invert,
timestamps,
} = self;
let server = get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read.logs(),
)
.await?;
let res = periphery_client(&server)
.await?
.request(periphery::container::GetContainerLogSearch {
name: container,
terms,
combinator,
invert,
timestamps,
})
.await
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListComposeProjects {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListComposeProjectsResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.projects.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for ListNetworks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListNetworksResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.networks.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectNetwork {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Network> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect network: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(periphery::docker::InspectNetwork {
name: self.network,
})
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListImages {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListImagesResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.images.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectImage {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Image> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!("Cannot inspect image: server is {:?}", cache.state)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(periphery::docker::InspectImage { name: self.image })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListImageHistory {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Vec<ImageHistoryResponseItem>> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot get image history: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(periphery::docker::ImageHistory { name: self.image })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListVolumes {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListVolumesResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.volumes.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectVolume {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Volume> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!("Cannot inspect volume: server is {:?}", cache.state)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(periphery::docker::InspectVolume { name: self.volume })
.await?;
Ok(res)
}
}
+47 -92
View File
@@ -10,7 +10,7 @@ use komodo_client::{
ResourceTarget,
build::Build,
builder::{Builder, BuilderConfig},
config::{GitProvider, ImageRegistry},
config::{DockerRegistry, GitProvider},
permission::PermissionLevel,
repo::Repo,
server::Server,
@@ -44,7 +44,6 @@ mod alerter;
mod build;
mod builder;
mod deployment;
mod docker;
mod onboarding_key;
mod permission;
mod procedure;
@@ -67,17 +66,11 @@ pub struct ReadArgs {
pub user: User,
}
/// Resolve the page limit for List apis, falling back to the
/// configured `default_pagination_limit` when not provided.
fn list_limit(limit: Option<u64>) -> u64 {
limit.unwrap_or(core_config().default_pagination_limit)
}
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants,
)]
#[strum_discriminants(name(ReadRequestMethod), derive(Display))]
#[strum_discriminants(name(ReadRequestVariant), derive(Display))]
#[args(ReadArgs)]
#[response(Response)]
#[error(mogh_error::Error)]
@@ -87,8 +80,7 @@ enum ReadRequest {
GetCoreInfo(GetCoreInfo),
ListSecrets(ListSecrets),
ListGitProvidersFromConfig(ListGitProvidersFromConfig),
#[serde(alias = "ListDockerRegistriesFromConfig")]
ListImageRegistriesFromConfig(ListImageRegistriesFromConfig),
ListDockerRegistriesFromConfig(ListDockerRegistriesFromConfig),
// ==== SWARM ====
GetSwarmsSummary(GetSwarmsSummary),
@@ -125,33 +117,22 @@ enum ReadRequest {
// ==== TERMINAL ====
ListTerminals(ListTerminals),
// ==== CONTAINER ====
#[serde(alias = "GetDockerContainersSummary")]
GetContainersSummary(GetContainersSummary),
#[serde(alias = "ListAllDockerContainers")]
ListAllContainers(ListAllContainers),
#[serde(alias = "ListDockerContainers")]
ListContainers(ListContainers),
#[serde(alias = "InspectDockerContainer")]
InspectContainer(InspectContainer),
// ==== DOCKER ====
GetDockerContainersSummary(GetDockerContainersSummary),
ListAllDockerContainers(ListAllDockerContainers),
ListDockerContainers(ListDockerContainers),
InspectDockerContainer(InspectDockerContainer),
GetResourceMatchingContainer(GetResourceMatchingContainer),
GetContainerLog(GetContainerLog),
SearchContainerLog(SearchContainerLog),
ListComposeProjects(ListComposeProjects),
#[serde(alias = "ListDockerNetworks")]
ListNetworks(ListNetworks),
#[serde(alias = "InspectDockerNetwork")]
InspectNetwork(InspectNetwork),
#[serde(alias = "ListDockerImages")]
ListImages(ListImages),
#[serde(alias = "InspectDockerImage")]
InspectImage(InspectImage),
#[serde(alias = "ListDockerImageHistory")]
ListImageHistory(ListImageHistory),
#[serde(alias = "ListDockerVolumes")]
ListVolumes(ListVolumes),
#[serde(alias = "InspectDockerVolume")]
InspectVolume(InspectVolume),
ListDockerNetworks(ListDockerNetworks),
InspectDockerNetwork(InspectDockerNetwork),
ListDockerImages(ListDockerImages),
InspectDockerImage(InspectDockerImage),
ListDockerImageHistory(ListDockerImageHistory),
ListDockerVolumes(ListDockerVolumes),
InspectDockerVolume(InspectDockerVolume),
// ==== SERVER STATS ====
GetSystemInformation(GetSystemInformation),
@@ -170,7 +151,6 @@ enum ReadRequest {
ListStacks(ListStacks),
ListFullStacks(ListFullStacks),
ListStackServices(ListStackServices),
ListAllStackServices(ListAllStackServices),
ListCommonStackExtraArgs(ListCommonStackExtraArgs),
ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs),
@@ -278,10 +258,8 @@ enum ReadRequest {
// ==== PROVIDER ====
GetGitProviderAccount(GetGitProviderAccount),
ListGitProviderAccounts(ListGitProviderAccounts),
#[serde(alias = "GetDockerRegistryAccount")]
GetImageRegistryAccount(GetImageRegistryAccount),
#[serde(alias = "ListDockerRegistryAccounts")]
ListImageRegistryAccounts(ListImageRegistryAccounts),
GetDockerRegistryAccount(GetDockerRegistryAccount),
ListDockerRegistryAccounts(ListDockerRegistryAccounts),
// ==== ONBOARDING KEY ====
ListOnboardingKeys(ListOnboardingKeys),
@@ -313,28 +291,18 @@ async fn handler(
Json(request): Json<ReadRequest>,
) -> mogh_error::Result<axum::response::Response> {
let req_id = Uuid::new_v4();
let method: ReadRequestMethod = (&request).into();
let variant: ReadRequestVariant = (&request).into();
let user_id = user.id.clone();
let username = user.username.clone();
trace!(
req_id = req_id.to_string(),
method = method.to_string(),
user_id,
username,
"READ REQUEST",
debug!(
"READ REQUEST {req_id} | METHOD: {variant} | USER: {} ({})",
user.username, user.id
);
let res = request.resolve(&ReadArgs { user }).await;
if let Err(e) = &res {
trace!(
req_id = req_id.to_string(),
method = method.to_string(),
user_id,
username,
"READ REQUEST | ERROR: {:#}",
debug!(
"READ REQUEST {req_id} | METHOD: {variant} | ERROR: {:#}",
e.error
);
}
@@ -376,7 +344,6 @@ impl Resolve<ReadArgs> for GetCoreInfo {
disable_websocket_reconnect: config.disable_websocket_reconnect,
enable_fancy_toml: config.enable_fancy_toml,
timezone: config.timezone.clone(),
default_pagination_limit: config.default_pagination_limit,
public_key: core_keys().load().public.to_string(),
};
Ok(info)
@@ -402,9 +369,7 @@ impl Resolve<ReadArgs> for ListSecrets {
ResourceTarget::Builder(id) => {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => None,
BuilderConfig::Server(config) => {
config.server_ids.first().cloned()
}
BuilderConfig::Server(config) => Some(config.server_id),
BuilderConfig::Aws(config) => {
secrets.extend(config.secrets);
None
@@ -459,13 +424,11 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => {}
BuilderConfig::Server(config) => {
if let Some(server_id) = config.server_ids.first() {
merge_git_providers_for_server(
&mut providers,
server_id,
)
.await?;
}
merge_git_providers_for_server(
&mut providers,
&config.server_id,
)
.await?;
}
BuilderConfig::Aws(config) => {
merge_git_providers(
@@ -487,24 +450,18 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
let (builds, repos, syncs) = tokio::try_join!(
resource::list_full_for_user::<Build>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[]
),
resource::list_full_for_user::<Repo>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[]
),
resource::list_full_for_user::<ResourceSync>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[]
@@ -556,35 +513,33 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
//
impl Resolve<ReadArgs> for ListImageRegistriesFromConfig {
impl Resolve<ReadArgs> for ListDockerRegistriesFromConfig {
async fn resolve(
self,
_: &ReadArgs,
) -> mogh_error::Result<ListImageRegistriesFromConfigResponse> {
let mut registries = core_config().image_registries.clone();
) -> mogh_error::Result<ListDockerRegistriesFromConfigResponse> {
let mut registries = core_config().docker_registries.clone();
if let Some(target) = self.target {
match target {
ResourceTarget::Server(id) => {
merge_image_registries_for_server(&mut registries, &id)
merge_docker_registries_for_server(&mut registries, &id)
.await?;
}
ResourceTarget::Builder(id) => {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => {}
BuilderConfig::Server(config) => {
if let Some(server_id) = config.server_ids.first() {
merge_image_registries_for_server(
&mut registries,
server_id,
)
.await?;
}
merge_docker_registries_for_server(
&mut registries,
&config.server_id,
)
.await?;
}
BuilderConfig::Aws(config) => {
merge_image_registries(
merge_docker_registries(
&mut registries,
config.image_registries,
config.docker_registries,
);
}
}
@@ -642,14 +597,14 @@ fn merge_git_providers(
}
}
async fn merge_image_registries_for_server(
registries: &mut Vec<ImageRegistry>,
async fn merge_docker_registries_for_server(
registries: &mut Vec<DockerRegistry>,
server_id: &str,
) -> mogh_error::Result<()> {
let server = resource::get::<Server>(server_id).await?;
let more = periphery_client(&server)
.await?
.request(periphery_client::api::ListImageRegistries {})
.request(periphery_client::api::ListDockerRegistries {})
.await
.with_context(|| {
format!(
@@ -657,13 +612,13 @@ async fn merge_image_registries_for_server(
server.name
)
})?;
merge_image_registries(registries, more);
merge_docker_registries(registries, more);
Ok(())
}
fn merge_image_registries(
registries: &mut Vec<ImageRegistry>,
more: Vec<ImageRegistry>,
fn merge_docker_registries(
registries: &mut Vec<DockerRegistry>,
more: Vec<DockerRegistry>,
) {
for incoming_registry in more {
if let Some(registry) = registries
+12 -63
View File
@@ -3,21 +3,19 @@ use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel,
procedure::{
Procedure, ProcedureListItem, ProcedureSortBy, ProcedureState,
},
procedure::{Procedure, ProcedureState},
},
};
use mogh_resolver::Resolve;
use crate::{
helpers::query::{get_all_tags, get_procedure_state},
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{action_states, procedure_state_cache},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetProcedure {
async fn resolve(
@@ -45,45 +43,15 @@ impl Resolve<ReadArgs> for ListProcedures {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<ProcedureListItem> =
match self.sort_by {
ProcedureSortBy::Name => resource::ListItemSort::Name,
ProcedureSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| a.name.cmp(&b.name))
}))
}
ProcedureSortBy::NextRun => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.next_scheduled_run
.cmp(&b.info.next_scheduled_run)
.then_with(|| a.name.cmp(&b.name))
}))
}
};
let procedures = resource::list_items_for_user::<Procedure>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|procedure| {
states.is_empty() || states.contains(&procedure.info.state)
},
Ok(
resource::list_for_user::<Procedure>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
.await?;
Ok(procedures)
}
}
@@ -97,29 +65,12 @@ impl Resolve<ReadArgs> for ListFullProcedures {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user_filtered::<Procedure, _>(
resource::list_full_for_user::<Procedure>(
self.query,
limit,
self.page,
user,
PermissionLevel::Read.into(),
&all_tags,
|procedure| {
let states = states.clone();
async move {
if states.is_empty()
|| states
.contains(&get_procedure_state(&procedure.id).await)
{
Some(procedure)
} else {
None
}
}
},
)
.await?,
)
@@ -133,8 +84,6 @@ impl Resolve<ReadArgs> for GetProceduresSummary {
) -> mogh_error::Result<GetProceduresSummaryResponse> {
let procedures = resource::list_full_for_user::<Procedure>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
+4 -4
View File
@@ -61,11 +61,11 @@ impl Resolve<ReadArgs> for ListGitProviderAccounts {
}
}
impl Resolve<ReadArgs> for GetImageRegistryAccount {
impl Resolve<ReadArgs> for GetDockerRegistryAccount {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetImageRegistryAccountResponse> {
) -> mogh_error::Result<GetDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can read docker registry accounts")
@@ -83,11 +83,11 @@ impl Resolve<ReadArgs> for GetImageRegistryAccount {
}
}
impl Resolve<ReadArgs> for ListImageRegistryAccounts {
impl Resolve<ReadArgs> for ListDockerRegistryAccounts {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListImageRegistryAccountsResponse> {
) -> mogh_error::Result<ListDockerRegistryAccountsResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can read docker registry accounts")
+11 -58
View File
@@ -3,9 +3,7 @@ use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel,
repo::{
Repo, RepoActionState, RepoListItem, RepoSortBy, RepoState,
},
repo::{Repo, RepoActionState, RepoListItem, RepoState},
},
};
use mogh_resolver::Resolve;
@@ -17,7 +15,7 @@ use crate::{
state::{action_states, repo_state_cache},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetRepo {
async fn resolve(
@@ -45,41 +43,15 @@ impl Resolve<ReadArgs> for ListRepos {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<RepoListItem> =
match self.sort_by {
RepoSortBy::Name => resource::ListItemSort::Name,
RepoSortBy::Repo => {
resource::ListItemSort::DbField("config.repo")
}
RepoSortBy::Branch => {
resource::ListItemSort::DbField("config.branch")
}
RepoSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| a.name.cmp(&b.name))
}))
}
};
let repos = resource::list_items_for_user::<Repo>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|repo| states.is_empty() || states.contains(&repo.info.state),
Ok(
resource::list_for_user::<Repo>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
.await?;
Ok(repos)
}
}
@@ -93,29 +65,12 @@ impl Resolve<ReadArgs> for ListFullRepos {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user_filtered::<Repo, _>(
resource::list_full_for_user::<Repo>(
self.query,
limit,
self.page,
user,
PermissionLevel::Read.into(),
&all_tags,
|repo| {
let states = states.clone();
async move {
if states.is_empty()
|| states
.contains(&resource::get_repo_state(&repo.id).await)
{
Some(repo)
} else {
None
}
}
},
)
.await?,
)
@@ -150,8 +105,6 @@ impl Resolve<ReadArgs> for GetReposSummary {
) -> mogh_error::Result<GetReposSummaryResponse> {
let repos = resource::list_full_for_user::<Repo>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
+15 -68
View File
@@ -1,15 +1,13 @@
use std::cmp::Ordering;
use futures_util::future::join_all;
use komodo_client::{
api::read::*,
entities::{
ResourceTarget,
action::{Action, ActionQuerySpecifics},
action::Action,
permission::PermissionLevel,
procedure::{Procedure, ProcedureQuerySpecifics},
procedure::Procedure,
resource::{ResourceQuery, TemplatesQueryBehavior},
schedule::{Schedule, ScheduleSortBy},
schedule::Schedule,
},
};
use mogh_resolver::Resolve;
@@ -20,7 +18,7 @@ use crate::{
schedule::get_schedule_item_info,
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for ListSchedules {
async fn resolve(
@@ -31,36 +29,24 @@ impl Resolve<ReadArgs> for ListSchedules {
let (actions, procedures) = tokio::try_join!(
list_full_for_user::<Action>(
ResourceQuery {
names: Default::default(),
templates: TemplatesQueryBehavior::Include,
tag_behavior: self.tag_behavior,
tags: self.tags.clone(),
terms: self.terms.clone(),
specific: ActionQuerySpecifics {
scheduled: Some(true),
..Default::default()
},
..Default::default()
specific: Default::default(),
},
None,
None,
&args.user,
PermissionLevel::Read.into(),
&all_tags,
),
list_full_for_user::<Procedure>(
ResourceQuery {
names: Default::default(),
templates: TemplatesQueryBehavior::Include,
tag_behavior: self.tag_behavior,
tags: self.tags.clone(),
terms: self.terms,
specific: ProcedureQuerySpecifics {
scheduled: Some(true),
..Default::default()
},
..Default::default()
specific: Default::default(),
},
None,
None,
&args.user,
PermissionLevel::Read.into(),
&all_tags,
@@ -110,51 +96,12 @@ impl Resolve<ReadArgs> for ListSchedules {
let (actions, procedures) =
tokio::join!(join_all(actions), join_all(procedures));
// The terms / scheduled filters are already applied
// at the db level by the queries above.
let mut schedules =
actions.into_iter().chain(procedures).collect::<Vec<_>>();
// The schedules are composed in memory across resource types,
// so all matching schedules are collected and sorted
// before applying pagination.
// All comparators fall back to name based sorting for equal
// sort keys, inside `compare`, so descending sorts are fully
// descending, matching the List<Resource> apis.
let compare: fn(&Schedule, &Schedule) -> Ordering =
match self.sort_by {
ScheduleSortBy::Name => |a, b| a.name.cmp(&b.name),
ScheduleSortBy::Schedule => |a, b| {
a.schedule
.cmp(&b.schedule)
.then_with(|| a.name.cmp(&b.name))
},
// Order unscheduled (None) last, matching the UI.
ScheduleSortBy::NextRun => |a, b| {
(a.next_scheduled_run.is_none(), a.next_scheduled_run)
.cmp(&(
b.next_scheduled_run.is_none(),
b.next_scheduled_run,
))
.then_with(|| a.name.cmp(&b.name))
},
ScheduleSortBy::Enabled => |a, b| {
a.enabled.cmp(&b.enabled).then_with(|| a.name.cmp(&b.name))
},
};
if self.sort_desc {
schedules.sort_by(|a, b| compare(b, a));
} else {
schedules.sort_by(compare);
}
let limit = list_limit(self.limit);
let skip = limit.saturating_mul(self.page) as usize;
let take = if limit == 0 {
usize::MAX
} else {
limit as usize
};
Ok(schedules.into_iter().skip(skip).take(take).collect())
Ok(
actions
.into_iter()
.chain(procedures)
.filter(|s| !s.schedule.is_empty())
.collect(),
)
}
}
+546 -113
View File
@@ -1,5 +1,5 @@
use std::{
cmp::Ordering,
cmp,
collections::HashMap,
sync::{Arc, OnceLock},
};
@@ -15,31 +15,47 @@ use database::mungos::{
use komodo_client::{
api::read::*,
entities::{
ResourceTarget,
deployment::Deployment,
docker::{
container::{
Container, ContainerListItem, ContainerStateStatusEnum,
},
image::{Image, ImageHistoryResponseItem},
network::Network,
volume::Volume,
},
permission::PermissionLevel,
server::{
Server, ServerActionState, ServerListItem, ServerSortBy,
Server, ServerActionState, ServerListItem, ServerQuery,
ServerState,
},
stats::{MinimalSystemStats, SystemInformation, SystemProcess},
stack::{Stack, StackServiceNames},
stats::{SystemInformation, SystemProcess},
update::Log,
},
};
use mogh_error::AddStatusCode;
use mogh_resolver::Resolve;
use periphery_client::api as periphery;
use periphery_client::api::{
self as periphery,
container::InspectContainer,
docker::{
ImageHistory, InspectImage, InspectNetwork, InspectVolume,
},
};
use reqwest::StatusCode;
use tokio::sync::Mutex;
use crate::{
helpers::{
periphery_client,
query::{get_all_tags, get_cached_server_state},
},
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},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetServersSummary {
async fn resolve(
@@ -48,8 +64,6 @@ impl Resolve<ReadArgs> for GetServersSummary {
) -> mogh_error::Result<GetServersSummaryResponse> {
let servers = resource::list_for_user::<Server>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
@@ -111,89 +125,15 @@ impl Resolve<ReadArgs> for ListServers {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<ServerListItem> = match self
.sort_by
{
ServerSortBy::Name => resource::ListItemSort::Name,
ServerSortBy::Region => {
resource::ListItemSort::DbField("config.region")
}
ServerSortBy::Version => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.version
.cmp(&b.info.version)
.then_with(|| a.name.cmp(&b.name))
}))
}
ServerSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| a.name.cmp(&b.name))
}))
}
ServerSortBy::Cpu => stats_sort(|stats| stats.cpu_perc as f64),
ServerSortBy::Memory => stats_sort(|stats| {
usage_percent(stats.mem_used_gb, stats.mem_total_gb)
}),
ServerSortBy::Disk => stats_sort(|stats| {
usage_percent(stats.disk_used_gb, stats.disk_total_gb)
}),
ServerSortBy::LoadAverage => {
stats_sort(|stats| stats.load_average.one)
}
ServerSortBy::Network => stats_sort(|stats| {
stats.network_ingress_bytes + stats.network_egress_bytes
}),
};
let servers = resource::list_items_for_user::<Server>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|server| {
states.is_empty() || states.contains(&server.info.state)
},
Ok(
resource::list_for_user::<Server>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
.await?;
Ok(servers)
}
}
/// Build an in memory sort on the list item stats,
/// matching the stats displayed on the server stats table.
/// Servers without stats (unreachable / disabled) order last.
fn stats_sort(
metric: fn(&MinimalSystemStats) -> f64,
) -> resource::ListItemSort<ServerListItem> {
resource::ListItemSort::InMemory(Box::new(move |a, b| {
match (a.info.stats.as_ref(), b.info.stats.as_ref()) {
(Some(a_stats), Some(b_stats)) => {
metric(a_stats).total_cmp(&metric(b_stats))
}
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(None, None) => Ordering::Equal,
}
.then_with(|| a.name.cmp(&b.name))
}))
}
fn usage_percent(used: f64, total: f64) -> f64 {
if total > 0.0 {
100.0 * used / total
} else {
0.0
}
}
@@ -207,29 +147,12 @@ impl Resolve<ReadArgs> for ListFullServers {
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user_filtered::<Server, _>(
resource::list_full_for_user::<Server>(
self.query,
limit,
self.page,
user,
PermissionLevel::Read.into(),
&all_tags,
|server| {
let states = states.clone();
async move {
if states.is_empty()
|| states
.contains(&get_cached_server_state(&server.id).await)
{
Some(server)
} else {
None
}
}
},
)
.await?,
)
@@ -416,7 +339,7 @@ impl Resolve<ReadArgs> for GetHistoricalServerStats {
let curr_ts = unix_timestamp_ms() as i64;
let mut curr_ts = curr_ts
- curr_ts % granularity
- granularity * (page as i64).saturating_mul(STATS_PER_PAGE);
- granularity * STATS_PER_PAGE * page as i64;
for _ in 0..STATS_PER_PAGE {
ts_vec.push(curr_ts);
curr_ts -= granularity;
@@ -430,6 +353,7 @@ impl Resolve<ReadArgs> for GetHistoricalServerStats {
},
FindOptions::builder()
.sort(doc! { "ts": -1 })
.skip(page as u64 * STATS_PER_PAGE as u64)
.limit(STATS_PER_PAGE)
.build(),
)
@@ -444,3 +368,512 @@ impl Resolve<ReadArgs> for GetHistoricalServerStats {
Ok(res)
}
}
impl Resolve<ReadArgs> for ListDockerContainers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListDockerContainersResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.containers.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for ListAllDockerContainers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListAllDockerContainersResponse> {
let servers = resource::list_for_user::<Server>(
ServerQuery::builder().names(self.servers.clone()).build(),
user,
PermissionLevel::Read.into(),
&[],
)
.await?;
let mut containers = Vec::<ContainerListItem>::new();
for server in servers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
let Some(docker) = &cache.docker else {
continue;
};
let more = docker
.containers
.iter()
.filter(|container| {
self.containers.is_empty()
|| self.containers.contains(&container.name)
})
.cloned();
containers.extend(more);
}
Ok(containers)
}
}
impl Resolve<ReadArgs> for GetDockerContainersSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetDockerContainersSummaryResponse> {
let servers = resource::list_full_for_user::<Server>(
Default::default(),
user,
PermissionLevel::Read.into(),
&[],
)
.await
.context("failed to get servers from db")?;
let mut res = GetDockerContainersSummaryResponse::default();
for server in servers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
for container in &docker.containers {
res.total += 1;
match container.state {
ContainerStateStatusEnum::Created
| ContainerStateStatusEnum::Paused
| ContainerStateStatusEnum::Exited => res.stopped += 1,
ContainerStateStatusEnum::Running => res.running += 1,
ContainerStateStatusEnum::Empty => res.unknown += 1,
_ => res.unhealthy += 1,
}
}
}
}
Ok(res)
}
}
impl Resolve<ReadArgs> for InspectDockerContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Container> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.inspect(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(InspectContainer {
name: self.container,
})
.await?;
Ok(res)
}
}
const MAX_LOG_LENGTH: u64 = 5000;
impl Resolve<ReadArgs> for GetContainerLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Log> {
let GetContainerLog {
server,
container,
tail,
timestamps,
} = self;
let server = get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read.logs(),
)
.await?;
let res = periphery_client(&server)
.await?
.request(periphery::container::GetContainerLog {
name: container,
tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps,
})
.await
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for SearchContainerLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Log> {
let SearchContainerLog {
server,
container,
terms,
combinator,
invert,
timestamps,
} = self;
let server = get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read.logs(),
)
.await?;
let res = periphery_client(&server)
.await?
.request(periphery::container::GetContainerLogSearch {
name: container,
terms,
combinator,
invert,
timestamps,
})
.await
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for GetResourceMatchingContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetResourceMatchingContainerResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
// first check deployments
if let Ok(deployment) =
resource::get::<Deployment>(&self.container).await
{
return Ok(GetResourceMatchingContainerResponse {
resource: ResourceTarget::Deployment(deployment.id).into(),
});
}
// then check stacks
let stacks = list_resources_for_user::<Stack>(
doc! { "config.server_id": &server.id },
user,
PermissionLevel::Read.into(),
)
.await?;
// check matching stack
for stack in stacks {
for StackServiceNames {
service_name,
container_name,
..
} in stack
.info
.deployed_services
.unwrap_or(stack.info.latest_services)
{
let is_match = match compose_container_match_regex(&container_name)
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
{
Ok(regex) => regex,
Err(e) => {
warn!("{e:#}");
continue;
}
}.is_match(&self.container);
if is_match {
return Ok(GetResourceMatchingContainerResponse {
resource: ResourceTarget::Stack(stack.id).into(),
});
}
}
}
Ok(GetResourceMatchingContainerResponse { resource: None })
}
}
impl Resolve<ReadArgs> for ListDockerNetworks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListDockerNetworksResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.networks.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectDockerNetwork {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Network> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect network: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(InspectNetwork { name: self.network })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListDockerImages {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListDockerImagesResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.images.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectDockerImage {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Image> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!("Cannot inspect image: server is {:?}", cache.state)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(InspectImage { name: self.image })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListDockerImageHistory {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Vec<ImageHistoryResponseItem>> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot get image history: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(ImageHistory { name: self.image })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListDockerVolumes {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListDockerVolumesResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.volumes.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectDockerVolume {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Volume> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!("Cannot inspect volume: server is {:?}", cache.state)
.into(),
);
}
let res = periphery_client(&server)
.await?
.request(InspectVolume { name: self.volume })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListComposeProjects {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListComposeProjectsResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(docker) = &cache.docker {
Ok(docker.projects.clone())
} else {
Ok(Vec::new())
}
}
}
// impl Resolve<ReadArgs> for ListAllTerminals {
// async fn resolve(
// self,
// args: &ReadArgs,
// ) -> Result<Self::Response, Self::Error> {
// // match self.tar
// let mut terminals = resource::list_full_for_user::<Server>(
// self.query, &args.user, &all_tags,
// )
// .await?
// .into_iter()
// .map(|server| async move {
// (
// list_terminals_inner(&server, self.fresh).await,
// (server.id, server.name),
// )
// })
// .collect::<FuturesUnordered<_>>()
// .collect::<Vec<_>>()
// .await
// .into_iter()
// .flat_map(|(terminals, server)| {
// let terminals = terminals.ok()?;
// Some((terminals, server))
// })
// .flat_map(|(terminals, (server_id, server_name))| {
// terminals.into_iter().map(move |info| {
// TerminalInfoWithServer::from_terminal_info(
// &server_id,
// &server_name,
// info,
// )
// })
// })
// .collect::<Vec<_>>();
// terminals.sort_by(|a, b| {
// a.server_name.cmp(&b.server_name).then(a.name.cmp(&b.name))
// });
// Ok(terminals)
// }
// }
+61 -205
View File
@@ -9,10 +9,7 @@ use komodo_client::{
container::Container, service::SwarmService, stack::SwarmStack,
},
permission::PermissionLevel,
stack::{
Stack, StackActionState, StackListItem, StackQuery,
StackService, StackSortBy, StackState,
},
stack::{Stack, StackActionState, StackListItem, StackState},
},
};
use mogh_error::AddStatusCodeError as _;
@@ -25,9 +22,7 @@ use reqwest::StatusCode;
use crate::{
helpers::{
periphery_client,
query::{get_all_tags, get_cached_stack_state},
swarm::swarm_request,
periphery_client, query::get_all_tags, swarm::swarm_request,
},
permission::get_check_permissions,
resource,
@@ -35,7 +30,7 @@ use crate::{
state::{action_states, stack_status_cache},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetStack {
async fn resolve(
@@ -53,128 +48,6 @@ impl Resolve<ReadArgs> for GetStack {
}
}
impl Resolve<ReadArgs> for ListStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Vec<StackListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let only_update_available = self.query.specific.update_available;
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<StackListItem> =
match self.sort_by {
StackSortBy::Name => resource::ListItemSort::Name,
StackSortBy::Source => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.files_on_host
.cmp(&b.info.files_on_host)
.then_with(|| {
a.info.linked_repo_name.cmp(&b.info.linked_repo_name)
})
.then_with(|| a.info.repo.cmp(&b.info.repo))
.then_with(|| a.name.cmp(&b.name))
}))
}
StackSortBy::Host => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
let host_a = if a.info.swarm_id.is_empty() {
&a.info.server_name
} else {
&a.info.swarm_name
};
let host_b = if b.info.swarm_id.is_empty() {
&b.info.server_name
} else {
&b.info.swarm_name
};
host_a.cmp(host_b).then_with(|| a.name.cmp(&b.name))
}))
}
StackSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| {
// Use ! with update available to order 'true' first
(!a.info.update_available())
.cmp(&!b.info.update_available())
})
.then_with(|| a.name.cmp(&b.name))
}))
}
};
let stacks = resource::list_items_for_user::<Stack>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|stack| {
(!only_update_available
|| stack
.info
.services
.iter()
.any(|service| service.update_available))
&& (states.is_empty() || states.contains(&stack.info.state))
},
)
.await?;
Ok(stacks)
}
}
impl Resolve<ReadArgs> for ListFullStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListFullStacksResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let states = self.query.specific.states.clone();
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user_filtered::<Stack, _>(
self.query,
limit,
self.page,
user,
PermissionLevel::Read.into(),
&all_tags,
|stack| {
let states = states.clone();
async move {
if states.is_empty()
|| states
.contains(&get_cached_stack_state(&stack.id).await)
{
Some(stack)
} else {
None
}
}
},
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListStackServices {
async fn resolve(
self,
@@ -199,75 +72,6 @@ impl Resolve<ReadArgs> for ListStackServices {
}
}
impl Resolve<ReadArgs> for ListAllStackServices {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListStackServicesResponse> {
let all_tags = if self.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let stacks = resource::list_for_user::<Stack>(
StackQuery::builder()
.names(self.stacks.clone())
.tags(self.tags)
.build(),
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?;
let mut services = Vec::<StackService>::new();
let mut skipped = 0;
let limit = list_limit(self.limit);
let limit_usize = limit as usize;
// Eg. page 1 skips until after 100 services, page 2 after 200.
let skip = limit.saturating_mul(self.page);
// Match terms case insensitively.
let terms = self
.terms
.iter()
.map(|term| term.to_lowercase())
.collect::<Vec<_>>();
for stack in stacks {
let cache =
stack_status_cache().get_or_insert_default(&stack.id).await;
let more = cache.curr.services
.iter()
.filter(|service| {
// Apply state filter if defined.
(self.state.is_empty() || self.state.contains(&service.state)) &&
// Apply terms filter if defined
(terms.is_empty()
// Match when all terms contained within a name.
|| {
let name = service.service.to_lowercase();
terms.iter().all(|term| name.contains(term))
})
});
for service in more {
if skipped < skip {
skipped += 1;
} else {
// push and maybe early return
services.push(service.clone());
if limit > 0 && services.len() >= limit_usize {
return Ok(services);
}
}
}
}
Ok(services)
}
}
impl Resolve<ReadArgs> for GetStackLog {
async fn resolve(
self,
@@ -535,8 +339,6 @@ impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
};
let stacks = resource::list_full_for_user::<Stack>(
self.query,
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
@@ -571,8 +373,6 @@ impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
};
let stacks = resource::list_full_for_user::<Stack>(
self.query,
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
@@ -595,6 +395,64 @@ impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
}
}
impl Resolve<ReadArgs> for ListStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<Vec<StackListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let only_update_available = self.query.specific.update_available;
let stacks = resource::list_for_user::<Stack>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?;
let stacks = if only_update_available {
stacks
.into_iter()
.filter(|stack| {
stack
.info
.services
.iter()
.any(|service| service.update_available)
})
.collect()
} else {
stacks
};
Ok(stacks)
}
}
impl Resolve<ReadArgs> for ListFullStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListFullStacksResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Stack>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetStackActionState {
async fn resolve(
self,
@@ -623,8 +481,6 @@ impl Resolve<ReadArgs> for GetStacksSummary {
) -> mogh_error::Result<GetStacksSummaryResponse> {
let stacks = resource::list_full_for_user::<Stack>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
+3 -30
View File
@@ -3,9 +3,7 @@ use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel,
swarm::{
Swarm, SwarmActionState, SwarmListItem, SwarmSortBy, SwarmState,
},
swarm::{Swarm, SwarmActionState, SwarmListItem, SwarmState},
},
};
use mogh_resolver::Resolve;
@@ -17,7 +15,7 @@ use crate::{
state::{action_states, server_status_cache, swarm_status_cache},
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetSwarm {
async fn resolve(
@@ -45,32 +43,12 @@ impl Resolve<ReadArgs> for ListSwarms {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<SwarmListItem> =
match self.sort_by {
SwarmSortBy::Name => resource::ListItemSort::Name,
SwarmSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| a.name.cmp(&b.name))
}))
}
};
Ok(
resource::list_items_for_user::<Swarm>(
resource::list_for_user::<Swarm>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|_| true,
)
.await?,
)
@@ -87,12 +65,9 @@ impl Resolve<ReadArgs> for ListFullSwarms {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user::<Swarm>(
self.query,
limit as i64,
self.page.saturating_mul(limit),
user,
PermissionLevel::Read.into(),
&all_tags,
@@ -130,8 +105,6 @@ impl Resolve<ReadArgs> for GetSwarmsSummary {
) -> mogh_error::Result<GetSwarmsSummaryResponse> {
let swarms = resource::list_full_for_user::<Swarm>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
+3 -49
View File
@@ -5,7 +5,6 @@ use komodo_client::{
permission::PermissionLevel,
sync::{
ResourceSync, ResourceSyncActionState, ResourceSyncListItem,
ResourceSyncSortBy,
},
},
};
@@ -16,7 +15,7 @@ use crate::{
resource, state::action_states,
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetResourceSync {
async fn resolve(
@@ -44,52 +43,12 @@ impl Resolve<ReadArgs> for ListResourceSyncs {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
let sort_by: resource::ListItemSort<ResourceSyncListItem> =
match self.sort_by {
ResourceSyncSortBy::Name => resource::ListItemSort::Name,
ResourceSyncSortBy::Source => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.files_on_host
.cmp(&b.info.files_on_host)
.then_with(|| {
a.info.linked_repo_name.cmp(&b.info.linked_repo_name)
})
.then_with(|| a.info.repo.cmp(&b.info.repo))
.then_with(|| a.name.cmp(&b.name))
}))
}
ResourceSyncSortBy::Branch => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.branch
.cmp(&b.info.branch)
.then_with(|| a.name.cmp(&b.name))
}))
}
ResourceSyncSortBy::State => {
resource::ListItemSort::InMemory(Box::new(|a, b| {
a.info
.state
.cmp(&b.info.state)
.then_with(|| a.name.cmp(&b.name))
}))
}
};
Ok(
resource::list_items_for_user::<ResourceSync>(
resource::list_for_user::<ResourceSync>(
self.query,
resource::ListItemsQueryOptions {
limit,
page: self.page,
sort_desc: self.sort_desc,
sort_by,
},
user,
PermissionLevel::Read.into(),
&all_tags,
|_| true,
)
.await?,
)
@@ -106,12 +65,9 @@ impl Resolve<ReadArgs> for ListFullResourceSyncs {
} else {
get_all_tags(None).await?
};
let limit = list_limit(self.limit);
Ok(
resource::list_full_for_user::<ResourceSync>(
self.query,
limit as i64,
self.page.saturating_mul(limit),
user,
PermissionLevel::Read.into(),
&all_tags,
@@ -150,8 +106,6 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
let resource_syncs =
resource::list_full_for_user::<ResourceSync>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
@@ -166,7 +120,7 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
for resource_sync in resource_syncs {
res.total += 1;
if !(resource_sync.info.pending_deploys.is_empty()
if !(resource_sync.info.pending_deploy.to_deploy == 0
&& resource_sync.info.resource_updates.is_empty()
&& resource_sync.info.variable_updates.is_empty()
&& resource_sync.info.user_group_updates.is_empty())
+56 -118
View File
@@ -1,5 +1,3 @@
use std::cmp::Ordering;
use anyhow::Context as _;
use futures_util::{
FutureExt, StreamExt as _, stream::FuturesUnordered,
@@ -11,7 +9,7 @@ use komodo_client::{
permission::PermissionLevel,
server::Server,
stack::Stack,
terminal::{Terminal, TerminalSortBy, TerminalTarget},
terminal::{Terminal, TerminalTarget},
user::User,
},
};
@@ -24,7 +22,7 @@ use crate::{
resource,
};
use super::{ReadArgs, list_limit};
use super::ReadArgs;
//
@@ -33,111 +31,57 @@ impl Resolve<ReadArgs> for ListTerminals {
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListTerminalsResponse> {
let mut terminals = match self.target {
None => {
list_all_terminals_for_user(user, self.use_names).await?
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
}
Some(target) => 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?
}
},
};
// The terminals come from Periphery agents rather than the db,
// so the terms filter / sort / pagination are applied in memory.
if !self.terms.is_empty() {
let terms = self
.terms
.iter()
.map(|term| term.to_lowercase())
.collect::<Vec<_>>();
terminals.retain(|terminal| {
let name = terminal.name.to_lowercase();
terms.iter().all(|term| name.contains(term))
});
}
// All comparators fall back to name based sorting for equal
// sort keys, inside `compare`, so descending sorts are fully
// descending, matching the List<Resource> apis.
let compare: fn(&Terminal, &Terminal) -> Ordering =
match self.sort_by {
TerminalSortBy::Name => |a, b| a.name.cmp(&b.name),
TerminalSortBy::Target => |a, b| {
a.target.cmp(&b.target).then_with(|| a.name.cmp(&b.name))
},
TerminalSortBy::Command => |a, b| {
a.command.cmp(&b.command).then_with(|| a.name.cmp(&b.name))
},
TerminalSortBy::Size => |a, b| {
a.stored_size_kb
.total_cmp(&b.stored_size_kb)
.then_with(|| a.name.cmp(&b.name))
},
TerminalSortBy::Created => |a, b| {
a.created_at
.cmp(&b.created_at)
.then_with(|| a.name.cmp(&b.name))
},
};
if self.sort_desc {
terminals.sort_by(|a, b| compare(b, a));
} else {
terminals.sort_by(compare);
}
let limit = list_limit(self.limit);
let skip = limit.saturating_mul(self.page) as usize;
let take = if limit == 0 {
usize::MAX
} else {
limit as usize
};
Ok(terminals.into_iter().skip(skip).take(take).collect())
}
}
@@ -148,8 +92,6 @@ async fn list_all_terminals_for_user(
let (mut servers, stacks, deployments) = tokio::try_join!(
resource::list_full_for_user::<Server>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.terminal(),
&[]
@@ -161,16 +103,12 @@ async fn list_all_terminals_for_user(
.collect::<Vec<_>>())),
resource::list_full_for_user::<Stack>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.terminal(),
&[]
),
resource::list_full_for_user::<Deployment>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.terminal(),
&[]
@@ -201,7 +139,7 @@ async fn list_all_terminals_for_user(
}
}
let terminals = servers
let mut terminals = servers
.into_iter()
.map(|(server, server_permission)| async move {
(
@@ -230,7 +168,6 @@ async fn list_all_terminals_for_user(
server_id.clone()
}),
};
terminal.target_name = Some(server_name.clone());
terminal
}),
TerminalTarget::Container { container, .. } => {
@@ -243,7 +180,6 @@ async fn list_all_terminals_for_user(
},
container,
};
terminal.target_name = Some(server_name.clone());
terminal
})
}
@@ -257,7 +193,6 @@ async fn list_all_terminals_for_user(
},
service,
};
terminal.target_name = Some(s.name.clone());
terminal
})
}
@@ -271,7 +206,6 @@ async fn list_all_terminals_for_user(
d.id.clone()
},
};
terminal.target_name = Some(d.name.clone());
terminal
},
)
@@ -286,6 +220,10 @@ async fn list_all_terminals_for_user(
.flatten()
.collect::<Vec<_>>();
terminals.sort_by(|a, b| {
a.target.cmp(&b.target).then(a.name.cmp(&b.name))
});
Ok(terminals)
}
+340 -123
View File
@@ -1,8 +1,5 @@
use anyhow::Context;
use database::{
bson::doc,
mungos::{find::find_collect, mongodb::options::FindOptions},
};
use database::mungos::find::find_collect;
use komodo_client::{
api::read::{
ExportAllResourcesToToml, ExportAllResourcesToTomlResponse,
@@ -28,7 +25,6 @@ use crate::{
resource,
state::db_client,
sync::{
replace_ids::ReplaceIds,
toml::{ToToml, convert_resource},
user_groups::{convert_user_groups, user_group_to_toml},
variables::variable_to_toml,
@@ -42,47 +38,122 @@ async fn get_all_targets(
user: &User,
) -> anyhow::Result<Vec<ResourceTarget>> {
let mut targets = Vec::<ResourceTarget>::new();
let all_tags = if tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
macro_rules! extend_targets {
($($Type:ident),* $(,)?) => {
$(
targets.extend(
resource::list_full_for_user::<$Type>(
ResourceQuery::builder().tags(tags).build(),
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::$Type(resource.id)),
);
)*
};
}
extend_targets!(
Alerter,
Builder,
Server,
Swarm,
Stack,
Deployment,
Build,
Repo,
Procedure,
Action,
ResourceSync,
targets.extend(
resource::list_full_for_user::<Alerter>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Alerter(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Builder>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Builder(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Server>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Server(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Stack>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Stack(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Deployment>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Deployment(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Build>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Build(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Repo>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Repo(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Procedure>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Procedure(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Action>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Action(resource.id)),
);
targets.extend(
resource::list_full_for_user::<ResourceSync>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
// These will already be filtered by [ExportResourcesToToml]
.map(|resource| ResourceTarget::ResourceSync(resource.id)),
);
Ok(targets)
}
@@ -116,7 +187,6 @@ impl Resolve<ReadArgs> for ExportAllResourcesToToml {
targets,
user_groups,
include_variables: self.include_variables,
existing: self.existing,
}
.resolve(args)
.await
@@ -132,64 +202,131 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
targets,
user_groups,
include_variables,
existing,
} = self;
let mut res = ResourcesToml::default();
let id_to_tags = get_id_to_tags(None).await?;
let ReadArgs { user } = args;
macro_rules! convert_target {
($id:expr, $Type:ident, $field:ident) => {{
let mut resource = get_check_permissions::<$Type>(
&$id,
user,
PermissionLevel::Read.into(),
)
.await?;
$Type::replace_ids(&mut resource.config);
let (deploy, after) = existing
.as_ref()
.and_then(|e| {
e.$field.iter().find(|r| r.name == resource.name)
})
.map(|r| (r.deploy, r.after.clone()))
.unwrap_or_default();
res.$field.push(convert_resource::<$Type>(
resource,
deploy,
after,
&id_to_tags,
));
}};
}
for target in targets {
match target {
ResourceTarget::Server(id) => {
convert_target!(id, Server, servers)
}
ResourceTarget::Swarm(id) => {
convert_target!(id, Swarm, swarms)
let mut swarm = get_check_permissions::<Swarm>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Swarm::replace_ids(&mut swarm);
res.swarms.push(convert_resource::<Swarm>(
swarm,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Server(id) => {
let mut server = get_check_permissions::<Server>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Server::replace_ids(&mut server);
res.servers.push(convert_resource::<Server>(
server,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Stack(id) => {
convert_target!(id, Stack, stacks)
let mut stack = get_check_permissions::<Stack>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Stack::replace_ids(&mut stack);
res.stacks.push(convert_resource::<Stack>(
stack,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Deployment(id) => {
convert_target!(id, Deployment, deployments)
let mut deployment = get_check_permissions::<Deployment>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Deployment::replace_ids(&mut deployment);
res.deployments.push(convert_resource::<Deployment>(
deployment,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Build(id) => {
convert_target!(id, Build, builds)
let mut build = get_check_permissions::<Build>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Build::replace_ids(&mut build);
res.builds.push(convert_resource::<Build>(
build,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Repo(id) => {
let mut repo = get_check_permissions::<Repo>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Repo::replace_ids(&mut repo);
res.repos.push(convert_resource::<Repo>(
repo,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Repo(id) => convert_target!(id, Repo, repos),
ResourceTarget::Procedure(id) => {
convert_target!(id, Procedure, procedures)
let mut procedure = get_check_permissions::<Procedure>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Procedure::replace_ids(&mut procedure);
res.procedures.push(convert_resource::<Procedure>(
procedure,
false,
vec![],
&id_to_tags,
));
}
ResourceTarget::Action(id) => {
convert_target!(id, Action, actions)
}
ResourceTarget::Builder(id) => {
convert_target!(id, Builder, builders)
}
ResourceTarget::Alerter(id) => {
convert_target!(id, Alerter, alerters)
let mut action = get_check_permissions::<Action>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Action::replace_ids(&mut action);
res.actions.push(convert_resource::<Action>(
action,
false,
vec![],
&id_to_tags,
));
}
ResourceTarget::ResourceSync(id) => {
let mut sync = get_check_permissions::<ResourceSync>(
@@ -203,7 +340,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|| !sync.config.repo.is_empty()
|| !sync.config.linked_repo.is_empty())
{
ResourceSync::replace_ids(&mut sync.config);
ResourceSync::replace_ids(&mut sync);
res.resource_syncs.push(convert_resource::<ResourceSync>(
sync,
false,
@@ -212,6 +349,36 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
))
}
}
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,
};
}
@@ -221,21 +388,18 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
.context("failed to add user groups")?;
if include_variables {
res.variables = find_collect(
&db_client().variables,
None,
FindOptions::builder().sort(doc! { "name": 1 }).build(),
)
.await
.context("failed to get variables from db")?
.into_iter()
.map(|mut variable| {
if !user.admin && variable.is_secret {
variable.value = "#".repeat(variable.value.len())
}
variable
})
.collect();
res.variables =
find_collect(&db_client().variables, None, None)
.await
.context("failed to get variables from db")?
.into_iter()
.map(|mut variable| {
if !user.admin && variable.is_secret {
variable.value = "#".repeat(variable.value.len())
}
variable
})
.collect();
}
let toml = serialize_resources_toml(res)
@@ -270,32 +434,85 @@ fn serialize_resources_toml(
) -> anyhow::Result<String> {
let mut toml = String::new();
macro_rules! serialize_resources {
($(($Type:ident, $field:ident, $header:literal)),* $(,)?) => {
$(
for resource in resources.$field {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str(concat!("[[",$header,"]]\n"));
$Type::push_to_toml_string(resource, &mut toml)?;
}
)*
};
for server in resources.servers {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[server]]\n");
Server::push_to_toml_string(server, &mut toml)?;
}
for stack in resources.stacks {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[stack]]\n");
Stack::push_to_toml_string(stack, &mut toml)?;
}
for deployment in resources.deployments {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[deployment]]\n");
Deployment::push_to_toml_string(deployment, &mut toml)?;
}
for build in resources.builds {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[build]]\n");
Build::push_to_toml_string(build, &mut toml)?;
}
for repo in resources.repos {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[repo]]\n");
Repo::push_to_toml_string(repo, &mut toml)?;
}
for procedure in resources.procedures {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[procedure]]\n");
Procedure::push_to_toml_string(procedure, &mut toml)?;
}
for action in resources.actions {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[action]]\n");
Action::push_to_toml_string(action, &mut toml)?;
}
for alerter in resources.alerters {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[alerter]]\n");
Alerter::push_to_toml_string(alerter, &mut toml)?;
}
for builder in resources.builders {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[builder]]\n");
Builder::push_to_toml_string(builder, &mut toml)?;
}
for resource_sync in resources.resource_syncs {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[resource_sync]]\n");
ResourceSync::push_to_toml_string(resource_sync, &mut toml)?;
}
serialize_resources!(
(Server, servers, "server"),
(Swarm, swarms, "swarm"),
(Stack, stacks, "stack"),
(Deployment, deployments, "deployment"),
(Build, builds, "build"),
(Repo, repos, "repo"),
(Procedure, procedures, "procedure"),
(Action, actions, "action"),
(Alerter, alerters, "alerter"),
(Builder, builders, "builder"),
(ResourceSync, resource_syncs, "resource_sync"),
);
for variable in &resources.variables {
if !toml.is_empty() {
+1 -3
View File
@@ -47,9 +47,7 @@ impl Resolve<ReadArgs> for ListUpdates {
query,
FindOptions::builder()
.sort(doc! { "start_ts": -1 })
.skip(
(self.page as u64).saturating_mul(UPDATES_PER_PAGE as u64),
)
.skip(self.page as u64 * UPDATES_PER_PAGE as u64)
.limit(UPDATES_PER_PAGE)
.build(),
)
+1 -12
View File
@@ -71,20 +71,9 @@ impl Resolve<ReadArgs> for ListUsers {
anyhow!("this route is only accessable by admins").into(),
);
}
let filter = match self.service_users {
komodo_client::api::read::ServiceUserQueryBehavior::Include => {
None
}
komodo_client::api::read::ServiceUserQueryBehavior::Exclude => {
Some(doc! { "config.type": { "$ne": "Service" } })
}
komodo_client::api::read::ServiceUserQueryBehavior::Only => {
Some(doc! { "config.type": "Service" })
}
};
let mut users = find_collect(
&db_client().users,
filter,
None,
FindOptions::builder().sort(doc! { "username": 1 }).build(),
)
.await
+49 -12
View File
@@ -1,8 +1,10 @@
use std::path::PathBuf;
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::*,
@@ -13,6 +15,7 @@ use komodo_client::{
builder::{Builder, BuilderConfig},
permission::PermissionLevel,
repo::Repo,
server::ServerState,
update::Update,
},
};
@@ -22,11 +25,12 @@ use periphery_client::api::build::{
};
use tokio::fs;
use crate::helpers::builder::connect_builder_periphery;
use crate::{
config::core_config,
connection::PeripheryConnectionArgs,
helpers::{
git_token,
git_token, periphery_client,
query::get_server_with_state,
update::{add_update, make_update},
},
periphery::PeripheryClient,
@@ -472,15 +476,48 @@ async fn get_on_host_periphery(
.await
.context("Failed to get builder")?;
if let BuilderConfig::Aws(_) = builder.config {
return Err(anyhow!(
"Files on host doesn't work with AWS builder"
));
match builder.config {
BuilderConfig::Aws(_) => {
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(
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() {
return Err(anyhow!(
"Builder is type server, but has no server attached"
));
}
let (server, state) =
get_server_with_state(&config.server_id).await?;
if state != ServerState::Ok {
return Err(anyhow!(
"Builder server is disabled or not reachable"
));
};
periphery_client(&server).await
}
}
connect_builder_periphery(build.name.clone(), None, builder, None)
.await
.map(|(periphery, _)| periphery)
}
/// The successful case will be included as Some(remote_contents).
+65 -217
View File
@@ -2,11 +2,10 @@ 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::{execute::Deploy, write::*},
entities::{
Operation, ResourceTarget, SwarmOrServer,
NoData, Operation, ResourceTarget, SwarmOrServer,
alert::{Alert, AlertData, SeverityLevel},
deployment::{
Deployment, DeploymentImage, DeploymentInfo, DeploymentState,
@@ -15,7 +14,6 @@ use komodo_client::{
docker::container::RestartPolicyNameEnum,
komodo_timestamp, optional_string,
permission::PermissionLevel,
resource::ResourceQuery,
server::{Server, ServerState},
to_container_compatible_name,
update::Update,
@@ -28,20 +26,15 @@ use periphery_client::api::{self, container::InspectContainer};
use crate::{
alert::send_alerts,
api::execute::{self, ExecuteRequest, ExecutionResult},
api::execute::{self, ExecuteRequest},
helpers::{
periphery_client,
query::{
get_all_tags, get_deployment_state, get_swarm_or_server,
},
query::get_deployment_state,
registry_token,
update::{add_update, make_update, poll_update_until_complete},
update::{add_update, make_update},
},
permission::get_check_permissions,
resource::{
self, list_full_for_user_using_pattern,
setup_deployment_execution,
},
resource::{self, setup_deployment_execution},
state::{
action_states, db_client, deployment_status_cache,
image_digest_cache, server_status_cache,
@@ -303,24 +296,10 @@ impl Resolve<WriteArgs> for RenameDeployment {
let name = to_container_compatible_name(&self.name);
let state = get_deployment_state(&deployment.id).await?;
let container_state =
get_deployment_state(&deployment.id).await?;
// When no custom name is configured, the container / service
// follows the Deployment name, and must be kept matched:
// - Server mode: rename the container to the new name.
// - Swarm mode: services cannot be renamed, so the current
// service name is pinned in info.deployed_name, and stays
// in use until the next deploy recreates the service.
// A configured custom name is unaffected by Deployment rename.
let follows_name =
deployment.config.custom_name.trim().is_empty();
let deployed = state != DeploymentState::NotDeployed;
let rename_container = follows_name
&& deployed
&& !deployment.config.server_id.is_empty();
if rename_container && state == DeploymentState::Unknown {
if container_state == DeploymentState::Unknown {
return Err(
anyhow!(
"Cannot rename Deployment when container status is unknown"
@@ -329,51 +308,27 @@ impl Resolve<WriteArgs> for RenameDeployment {
);
}
// Includes the Unknown state, in case a service
// is still running under the current name.
let pin_deployed_name = follows_name
&& deployed
&& !deployment.config.swarm_id.is_empty()
&& deployment.info.deployed_name.is_empty();
let mut update =
make_update(&deployment, Operation::RenameDeployment, user);
let mut set =
doc! { "name": &name, "updated_at": komodo_timestamp() };
if rename_container {
// Container is renamed to the new name below.
set.insert("info.deployed_name", name.as_str());
} else if pin_deployed_name {
set.insert("info.deployed_name", deployment.deployed_name());
}
update_one_by_id(
&db_client().deployments,
&deployment.id,
database::mungos::update::Update::Set(set),
database::mungos::update::Update::Set(
doc! { "name": &name, "updated_at": komodo_timestamp() },
),
None,
)
.await
.context("Failed to update Deployment name on db")?;
if pin_deployed_name {
update.push_simple_log(
"Pin Service Name",
format!(
"Swarm services cannot be renamed, so the current service name '{}' stays in use until the next deploy recreates the service under the new name.",
deployment.deployed_name()
),
);
}
if rename_container {
if container_state != DeploymentState::NotDeployed {
let server =
resource::get::<Server>(&deployment.config.server_id).await?;
let log = periphery_client(&server)
.await?
.request(api::container::RenameContainer {
curr_name: deployment.deployed_name().to_string(),
curr_name: deployment.name.clone(),
new_name: name.clone(),
})
.await
@@ -404,14 +359,12 @@ impl Resolve<WriteArgs> for CheckDeploymentForUpdate {
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> {
) -> mogh_error::Result<NoData> {
// 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(
@@ -429,8 +382,9 @@ impl Resolve<WriteArgs> for CheckDeploymentForUpdate {
self.skip_auto_update,
self.wait_for_auto_update,
)
.await
.map_err(Into::into)
.await?;
Ok(NoData {})
}
}
@@ -455,8 +409,6 @@ fn deployment_alert_sent_cache() -> &'static SetCache<String> {
skip_all,
fields(
deployment = deployment.id,
skip_auto_update,
wait_for_auto_update,
)
)]
pub async fn check_deployment_for_update_inner(
@@ -465,19 +417,16 @@ pub async fn check_deployment_for_update_inner(
skip_auto_update: bool,
// Otherwise spawns task to run in background
wait_for_auto_update: bool,
) -> anyhow::Result<CheckDeploymentForUpdateResponse> {
) -> anyhow::Result<bool> {
let alert_cache = deployment_alert_sent_cache();
let (image, account, token) = match &deployment.config.image {
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,
});
return Ok(false)
}
let domain = extract_registry_domain(image)?;
let domain = extract_registry_domain(&image)?;
let account =
optional_string(&deployment.config.image_registry_account);
let token = if let Some(account) = &account {
@@ -491,70 +440,55 @@ pub async fn check_deployment_for_update_inner(
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,
});
return Ok(false);
}
};
let latest_digest = image_digest_cache()
.get(swarm_or_server, image, account, token)
.get(&swarm_or_server, &image, account, token)
.await?;
resource::update_info::<Deployment>(
&deployment.id,
&DeploymentInfo {
latest_image_digest: latest_digest.clone(),
deployed_name: deployment.info.deployed_name.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()))
let Some((state, Some(current_digest))) = deployment_status_cache()
.get(&deployment.id)
.await
.map(|s| (s.curr.state, s.curr.image_digest.clone()))
else {
alert_cache.remove(&deployment.id).await;
return Ok(CheckDeploymentForUpdateResponse {
deployment: deployment.id,
update_available: false,
});
return Ok(false);
};
// If not running or latest digest matches current, early return
if !matches!(state, DeploymentState::Running)
|| !latest_digest.update_available(&current_digests)
|| !latest_digest.update_available(&current_digest)
{
alert_cache.remove(&deployment.id).await;
return Ok(CheckDeploymentForUpdateResponse {
deployment: deployment.id,
update_available: false,
});
return Ok(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();
// 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 run = async move {
match execute::inner_handler(
ExecuteRequest::Deploy(Deploy {
deployment: name.clone(),
deployment: deployment.name.clone(),
stop_signal: None,
stop_time: None,
}),
@@ -562,45 +496,38 @@ pub async fn check_deployment_for_update_inner(
)
.await
{
Ok(res) => {
let ExecutionResult::Single(update) = res else {
unreachable!()
Ok(_) => {
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::DeploymentAutoUpdated {
id: deployment.id.clone(),
name: deployment.name.clone(),
swarm_id,
swarm_name,
server_id,
server_name,
image: image.clone(),
},
};
let Ok(update) =
poll_update_until_complete(&update.id).await
else {
return;
};
if update.success {
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;
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:#}",)
warn!(
"Failed to auto update Deployment {} | {e:#}",
deployment.name
)
}
}
};
@@ -613,10 +540,7 @@ pub async fn check_deployment_for_update_inner(
} else {
// Avoid spamming alerts
if alert_cache.contains(&deployment.id).await {
return Ok(CheckDeploymentForUpdateResponse {
deployment: deployment.id,
update_available: true,
});
return Ok(true);
}
alert_cache.insert(deployment.id.clone()).await;
let ts = komodo_timestamp();
@@ -648,81 +572,5 @@ pub async fn check_deployment_for_update_inner(
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,
tags = self.tags.join(","),
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 all_tags = if self.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let deployments = list_full_for_user_using_pattern::<Deployment>(
&self.pattern,
ResourceQuery {
tags: self.tags,
..Default::default()
},
None,
None,
user,
PermissionLevel::Execute.into(),
&all_tags,
)
.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)
}
Ok(!deployment.config.auto_update)
}
+11 -32
View File
@@ -54,7 +54,7 @@ pub struct WriteArgs {
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants,
)]
#[strum_discriminants(name(WriteRequestMethod), derive(Display))]
#[strum_discriminants(name(WriteRequestVariant), derive(Display))]
#[args(WriteArgs)]
#[response(Response)]
#[error(mogh_error::Error)]
@@ -95,7 +95,6 @@ pub enum WriteRequest {
WriteStackFileContents(WriteStackFileContents),
RefreshStackCache(RefreshStackCache),
CheckStackForUpdate(CheckStackForUpdate),
BatchCheckStackForUpdate(BatchCheckStackForUpdate),
// ==== DEPLOYMENT ====
CreateDeployment(CreateDeployment),
@@ -105,7 +104,6 @@ pub enum WriteRequest {
UpdateDeployment(UpdateDeployment),
RenameDeployment(RenameDeployment),
CheckDeploymentForUpdate(CheckDeploymentForUpdate),
BatchCheckDeploymentForUpdate(BatchCheckDeploymentForUpdate),
// ==== BUILD ====
CreateBuild(CreateBuild),
@@ -211,12 +209,9 @@ pub enum WriteRequest {
CreateGitProviderAccount(CreateGitProviderAccount),
UpdateGitProviderAccount(UpdateGitProviderAccount),
DeleteGitProviderAccount(DeleteGitProviderAccount),
#[serde(alias = "CreateDockerRegistryAccount")]
CreateImageRegistryAccount(CreateImageRegistryAccount),
#[serde(alias = "UpdateDockerRegistryAccount")]
UpdateImageRegistryAccount(UpdateImageRegistryAccount),
#[serde(alias = "DeleteDockerRegistryAccount")]
DeleteImageRegistryAccount(DeleteImageRegistryAccount),
CreateDockerRegistryAccount(CreateDockerRegistryAccount),
UpdateDockerRegistryAccount(UpdateDockerRegistryAccount),
DeleteDockerRegistryAccount(DeleteDockerRegistryAccount),
// ==== ALERT ====
CloseAlert(CloseAlert),
@@ -258,35 +253,19 @@ async fn task(
request: WriteRequest,
user: User,
) -> mogh_error::Result<axum::response::Response> {
let task_id = Uuid::new_v4();
let method: WriteRequestMethod = (&request).into();
let req_id = Uuid::new_v4();
let variant: WriteRequestVariant = (&request).into();
let user_id = user.id.clone();
let username = user.username.clone();
if !matches!(
request,
WriteRequest::SetLastSeenUpdate(_)
| WriteRequest::PushRecentlyViewed(_)
) {
info!(
task_id = task_id.to_string(),
method = method.to_string(),
user_id,
username,
"WRITE REQUEST",
);
}
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!(
task_id = task_id.to_string(),
method = method.to_string(),
user_id,
username,
"WRITE REQUEST | ERROR: {:#}",
"WRITE REQUEST {req_id} | METHOD: {variant} | ERROR: {:#}",
e.error
);
}
+5 -38
View File
@@ -15,10 +15,7 @@ use mogh_pki::EncodedKeyPair;
use mogh_resolver::Resolve;
use reqwest::StatusCode;
use crate::{
api::write::WriteArgs, helpers::query::get_all_tags,
state::db_client,
};
use crate::{api::write::WriteArgs, state::db_client};
//
@@ -56,21 +53,6 @@ impl Resolve<WriteArgs> for CreateOnboardingKey {
)?
.public
.into_inner();
let tags = if self.tags.is_empty() {
self.tags
} else {
// fix_tags by ensuring existence, and force replace with tag name.
let all_tags = get_all_tags(None).await?;
self
.tags
.into_iter()
.filter_map(|tag| {
let tag =
all_tags.iter().find(|t| t.id == tag || t.name == tag)?;
Some(tag.name.clone())
})
.collect()
};
let onboarding_key = OnboardingKey {
public_key,
name: self.name,
@@ -78,8 +60,8 @@ impl Resolve<WriteArgs> for CreateOnboardingKey {
onboarded: Default::default(),
created_at: komodo_timestamp(),
expires: self.expires,
tags,
privileged: self.privileged,
tags: self.tags,
fix_existing_servers: self.fix_existing_servers,
copy_server: self.copy_server,
create_builder: self.create_builder,
};
@@ -157,26 +139,11 @@ impl Resolve<WriteArgs> for UpdateOnboardingKey {
}
if let Some(tags) = self.tags {
let tags = if tags.is_empty() {
tags
} else {
// fix_tags by ensuring existence, and force replace with tag name.
let all_tags = get_all_tags(None).await?;
tags
.into_iter()
.filter_map(|tag| {
let tag = all_tags
.iter()
.find(|t| t.id == tag || t.name == tag)?;
Some(tag.name.clone())
})
.collect()
};
update.insert("tags", tags);
}
if let Some(privileged) = self.privileged {
update.insert("privileged", privileged);
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 {
+2 -2
View File
@@ -347,9 +347,9 @@ async fn extract_resource_target_with_validation(
.find_one(filter)
.await
.context("Failed to query db for swarms")?
.context("No matching swarm found")?
.context("No matching server found")?
.id;
Ok((ResourceTargetVariant::Swarm, id))
Ok((ResourceTargetVariant::Server, id))
}
ResourceTarget::Server(ident) => {
let filter = match ObjectId::from_str(ident) {
+8 -8
View File
@@ -7,7 +7,7 @@ use komodo_client::{
api::write::*,
entities::{
Operation, ResourceTarget,
provider::{GitProviderAccount, ImageRegistryAccount},
provider::{DockerRegistryAccount, GitProviderAccount},
},
};
use mogh_error::AddStatusCodeError;
@@ -248,7 +248,7 @@ impl Resolve<WriteArgs> for DeleteGitProviderAccount {
}
}
impl Resolve<WriteArgs> for CreateImageRegistryAccount {
impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
#[instrument(
"CreateDockerRegistryAccount",
skip_all,
@@ -261,7 +261,7 @@ impl Resolve<WriteArgs> for CreateImageRegistryAccount {
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<CreateImageRegistryAccountResponse> {
) -> mogh_error::Result<CreateDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!(
@@ -271,7 +271,7 @@ impl Resolve<WriteArgs> for CreateImageRegistryAccount {
);
}
let mut account: ImageRegistryAccount = self.account.into();
let mut account: DockerRegistryAccount = self.account.into();
if account.domain.is_empty() {
return Err(
@@ -326,7 +326,7 @@ impl Resolve<WriteArgs> for CreateImageRegistryAccount {
}
}
impl Resolve<WriteArgs> for UpdateImageRegistryAccount {
impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
#[instrument(
"UpdateDockerRegistryAccount",
skip_all,
@@ -340,7 +340,7 @@ impl Resolve<WriteArgs> for UpdateImageRegistryAccount {
async fn resolve(
mut self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<UpdateImageRegistryAccountResponse> {
) -> mogh_error::Result<UpdateDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can update docker registry accounts")
@@ -423,7 +423,7 @@ impl Resolve<WriteArgs> for UpdateImageRegistryAccount {
}
}
impl Resolve<WriteArgs> for DeleteImageRegistryAccount {
impl Resolve<WriteArgs> for DeleteDockerRegistryAccount {
#[instrument(
"DeleteDockerRegistryAccount",
skip_all,
@@ -435,7 +435,7 @@ impl Resolve<WriteArgs> for DeleteImageRegistryAccount {
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<DeleteImageRegistryAccountResponse> {
) -> mogh_error::Result<DeleteDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can delete docker registry accounts")
+55 -213
View File
@@ -9,7 +9,6 @@ use database::{
},
};
use formatting::format_serror;
use futures_util::{StreamExt as _, stream::FuturesOrdered};
use komodo_client::{
api::{execute::DeployStack, write::*},
entities::{
@@ -19,11 +18,7 @@ use komodo_client::{
all_logs_success, komodo_timestamp,
permission::PermissionLevel,
repo::Repo,
resource::ResourceQuery,
stack::{
Stack, StackInfo, StackServiceNames, StackServiceWithUpdate,
StackState,
},
stack::{Stack, StackInfo, StackServiceWithUpdate, StackState},
update::Update,
user::{auto_redeploy_user, stack_user, system_user},
},
@@ -37,15 +32,15 @@ use periphery_client::api::compose::{
use crate::{
alert::send_alerts,
api::execute::{self, ExecuteRequest, ExecutionResult},
api::execute::{self, ExecuteRequest},
config::core_config,
helpers::{
query::{get_all_tags, get_swarm_or_server},
query::get_swarm_or_server,
stack_git_token, swarm_or_server_request,
update::{add_update, make_update, poll_update_until_complete},
update::{add_update, make_update},
},
permission::get_check_permissions,
resource::{self, list_full_for_user_using_pattern},
resource,
stack::{
remote::{RemoteComposeContents, get_repo_compose_contents},
services::{
@@ -559,25 +554,15 @@ impl Resolve<WriteArgs> for RefreshStackCache {
&mut services,
) {
warn!(
stack = stack.id,
stack_name = stack.name,
"Failed to extract stack services | {e:#}",
"failed to extract stack services, things won't works correctly. stack: {} | {e:#}",
stack.name
);
}
}
(services, Some(contents), Some(errors), None, None)
} else {
// This path is reached if the swarm / server is not available.
// It carries over the last successful poll.
(
stack.info.latest_services,
stack.info.remote_contents,
stack.info.remote_errors,
// Files on host can set hash / message back to None.
None,
None,
)
(vec![], None, None, None, None)
}
} else if !repo_empty {
// ================
@@ -691,7 +676,7 @@ impl Resolve<WriteArgs> for CheckStackForUpdate {
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<Self::Response> {
) -> mogh_error::Result<NoData> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// stack should be able to do this.
let (stack, swarm_or_server) = setup_stack_execution(
@@ -710,8 +695,9 @@ impl Resolve<WriteArgs> for CheckStackForUpdate {
self.wait_for_auto_update,
self.skip_cache_refresh,
)
.await
.map_err(Into::into)
.await?;
Ok(NoData {})
}
}
@@ -745,7 +731,7 @@ pub async fn check_stack_for_update_inner(
// Otherwise spawns task to run in background
wait_for_auto_update: bool,
skip_cache_refresh: bool,
) -> anyhow::Result<CheckStackForUpdateResponse> {
) -> anyhow::Result<Vec<StackServiceWithUpdate>> {
if !skip_cache_refresh {
(RefreshStackCache {
stack: stack.clone(),
@@ -764,34 +750,17 @@ pub async fn check_stack_for_update_inner(
let cache = image_digest_cache();
for service in &mut stack.info.latest_services {
// Prefer the image coming from deployed services
// so it will be after any interpolation.
let image = stack
.info
.deployed_services
.as_ref()
.and_then(|services| {
services.iter().find_map(|deployed| {
(deployed.service_name == service.service_name)
.then_some(&deployed.image)
})
})
.unwrap_or(&service.image);
if image.is_empty() ||
if service.image.is_empty() ||
// Images with a hardcoded digest can't have update.
image.contains('@') ||
// Services explicitly excluded from global auto-update checks.
// Manual checks should still evaluate all services.
(wait_for_auto_update &&
stack.config.auto_update_skip_services.contains(
&service.service_name,
))
service.image.contains('@')
{
service.image_digest = None;
continue;
}
match cache.get(swarm_or_server, image, None, None).await {
match cache
.get(&swarm_or_server, &service.image, None, None)
.await
{
Ok(digest) => service.image_digest = Some(digest),
Err(e) => {
warn!(
@@ -821,46 +790,34 @@ pub async fn check_stack_for_update_inner(
alert_cache
.retain(|(stack_id, _)| stack_id != &stack.id)
.await;
return Ok(CheckStackForUpdateResponse {
services: extract_services_from_stack(&stack)
return Ok(
extract_services_from_stack(&stack)
.into_iter()
.map(|service| StackServiceWithUpdate {
latest_image: find_latest_image(
&service.service_name,
&service.image,
&stack.info.latest_services,
),
service: service.service_name,
image: service.image,
update_available: false,
})
.collect(),
stack: stack.id,
});
);
};
let StackState::Running = status.curr.state else {
alert_cache
.retain(|(stack_id, _)| stack_id != &stack.id)
.await;
return Ok(CheckStackForUpdateResponse {
stack: stack.id,
services: status
return Ok(
status
.curr
.services
.iter()
.map(|service| StackServiceWithUpdate {
latest_image: find_latest_image(
&service.service,
&service.image,
&stack.info.latest_services,
),
service: service.service.clone(),
image: service.image.clone(),
update_available: false,
})
.collect(),
});
);
};
let mut services = Vec::new();
@@ -870,14 +827,9 @@ pub async fn check_stack_for_update_inner(
service: service.service.clone(),
image: service.image.clone(),
update_available: false,
latest_image: find_latest_image(
&service.service,
&service.image,
&stack.info.latest_services,
),
};
let Some(current_digests) = &service.image_digests else {
let Some(current_digest) = &service.image_digest else {
services.push(service_with_update);
continue;
};
@@ -896,7 +848,7 @@ pub async fn check_stack_for_update_inner(
};
service_with_update.update_available =
latest_digest.update_available(current_digests);
latest_digest.update_available(&current_digest.0);
if service_with_update.update_available
&& (skip_auto_update || !stack.config.auto_update)
@@ -953,10 +905,7 @@ pub async fn check_stack_for_update_inner(
|| !stack.config.auto_update
|| services_with_update.is_empty()
{
return Ok(CheckStackForUpdateResponse {
stack: stack.id,
services,
});
return Ok(services);
}
// Conservatively remove from alert cache so 'skip_auto_update'
@@ -965,10 +914,7 @@ pub async fn check_stack_for_update_inner(
.retain(|(stack_id, _)| stack_id != &stack.id)
.await;
let deploy_services = if stack.config.auto_update_all_services
// Swarm stacks don't support individual service deploy
|| !stack.config.swarm_id.is_empty()
{
let deploy_services = if stack.config.auto_update_all_services {
Vec::new()
} else {
services_with_update
@@ -982,8 +928,6 @@ pub async fn check_stack_for_update_inner(
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 stack_id = stack.id.clone();
let run = async move {
match execute::inner_handler(
ExecuteRequest::DeployStack(DeployStack {
@@ -995,42 +939,33 @@ pub async fn check_stack_for_update_inner(
)
.await
{
Ok(res) => {
let ExecutionResult::Single(update) = res else {
unreachable!()
Ok(_) => {
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
ts,
resolved: true,
resolved_ts: ts.into(),
level: SeverityLevel::Ok,
target: ResourceTarget::Stack(stack.id.clone()),
data: AlertData::StackAutoUpdated {
id: stack.id.clone(),
name: stack.name.clone(),
swarm_id,
swarm_name,
server_id,
server_name,
images: services_with_update
.iter()
.map(|service| service.image.clone())
.collect(),
},
};
let Ok(update) = poll_update_until_complete(&update.id).await
else {
return;
};
if update.success {
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
ts,
resolved: true,
resolved_ts: ts.into(),
level: SeverityLevel::Ok,
target: ResourceTarget::Stack(stack.id.clone()),
data: AlertData::StackAutoUpdated {
id: stack.id.clone(),
name: stack.name.clone(),
swarm_id,
swarm_name,
server_id,
server_name,
images: services_with_update
.iter()
.map(|service| service.image.clone())
.collect(),
},
};
let res = db_client().alerts.insert_one(&alert).await;
if let Err(e) = res {
error!("Failed to record StackAutoUpdated to db | {e:#}");
}
send_alerts(&[alert]).await;
let res = db_client().alerts.insert_one(&alert).await;
if let Err(e) = res {
error!("Failed to record StackAutoUpdated to db | {e:#}");
}
send_alerts(&[alert]).await;
}
Err(e) => {
warn!("Failed to auto update Stack {} | {e:#}", stack.name)
@@ -1044,98 +979,5 @@ pub async fn check_stack_for_update_inner(
tokio::spawn(run);
}
Ok(CheckStackForUpdateResponse {
stack: stack_id,
services,
})
}
fn find_latest_image(
service_name: &str,
current_image: &str,
latest_services: &[StackServiceNames],
) -> Option<String> {
latest_services.iter().find_map(|latest| {
if latest.service_name == service_name
&& latest.image != current_image
{
Some(latest.image.clone())
} else {
None
}
})
}
//
impl Resolve<WriteArgs> for BatchCheckStackForUpdate {
#[instrument(
"BatchCheckStackForUpdate",
skip_all,
fields(
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
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 all_tags = if self.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let stacks = list_full_for_user_using_pattern::<Stack>(
&self.pattern,
ResourceQuery {
tags: self.tags,
..Default::default()
},
None,
None,
user,
PermissionLevel::Execute.into(),
&all_tags,
)
.await?;
let res = stacks
.into_iter()
.map(|stack| async move {
let swarm_or_server = get_swarm_or_server(
&stack.config.swarm_id,
&stack.config.server_id,
)
.await?;
swarm_or_server.verify_has_target().map_err(|e| e.error)?;
check_stack_for_update_inner(
stack.id,
&swarm_or_server,
self.skip_auto_update,
self.wait_for_auto_update,
self.skip_cache_refresh,
)
.await
})
.collect::<FuturesOrdered<_>>()
.collect::<Vec<_>>()
.await
.into_iter()
.filter_map(|res| {
res
.inspect_err(|e| {
warn!(
"Failed to check stack for update in batch run | {e:#}"
)
})
.ok()
})
.collect();
Ok(res)
}
Ok(services)
}
+105 -46
View File
@@ -26,8 +26,7 @@ use komodo_client::{
repo::Repo,
server::Server,
stack::Stack,
swarm::Swarm,
sync::{ResourceSync, ResourceSyncInfo},
sync::{ResourceSync, ResourceSyncInfo, SyncDeployUpdate},
to_path_compatible_name,
update::{Log, Update},
user::sync_user,
@@ -516,20 +515,11 @@ impl Resolve<WriteArgs> for CommitSync {
None
};
// Get the latest existing resources to preserve any meta values
let RemoteResources { resources, .. } =
crate::sync::remote::get_remote_resources(&sync, repo.as_ref())
.await
.context("failed to get remote resources")?;
let res = ExportAllResourcesToToml {
include_resources: sync.config.include_resources,
tags: sync.config.match_tags.clone(),
include_variables: sync.config.include_variables,
include_user_groups: sync.config.include_user_groups,
existing: resources
.inspect_err(|e| warn!("Existing resource TOML is unavailable, resource meta will not be preserved | ERROR: {e:#}"))
.ok(),
}
.resolve(&ReadArgs {
user: sync_user().to_owned(),
@@ -785,40 +775,110 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
let mut diffs = Vec::new();
macro_rules! push_updates {
($(($Type:ident, $field:ident)),* $(,)?) => {
$(
push_updates_for_view::<$Type>(
resources.$field,
delete,
None,
None,
&id_to_tags,
&sync.config.match_tags,
&mut diffs,
)
.await?;
)*
};
}
// New resource types need to be added here manually.
push_updates!(
(Server, servers),
(Swarm, swarms),
(Stack, stacks),
(Deployment, deployments),
(Build, builds),
(Repo, repos),
(Procedure, procedures),
(Action, actions),
(Builder, builders),
(Alerter, alerters),
(ResourceSync, resource_syncs),
);
push_updates_for_view::<Server>(
resources.servers,
delete,
None,
None,
&id_to_tags,
&sync.config.match_tags,
&mut diffs,
)
.await?;
push_updates_for_view::<Stack>(
resources.stacks,
delete,
None,
None,
&id_to_tags,
&sync.config.match_tags,
&mut diffs,
)
.await?;
push_updates_for_view::<Deployment>(
resources.deployments,
delete,
None,
None,
&id_to_tags,
&sync.config.match_tags,
&mut diffs,
)
.await?;
push_updates_for_view::<Build>(
resources.builds,
delete,
None,
None,
&id_to_tags,
&sync.config.match_tags,
&mut diffs,
)
.await?;
push_updates_for_view::<Repo>(
resources.repos,
delete,
None,
None,
&id_to_tags,
&sync.config.match_tags,
&mut diffs,
)
.await?;
push_updates_for_view::<Procedure>(
resources.procedures,
delete,
None,
None,
&id_to_tags,
&sync.config.match_tags,
&mut diffs,
)
.await?;
push_updates_for_view::<Action>(
resources.actions,
delete,
None,
None,
&id_to_tags,
&sync.config.match_tags,
&mut diffs,
)
.await?;
push_updates_for_view::<Builder>(
resources.builders,
delete,
None,
None,
&id_to_tags,
&sync.config.match_tags,
&mut diffs,
)
.await?;
push_updates_for_view::<Alerter>(
resources.alerters,
delete,
None,
None,
&id_to_tags,
&sync.config.match_tags,
&mut diffs,
)
.await?;
push_updates_for_view::<ResourceSync>(
resources.resource_syncs,
delete,
None,
None,
&id_to_tags,
&sync.config.match_tags,
&mut diffs,
)
.await?;
(diffs, deploy_updates)
} else {
Default::default()
(Vec::new(), SyncDeployUpdate::default())
};
let variable_updates = if sync.config.include_variables {
@@ -852,7 +912,7 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
let (
resource_updates,
(pending_deploys, pending_deploy_error),
deploy_updates,
variable_updates,
user_group_updates,
pending_error,
@@ -868,7 +928,7 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
};
let has_updates = !resource_updates.is_empty()
|| !pending_deploys.is_empty()
|| !deploy_updates.to_deploy == 0
|| !variable_updates.is_empty()
|| !user_group_updates.is_empty();
@@ -880,8 +940,7 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
remote_errors: sync.info.remote_errors,
pending_hash: sync.info.pending_hash,
pending_message: sync.info.pending_message,
pending_deploys,
pending_deploy_error,
pending_deploy: deploy_updates,
resource_updates,
variable_updates,
user_group_updates,
+31 -51
View File
@@ -3,12 +3,8 @@ use futures_util::{StreamExt as _, stream::FuturesUnordered};
use komodo_client::{
api::write::*,
entities::{
NoData,
deployment::Deployment,
permission::PermissionLevel,
server::Server,
stack::Stack,
terminal::{Terminal, TerminalTarget},
NoData, deployment::Deployment, permission::PermissionLevel,
server::Server, stack::Stack, terminal::TerminalTarget,
user::User,
},
};
@@ -51,35 +47,30 @@ impl Resolve<WriteArgs> for CreateTerminal {
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<Terminal> {
) -> mogh_error::Result<NoData> {
match self.target.clone() {
TerminalTarget::Server { server } => {
let server = server
.context("Must provide 'target.params.server'")
.status_code(StatusCode::BAD_REQUEST)?;
create_server_terminal(self, server, user)
.await
.map_err(Into::into)
create_server_terminal(self, server, user).await?;
}
TerminalTarget::Container { server, container } => {
create_container_terminal(self, server, container, user)
.await
.map_err(Into::into)
.await?;
}
TerminalTarget::Stack { stack, service } => {
let service = service
.context("Must provide 'target.params.service'")
.status_code(StatusCode::BAD_REQUEST)?;
create_stack_service_terminal(self, stack, service, user)
.await
.map_err(Into::into)
.await?;
}
TerminalTarget::Deployment { deployment } => {
create_deployment_terminal(self, deployment, user)
.await
.map_err(Into::into)
create_deployment_terminal(self, deployment, user).await?;
}
}
};
Ok(NoData {})
}
}
@@ -93,7 +84,7 @@ async fn create_server_terminal(
}: CreateTerminal,
server: String,
user: &User,
) -> anyhow::Result<Terminal> {
) -> anyhow::Result<()> {
let server = get_check_permissions::<Server>(
&server,
user,
@@ -103,7 +94,7 @@ async fn create_server_terminal(
let periphery = periphery_client(&server).await?;
let mut terminal = periphery
periphery
.request(api::terminal::CreateServerTerminal {
name,
command,
@@ -112,12 +103,7 @@ async fn create_server_terminal(
.await
.context("Failed to create Server Terminal on Periphery")?;
// Fix server terminal target with server id
terminal.target = TerminalTarget::Server {
server: Some(server.id),
};
Ok(terminal)
Ok(())
}
async fn create_container_terminal(
@@ -125,7 +111,7 @@ async fn create_container_terminal(
server: String,
container: String,
user: &User,
) -> anyhow::Result<Terminal> {
) -> anyhow::Result<()> {
let server = get_check_permissions::<Server>(
&server,
user,
@@ -141,7 +127,7 @@ async fn create_stack_service_terminal(
stack: String,
service: String,
user: &User,
) -> anyhow::Result<Terminal> {
) -> anyhow::Result<()> {
let (_, periphery, container) =
get_stack_service_periphery_container(&stack, &service, user)
.await?;
@@ -152,7 +138,7 @@ async fn create_deployment_terminal(
req: CreateTerminal,
deployment: String,
user: &User,
) -> anyhow::Result<Terminal> {
) -> anyhow::Result<()> {
let (_, periphery, container) =
get_deployment_periphery_container(&deployment, user).await?;
create_container_terminal_inner(req, &periphery, container).await
@@ -171,10 +157,10 @@ impl Resolve<WriteArgs> for DeleteTerminal {
)
)]
async fn resolve(
mut self,
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<NoData> {
let server = match &mut self.target {
let server = match &self.target {
TerminalTarget::Server { server } => {
let server = server
.as_ref()
@@ -195,31 +181,27 @@ impl Resolve<WriteArgs> for DeleteTerminal {
)
.await?
}
TerminalTarget::Stack {
stack: stack_id, ..
} => {
let stack = get_check_permissions::<Stack>(
stack_id,
TerminalTarget::Stack { stack, .. } => {
let server = get_check_permissions::<Stack>(
stack,
user,
PermissionLevel::Read.terminal(),
)
.await?;
// Must fix any incoming stack name to id
*stack_id = stack.id;
resource::get::<Server>(&stack.config.server_id).await?
.await?
.config
.server_id;
resource::get::<Server>(&server).await?
}
TerminalTarget::Deployment {
deployment: deployment_id,
} => {
let deployment = get_check_permissions::<Deployment>(
deployment_id,
TerminalTarget::Deployment { deployment } => {
let server = get_check_permissions::<Deployment>(
deployment,
user,
PermissionLevel::Read.terminal(),
)
.await?;
// Must fix any incoming deployment name to id
*deployment_id = deployment.id;
resource::get::<Server>(&deployment.config.server_id).await?
.await?
.config
.server_id;
resource::get::<Server>(&server).await?
}
};
@@ -293,8 +275,6 @@ impl Resolve<WriteArgs> for BatchDeleteAllTerminals {
resource::list_full_for_user::<Server>(
self.query,
None,
None,
user,
PermissionLevel::Read.terminal(),
&all_tags,
-15
View File
@@ -36,15 +36,6 @@ use super::WriteArgs;
const RECENTLY_VIEWED_MAX: usize = 10;
impl Resolve<WriteArgs> for PushRecentlyViewed {
#[instrument(
"PushRecentlyViewed",
level = "debug",
skip_all,
fields(
user_id = user.id,
resource = format!("{:?}", self.resource)
)
)]
async fn resolve(
self,
WriteArgs { user, .. }: &WriteArgs,
@@ -88,12 +79,6 @@ impl Resolve<WriteArgs> for PushRecentlyViewed {
//
impl Resolve<WriteArgs> for SetLastSeenUpdate {
#[instrument(
"SetLastSeenUpdate",
level = "debug",
skip_all,
fields(user_id = user.id)
)]
async fn resolve(
self,
WriteArgs { user, .. }: &WriteArgs,
-3
View File
@@ -24,9 +24,6 @@ use crate::{
mod terminal;
mod update;
pub(super) const WS_KEEP_ALIVE_INTERVAL: std::time::Duration =
std::time::Duration::from_secs(30);
pub fn router() -> Router {
Router::new()
// Periphery facing
+6 -35
View File
@@ -5,7 +5,6 @@ use axum::{
response::IntoResponse,
};
use bytes::Bytes;
use colored::Colorize;
use futures_util::{SinkExt, StreamExt as _};
use komodo_client::{
api::terminal::ConnectTerminalQuery, entities::user::User,
@@ -117,25 +116,9 @@ async fn forward_ws_channel(
};
let periphery_to_core = async {
let mut keep_alive =
tokio::time::interval(super::WS_KEEP_ALIVE_INTERVAL);
loop {
// Already adheres to cancellation token
let recv = tokio::select! {
// Websocket keep-alive ping
_ = keep_alive.tick() => {
if client_send
.send(ws::Message::Ping(Bytes::new()))
.await
.is_err()
{
break;
}
continue;
}
recv = periphery_receiver.recv() => recv,
};
match recv {
match periphery_receiver.recv().await {
Ok(Ok(bytes)) => {
if let Err(e) =
client_send.send(ws::Message::Binary(bytes.into())).await
@@ -145,26 +128,14 @@ async fn forward_ws_channel(
};
}
Ok(Err(e)) => {
let message = format!("{}: {e:#}", "ERROR".red().bold());
if message.contains("pty exited") {
let _ = client_send
.send(ws::Message::text(format!(
"\n{} {}",
"pty".bold(),
"exited".red().bold()
)))
.await;
} else {
let _ = client_send
.send(ws::Message::text(message.replace('\n', "\r\n")))
.await;
}
let _ = client_send
.send(ws::Message::text(format!("{e:#}")))
.await;
break;
}
Err(_) => {
let _ = client_send
.send(ws::Message::text("\r\n\nSTREAM EOF"))
.await;
let _ =
client_send.send(ws::Message::text("STREAM EOF")).await;
break;
}
}
+1 -11
View File
@@ -3,7 +3,6 @@ use axum::{
extract::{WebSocketUpgrade, ws::Message},
response::IntoResponse,
};
use bytes::Bytes;
use futures_util::{SinkExt, StreamExt};
use komodo_client::entities::{
ResourceTarget, permission::PermissionLevel, user::User,
@@ -39,19 +38,10 @@ pub async fn handler(
let cancel_clone = cancel.clone();
tokio::spawn(async move {
let mut keep_alive =
tokio::time::interval(super::WS_KEEP_ALIVE_INTERVAL);
loop {
// poll for updates off the receiver / await cancel / send keep-alive ping.
// poll for updates off the receiver / await cancel.
let update = select! {
_ = cancel_clone.cancelled() => break,
_ = keep_alive.tick() => {
if ws_sender.send(Message::Ping(Bytes::new())).await.is_err() {
cancel_clone.cancel();
break;
}
continue;
}
update = receiver.recv() => {update.expect("failed to recv update msg")}
};
+4 -17
View File
@@ -143,12 +143,14 @@ impl AuthImpl for KomodoAuthImpl {
}
fn host(&self) -> &str {
&core_config().host
static AUTH_HOST: LazyLock<String> =
LazyLock::new(|| format!("{}/auth", core_config().host));
&AUTH_HOST
}
fn post_link_redirect(&self) -> &str {
static POST_LINK_REDIRECT: LazyLock<String> =
LazyLock::new(|| format!("{}/profile", core_config().host));
LazyLock::new(|| format!("{}/settings", core_config().host));
&POST_LINK_REDIRECT
}
@@ -205,20 +207,6 @@ impl AuthImpl for KomodoAuthImpl {
core_config().disable_user_registration
}
fn local_registration_disabled(&self) -> bool {
let config = core_config();
config
.disable_local_user_registration
.unwrap_or(config.disable_user_registration)
}
fn oidc_registration_disabled(&self) -> bool {
let config = core_config();
config
.disable_oidc_user_registration
.unwrap_or(config.disable_user_registration)
}
fn validate_username(
&self,
username: &str,
@@ -362,7 +350,6 @@ impl AuthImpl for KomodoAuthImpl {
additional_audiences: config
.oidc_additional_audiences
.clone(),
auto_redirect: config.oidc_auto_redirect,
}
});
Some(&OIDC_CONFIG)
+1 -1
View File
@@ -88,7 +88,7 @@ pub async fn launch_ec2_instance(
port: _,
use_https: _,
git_providers: _,
image_registries: _,
docker_registries: _,
secrets: _,
} = config;
let instance_type = handle_unknown_instance_type(
+2 -3
View File
@@ -2,9 +2,8 @@ pub mod aws;
#[derive(Debug)]
pub enum BuildCleanupData {
/// Store the builder id if it needs
/// to be removed from builder_usage_cache.
Server(Option<String>),
/// Nothing to clean up
Server,
/// Cleanup Periphery connection
Url,
/// Clean up AWS instance
+13 -53
View File
@@ -235,9 +235,6 @@ pub fn core_config() -> &'static CoreConfig {
env.komodo_oidc_additional_audiences,
)
.unwrap_or(config.oidc_additional_audiences),
oidc_auto_redirect: env
.komodo_oidc_auto_redirect
.unwrap_or(config.oidc_auto_redirect),
google_oauth: NamedOauthConfig {
enabled: env
.komodo_google_oauth_enabled
@@ -296,6 +293,9 @@ pub fn core_config() -> &'static CoreConfig {
first_server_name: env
.komodo_first_server_name
.or(config.first_server_name),
frontend_path: env
.komodo_frontend_path
.unwrap_or(config.frontend_path),
jwt_ttl: env.komodo_jwt_ttl.unwrap_or(config.jwt_ttl),
auth_rate_limit_disabled: env
.komodo_auth_rate_limit_disabled
@@ -312,24 +312,15 @@ pub fn core_config() -> &'static CoreConfig {
cors_allow_credentials: env
.komodo_cors_allow_credentials
.unwrap_or(config.cors_allow_credentials),
session_allow_cross_site: env
.komodo_session_allow_cross_site
.unwrap_or(config.session_allow_cross_site),
x_content_type_options: env
.komodo_x_content_type_options
.unwrap_or(config.x_content_type_options),
x_frame_options: env
.komodo_x_frame_options
.unwrap_or(config.x_frame_options),
x_xss_protection: env
.komodo_x_xss_protection
.unwrap_or(config.x_xss_protection),
referrer_policy: env
.komodo_referrer_policy
.unwrap_or(config.referrer_policy),
content_security_policy: env
.komodo_content_security_policy
.unwrap_or(config.content_security_policy),
sync_directory: env
.komodo_sync_directory
.unwrap_or(config.sync_directory),
repo_directory: env
.komodo_repo_directory
.unwrap_or(config.repo_directory),
action_directory: env
.komodo_action_directory
.unwrap_or(config.action_directory),
resource_poll_interval: env
.komodo_resource_poll_interval
.unwrap_or(config.resource_poll_interval),
@@ -348,9 +339,6 @@ pub fn core_config() -> &'static CoreConfig {
transparent_mode: env
.komodo_transparent_mode
.unwrap_or(config.transparent_mode),
default_pagination_limit: env
.komodo_default_pagination_limit
.unwrap_or(config.default_pagination_limit),
ui_write_disabled: env
.komodo_ui_write_disabled
.unwrap_or(config.ui_write_disabled),
@@ -366,12 +354,6 @@ pub fn core_config() -> &'static CoreConfig {
disable_user_registration: env
.komodo_disable_user_registration
.unwrap_or(config.disable_user_registration),
disable_local_user_registration: env
.komodo_disable_local_user_registration
.or(config.disable_local_user_registration),
disable_oidc_user_registration: env
.komodo_disable_oidc_user_registration
.or(config.disable_oidc_user_registration),
disable_non_admin_create: env
.komodo_disable_non_admin_create
.unwrap_or(config.disable_non_admin_create),
@@ -402,9 +384,6 @@ pub fn core_config() -> &'static CoreConfig {
.komodo_logging_location
.unwrap_or(config.logging.location),
ansi: env.komodo_logging_ansi.unwrap_or(config.logging.ansi),
timestamps: env
.komodo_logging_timestamps
.unwrap_or(config.logging.timestamps),
otlp_endpoint: env
.komodo_logging_otlp_endpoint
.unwrap_or(config.logging.otlp_endpoint),
@@ -433,30 +412,11 @@ pub fn core_config() -> &'static CoreConfig {
ssl_cert_file: env
.komodo_ssl_cert_file
.unwrap_or(config.ssl_cert_file),
reporting_enabled: env
.komodo_reporting_enabled
.unwrap_or(config.reporting_enabled),
reporting_private_key: env
.komodo_reporting_private_key
.unwrap_or(config.reporting_private_key),
ui_path: env.komodo_ui_path.unwrap_or(config.ui_path),
ui_index_force_no_cache: env
.komodo_ui_index_force_no_cache
.unwrap_or(config.ui_index_force_no_cache),
sync_directory: env
.komodo_sync_directory
.unwrap_or(config.sync_directory),
repo_directory: env
.komodo_repo_directory
.unwrap_or(config.repo_directory),
action_directory: env
.komodo_action_directory
.unwrap_or(config.action_directory),
// These can't be overridden on env
secrets: config.secrets,
git_providers: config.git_providers,
image_registries: config.image_registries,
docker_registries: config.docker_registries,
}
})
}
-23
View File
@@ -1,7 +1,6 @@
use std::time::Duration;
use anyhow::{Context, anyhow};
use komodo_client::entities::server::Server;
use periphery_client::{
CONNECTION_RETRY_SECONDS, transport::LoginMessage,
};
@@ -19,7 +18,6 @@ use transport::{
use crate::{
config::{core_config, core_connection_query},
monitor::refresh_server_cache,
periphery::PeripheryClient,
state::periphery_connections,
};
@@ -48,7 +46,6 @@ impl PeripheryConnectionArgs<'_> {
let responses = connection.responses.clone();
let _id = id.clone();
tokio::spawn(async move {
loop {
let ws = tokio::select! {
@@ -73,14 +70,6 @@ impl PeripheryConnectionArgs<'_> {
}
};
debug!(
host = identifiers.host(),
query = core_connection_query(),
sec_websocket_accept = accept.to_str().unwrap_or_default(),
resource_id = connection.args.id,
"[PERIPHERY AUTH] Zero trust identifiers"
);
let identifiers = identifiers.build(
accept.as_bytes(),
core_connection_query().as_bytes(),
@@ -97,18 +86,6 @@ impl PeripheryConnectionArgs<'_> {
continue;
};
// Waits until after connection is handled then
// force refreshes the server cache.
let id = _id.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
let Ok(server) = crate::resource::get::<Server>(&id).await
else {
return;
};
refresh_server_cache(&server, true).await;
});
connection.handle_socket(socket, &mut receiver).await
}
});
+9 -30
View File
@@ -1,4 +1,4 @@
use std::{str::FromStr, time::Duration};
use std::str::FromStr;
use anyhow::{Context, anyhow};
use axum::{
@@ -42,7 +42,6 @@ use crate::{
api::write::WriteArgs,
config::core_keys,
helpers::query::id_or_name_filter,
monitor::refresh_server_cache,
resource::KomodoResource,
state::{db_client, periphery_connections},
};
@@ -130,14 +129,6 @@ async fn existing_server_handler(
return;
};
debug!(
host = identifiers.host.to_str().unwrap_or_default(),
query,
sec_websocket_accept = identifiers.accept,
resource_id = &server.id,
"[PERIPHERY AUTH] Zero trust identifiers"
);
let span = info_span!(
"PeripheryLogin",
server_id = server.id,
@@ -164,7 +155,7 @@ async fn existing_server_handler(
socket: &mut socket,
identifiers: identifiers.build(query.as_bytes()),
private_key: core_keys().load().private.as_str(),
public_key_validator: OnboardingKeyValidator { privileged_required: true },
public_key_validator: OnboardingKeyValidator { fix_existing_servers_required: true },
should_close: true
})
.await
@@ -202,13 +193,6 @@ async fn existing_server_handler(
return;
}
// Waits until after connection is handled then
// force refreshes the server cache.
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
refresh_server_cache(&server, true).await;
});
connection.handle_socket(socket, &mut receiver).await
}))
}
@@ -235,7 +219,7 @@ async fn fix_existing_server_handler(
socket: &mut socket,
identifiers: identifiers.build(query.as_bytes()),
private_key: core_keys().load().private.as_str(),
public_key_validator: OnboardingKeyValidator { privileged_required: true },
public_key_validator: OnboardingKeyValidator { fix_existing_servers_required: true },
should_close: true,
})
.await
@@ -307,13 +291,6 @@ async fn fix_server(
} else {
None
},
// Move address to external_address if not set.
// This helps preserve container port link behavior.
external_address: if !server.config.external_address.is_empty() {
Some(server.config.address)
} else {
None
},
..Default::default()
};
if !config.is_none() {
@@ -356,7 +333,7 @@ async fn onboard_new_server_handler(
socket: &mut socket,
identifiers: identifiers.build(query.as_bytes()),
private_key: core_keys().load().private.as_str(),
public_key_validator: OnboardingKeyValidator { privileged_required: false },
public_key_validator: OnboardingKeyValidator { fix_existing_servers_required: false },
should_close: true
})
.await
@@ -503,7 +480,7 @@ async fn create_server_maybe_builder(
name: server_query,
config: PartialBuilderConfig::Server(
PartialServerBuilderConfig {
server_ids: Some(vec![server.id.clone()]),
server_id: Some(server.id.clone()),
},
),
})
@@ -520,7 +497,7 @@ async fn create_server_maybe_builder(
}
struct OnboardingKeyValidator {
privileged_required: bool,
fix_existing_servers_required: bool,
}
impl PublicKeyValidator for OnboardingKeyValidator {
@@ -542,7 +519,9 @@ impl PublicKeyValidator for OnboardingKeyValidator {
{
return Err(anyhow!("Onboarding key is invalid"));
}
if self.privileged_required && !onboarding_key.privileged {
if self.fix_existing_servers_required
&& !onboarding_key.fix_existing_servers
{
return Err(anyhow!(
"Onboarding key not able to fix existing servers"
));
+27 -112
View File
@@ -1,18 +1,16 @@
use std::{collections::HashMap, time::Duration};
use std::time::Duration;
use anyhow::{Context, anyhow};
use database::mungos::mongodb::bson::oid::ObjectId;
use formatting::muted;
use futures_util::{StreamExt, stream::FuturesOrdered};
use komodo_client::entities::{
Version,
builder::{AwsBuilderConfig, Builder, BuilderConfig},
komodo_timestamp,
server::{Server, ServerState},
server::Server,
update::{Log, Update},
};
use periphery_client::api::{self, GetVersionResponse};
use tokio::sync::Mutex;
use crate::{
cloud::{
@@ -26,7 +24,6 @@ use crate::{
helpers::update::update_update,
periphery::PeripheryClient,
resource,
state::{builder_usage_cache, server_status_cache},
};
use super::periphery_client;
@@ -40,26 +37,25 @@ const BUILDER_POLL_MAX_TRIES: usize = 60;
fields(
resource_name,
builder_id = builder.id,
update_id = update.as_ref().map(|u| u.id.as_str())
update_id = update.id
)
)]
pub async fn connect_builder_periphery(
// build: &Build,
resource_name: String,
version: Option<Version>,
builder: Builder,
update: Option<&mut Update>,
update: &mut Update,
) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {
match builder.config {
BuilderConfig::Aws(config) => {
get_aws_builder(&resource_name, version, config, update).await
}
BuilderConfig::Url(config) => {
if config.address.is_empty() {
return Err(anyhow!(
"Builder has not yet configured an address"
));
}
// Builder id no good because it may be active for multiple connections.
// TODO: Dont use builder id, or will be problems
// with simultaneous spawned builders.
let periphery = PeripheryClient::new(
PeripheryConnectionArgs::from_url_builder(
&ObjectId::new().to_hex(),
@@ -68,66 +64,22 @@ pub async fn connect_builder_periphery(
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
.context("Url Builder failed health check")
{
Ok(_) => return Ok((periphery, BuildCleanupData::Url)),
Err(e) => err = Some(e),
};
}
Err(err.context("Missing error")?)
periphery
.health_check()
.await
.context("Url Builder failed health check")?;
Ok((periphery, BuildCleanupData::Url))
}
BuilderConfig::Server(config) => {
if config.server_ids.is_empty() {
return Err(anyhow!(
"Server Builder has no configured Servers"
));
if config.server_id.is_empty() {
return Err(anyhow!("Builder has not configured a server"));
}
// Short path for single configured builder
if config.server_ids.len() == 1 {
let server =
resource::get::<Server>(&config.server_ids[0]).await?;
let periphery = periphery_client(&server).await?;
return Ok((periphery, BuildCleanupData::Server(None)));
}
// Get filtered list of available Servers
let server_status_cache = server_status_cache();
let available_server_ids = config
.server_ids
.iter()
.map(|server_id| async move {
server_status_cache
.get(server_id)
.await
.map(|s| matches!(s.state, ServerState::Ok))
.unwrap_or_default()
.then_some(server_id)
})
.collect::<FuturesOrdered<_>>()
.collect::<Vec<_>>()
.await
.into_iter()
.flatten()
.collect::<Vec<_>>();
let selected = builder_usage_cache()
.get_or_insert_default(&builder.id)
.await
.select(&available_server_ids)
.await
.context("Server builder has no available servers")?;
let server = resource::get::<Server>(selected).await?;
let server = resource::get::<Server>(&config.server_id).await?;
let periphery = periphery_client(&server).await?;
Ok((periphery, BuildCleanupData::Server(Some(server.id))))
Ok((periphery, BuildCleanupData::Server))
}
BuilderConfig::Aws(config) => {
get_aws_builder(&resource_name, version, config, update).await
}
}
}
@@ -137,14 +89,14 @@ pub async fn connect_builder_periphery(
skip_all,
fields(
resource_name,
update_id = update.as_ref().map(|u| u.id.as_str()),
update_id = update.id,
)
)]
async fn get_aws_builder(
resource_name: &str,
version: Option<Version>,
config: AwsBuilderConfig,
mut update: Option<&mut Update>,
update: &mut Update,
) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {
let start_create_ts = komodo_timestamp();
@@ -162,10 +114,9 @@ async fn get_aws_builder(
..Default::default()
};
if let Some(update) = &mut update {
update.logs.push(log);
update_update((*update).clone()).await?;
}
update.logs.push(log);
update_update(update.clone()).await?;
let protocol = if config.use_https { "wss" } else { "ws" };
@@ -202,10 +153,8 @@ async fn get_aws_builder(
end_ts: komodo_timestamp(),
..Default::default()
};
if let Some(update) = update {
update.logs.push(connect_log);
update_update(update.clone()).await?;
}
update.logs.push(connect_log);
update_update(update.clone()).await?;
return Ok((
periphery,
BuildCleanupData::Aws {
@@ -245,17 +194,9 @@ pub async fn cleanup_builder_instance(
update: &mut Update,
) {
match cleanup_data {
BuildCleanupData::Server(None) => {
BuildCleanupData::Server => {
// Nothing to clean up
}
BuildCleanupData::Server(Some(builder_id)) => {
// Release periphery (server) id from builder
builder_usage_cache()
.get_or_insert_default(&builder_id)
.await
.release(&periphery.id)
.await
}
BuildCleanupData::Url => {
periphery.cleanup().await;
}
@@ -311,29 +252,3 @@ pub fn start_aws_builder_log(
]
.join("\n")
}
#[derive(Default)]
pub struct BuilderUsage(Mutex<HashMap<String, usize>>);
impl BuilderUsage {
pub async fn select<'a>(
&self,
available: &'a [&String],
) -> Option<&'a str> {
let mut lock = self.0.lock().await;
let selected = *available.iter().min_by_key(|key| {
lock.get(key.as_str()).copied().unwrap_or(0)
})?;
*lock.entry(selected.clone()).or_insert(0) += 1;
Some(selected.as_str())
}
pub async fn release(&self, key: &str) {
let mut lock = self.0.lock().await;
if let Some(count) = lock.get_mut(key)
&& *count > 0
{
*count -= 1;
}
}
}
+23 -23
View File
@@ -39,34 +39,34 @@ impl ImageDigestCache {
account: Option<String>,
token: Option<String>,
) -> anyhow::Result<ImageDigest> {
if let Some((digest, valid_until)) = self.0.get(image).await
if let Some((digest, valid_until)) = self.0.get(&image).await
// Ensure the query time was within last 10 mins to use cache.
&& valid_until > komodo_timestamp()
{
return Ok(digest);
}
let digest = swarm_or_server_request(
swarm_or_server,
GetLatestImageDigest {
name: image.clone(),
account,
token,
},
)
.await?
.digest;
let digest = ImageDigest::new(image, &digest);
self
.0
.insert(
image,
(digest.clone(), komodo_timestamp() + 10 * 60 * 1_000),
} else {
let digest = swarm_or_server_request(
&swarm_or_server,
GetLatestImageDigest {
name: image.clone(),
account,
token,
},
)
.await;
.await?
.digest;
Ok(digest)
let digest = ImageDigest::new(image, &digest);
self
.0
.insert(
image,
(digest.clone(), komodo_timestamp() + 10 * 60 * 1_000),
)
.await;
Ok(digest)
}
}
}
+1 -1
View File
@@ -169,7 +169,7 @@ pub async fn registry_token(
}
Ok(
core_config()
.image_registries
.docker_registries
.iter()
.find(|provider| provider.domain == provider_domain)
.and_then(|provider| {
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -55,7 +55,7 @@ async fn prune_images() -> anyhow::Result<()> {
while let Some((res, server)) = futures.next().await {
if let Err(e) = res {
warn!(
"Failed to prune images on Server {} ({}) | {e:#}",
"failed to prune images on Server {} ({}) | {e:#}",
server.name, server.id
)
}
+1 -33
View File
@@ -45,7 +45,7 @@ use crate::{
state::{
action_state_cache, action_states, db_client,
deployment_status_cache, procedure_state_cache,
server_status_cache, stack_status_cache,
stack_status_cache,
},
};
@@ -97,38 +97,6 @@ pub async fn get_server_state(server: &Server) -> ServerState {
}
}
/// The Server state as computed for the server list items,
/// from the in memory status cache.
pub async fn get_cached_server_state(id: &String) -> ServerState {
server_status_cache()
.get(id)
.await
.map(|status| status.state)
.unwrap_or_default()
}
/// The Stack state as computed for the stack list items,
/// from the in memory action states / status cache.
pub async fn get_cached_stack_state(id: &String) -> StackState {
if action_states()
.stack
.get(id)
.await
.map(|s| s.get().map(|s| s.deploying))
.transpose()
.ok()
.flatten()
.unwrap_or_default()
{
return StackState::Deploying;
}
stack_status_cache()
.get(id)
.await
.map(|status| status.curr.state)
.unwrap_or_default()
}
pub async fn get_deployment_state(
id: &String,
) -> anyhow::Result<DeploymentState> {
+23 -1
View File
@@ -1,3 +1,5 @@
use std::time::Duration;
use anyhow::anyhow;
use komodo_client::entities::server::{Server, ServerState};
use mogh_resolver::HasResponse;
@@ -11,6 +13,23 @@ pub async fn swarm_request<T>(
server_ids: &[String],
request: T,
) -> anyhow::Result<T::Response>
where
T: std::fmt::Debug + Clone + Serialize + HasResponse,
T::Response: DeserializeOwned,
{
swarm_request_custom_timeout(
server_ids,
request,
Duration::from_secs(10),
)
.await
}
pub async fn swarm_request_custom_timeout<T>(
server_ids: &[String],
request: T,
timeout: Duration,
) -> anyhow::Result<T::Response>
where
T: std::fmt::Debug + Clone + Serialize + HasResponse,
T::Response: DeserializeOwned,
@@ -35,7 +54,10 @@ where
else {
continue;
};
match periphery.request(request.clone()).await {
match periphery
.request_custom_timeout(request.clone(), timeout)
.await
{
Ok(res) => return Ok(res),
Err(e) => err = Some(e),
}
+11 -10
View File
@@ -6,7 +6,7 @@ use komodo_client::{
permission::PermissionLevel,
server::Server,
stack::Stack,
terminal::{ContainerTerminalMode, Terminal, TerminalTarget},
terminal::{ContainerTerminalMode, TerminalTarget},
user::User,
},
};
@@ -84,7 +84,7 @@ async fn setup_server_target_for_user(
if let Some(init) = init {
periphery
.request(api::terminal::CreateServerTerminal {
name: Some(terminal.clone()),
name: terminal.clone(),
command: init.command,
recreate: init.recreate,
})
@@ -131,7 +131,7 @@ async fn setup_container_target_for_user(
if let Some(init) = init {
create_container_terminal_inner(
CreateTerminal {
name: Some(terminal.clone()),
name: terminal.clone(),
target: target.clone(),
command: init.command,
mode: init.mode,
@@ -166,7 +166,7 @@ async fn setup_stack_service_target_for_user(
if let Some(init) = init {
create_container_terminal_inner(
CreateTerminal {
name: Some(terminal.clone()),
name: terminal.clone(),
target: target.clone(),
command: init.command,
mode: init.mode,
@@ -199,7 +199,7 @@ async fn setup_deployment_target_for_user(
if let Some(init) = init {
create_container_terminal_inner(
CreateTerminal {
name: Some(terminal.clone()),
name: terminal.clone(),
target: target.clone(),
command: init.command,
mode: init.mode,
@@ -241,7 +241,7 @@ pub async fn create_container_terminal_inner(
}: CreateTerminal,
periphery: &PeripheryClient,
container: String,
) -> anyhow::Result<Terminal> {
) -> anyhow::Result<()> {
match mode.unwrap_or_default() {
ContainerTerminalMode::Exec => periphery
.request(periphery_client::api::terminal::CreateContainerExecTerminal {
@@ -254,7 +254,7 @@ pub async fn create_container_terminal_inner(
.await
.context(
"Failed to create Container Exec Terminal on Periphery",
),
)?,
ContainerTerminalMode::Attach => periphery
.request(periphery_client::api::terminal::CreateContainerAttachTerminal {
name,
@@ -265,8 +265,9 @@ pub async fn create_container_terminal_inner(
.await
.context(
"Failed to create Container Attach Terminal on Periphery",
),
}
)?,
};
Ok(())
}
pub async fn get_stack_service_periphery_container(
@@ -332,7 +333,7 @@ pub async fn get_deployment_periphery_container(
let periphery = periphery_client(&server).await?;
let container = deployment.deployed_name().to_string();
let container = deployment.name.clone();
Ok((
TerminalTarget::Deployment {

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