Compare commits

..
26 Commits
Author SHA1 Message Date
mbecker20 97efa4808a 2.3.0 2026-07-29 15:09:58 -07:00
8708f0f600 2.3.0 (#1520)
* dockerfile builds use latest rust 1.96.0

* bump rust and js dependencies

* deploy 2.3.0-dev-1

* omnisearch clear on close option (default true), fix omnisearch border radius cutting off search input

* fix resource description responsive overflow in some screen size

* kill process group

* Start wiring through shell timeout / external cancel options

* deploy 2.3.0-dev-2

* implement CommandOptions (timeout / cancel ability) throughout shell usage

* Add timeouts to read child commands

* deploy 2.3.0-dev-3

* remove periphery custom_timeout option, it doesn't function as timeout as expected

* customize command timeouts

* Ensure git is installed for clear error message

* deploy 2.3.0-dev-4

* deploy 2.3.0-dev-5

* deploy 2.3.0-dev-6

* swarm completed in roadmap

* unnecessary line

* Komodo v2 march 2026

* remove debub container ports logs

* remove log view console log

* Fix deployment tabs disappear

* align other resource store tab per resource id

* disable terminal tab when attached to swarm

* List All Docker Containers supports wildcard container name matching

* improve container pagination situation

* deploy 2.3.0-dev-7

* fix container data table search slowness

* deploy 2.3.0-dev-8

* ListAllStackServices, add stack services to omni search, All containers filter by tags (based on server)

* deploy 2.3.0-dev-9

* service icon correct size

* list all containers / services support filtering by state

* get latest image digest timeout 10s

* deploy 2.3.0-dev-10

* send keepalive pings on user websockets to survive idle proxy timeouts (#1486)

* bump mantine and other UI deps

* deploy 2.3.0-dev-11

* fix containers page server filter

* show server config always if no swarms exist

* DeleteTerminal: fix incoming stack / deployment names to ids internally

* single builder distribute to multiple specified servers

* deploy 2.3.0-dev-12

* fix server type builder config comment

* fix builder toml server_ids => server/s

* sync: fix alerter resources / except_resources using ids instead of names

* fix multi server builder resource page header

* deploy 2.3.0-dev-13

* fix builder resource header with multiple servers styling

* run dkf -> dku

* white background favicon + gen resources schema

* feat: break out cache/buffers and ZFS ARC from used memory (#1489)

* deploy 2.3.0-dev-14

* onmisearch improve finding containers / services

* refine service / container search

* deploy 2.3.0-dev-15

* deploy 2.3.0-dev-16

* bump mogh auth server, remove authors from cargo toml in line with newer spec

* deploy 2.3.0-dev-17

* improve selection of available building server, works when multiple builds sent simultaneously

* deploy 2.3.0-dev-18

* show stack services even when down

* improve stack services view when down

* deploy 2.3.0-dev-19

* mogh ui 0.7.0 including login page back button if already logged in

* deploy 2.3.0-dev-20

* fix containers pagination boundary

* fix update permissions on swarm

* deploy 2.3.0-dev-21

* skip alerting check on initial server cache refresh after startup

* deploy 2.3.0-dev-22

* Fix build - deployment flow when build doesn't publish semver image tags

* move check button to left of update available button

* omnisearch containers only fetch when query types

* Add cancel procedure / action apis

* add procedure / action cancel buttons

* deploy 2.3.0-dev-23

* Execution error

* useIsCancelling hook for better state conveyance

* fmt mem module

* allow all builders build to cancel during build

* deploy 2.3.0-dev-24

* bump rust to 1.96.1

* tweak ui index.html

* build webhook cancel build before triggering another - better behavior for back-to-back pushes

* deploy 2.3.0-dev-25

* De-vendor container apis from Docker

* deploy 2.3.0-dev-26

* startup support for pagination

* implement limit / page on the list routes

* swarm pagination and add swarm read support to cli

* use Option for limit

* resource selector and omni search using pagination / search

* deploy 2.3.0-dev-27

* update available disable action state polling in small

* resources page support pagination

* fix Image bug, improve resources page debounce

* fix omni missing !

* procedure failure update log provides link to view failed update

* fix omni search link ids with brute force query

* improve command error message formatting

* fix dashboard query for right resources

* attach linked resource names in addition to ids for efficient usage with pagination

* fix pagination bugs and improve pagination efficiency

* some brute force pagination fixes still with limit: 0

* deploy 2.3.0-dev-28

* replace limit: 0 server query with proper search based multi selector

* list api support filtering by state, use with dashboard active table

* Fix state filter pagination and create terminal limit: 0 queries

* deploy 2.3.0-dev-29

* deployment / stack filter by swarm id

* deploy 2.3.0-dev-30

* improve database query efficiency on large resource sets

* fix ListFull not filtering by query states

* fix the list permits respect resource.base_permission

* use saturating mul in skip computations

* Fix list all containers / stack services - tags query, terms case insensitive

* deploy 2.3.0-dev-31

* api based list resource sorting which works with pagination

* ui implement server side table sort

* fix cli usage

* deploy 2.3.0-dev-32

* ensure required refetch intervals are still passed for data freshness

* omni search services before containers

* add Deployment custom_name configuration to set specific container name

* GetResourceMatchingContainer fix empty container passed check

* validate conatiner / docker compatible names protect against injection

* track deployment deployed name for smooth swarm service renaming behavior

* deploy 2.3.0-dev-33

* batch executions support filtering by tag

* fix clippy lint

* improve tag multi selector matching resource sync config

* deploy 2.3.0-dev-34

* fix containers page networks / volumes wrapping

* use enum cmp for sorting by state

* container page uses api based sort

* move list stack methods

* deploy 2.3.0-dev-35

* stack / deployment state sorting prefers update available shown first

* ensure name is always fallback sort

* deploy 2.3.0-dev-36

* fix Source sorting for stacks, builds, syncs

* deploy 2.3.0-dev-37

* keepPreviousData method to prevent lots of loading flashes

* omni search tweaks / fixes

* fix clear on close

* paginated schedules

* for efficient schedule sort

* paginated terminals

* fix terminals cli

* fix clippy lint

* deploy 2.3.0-dev-38

* configurable default pagination, including 0 (no pagination)

* deploy 2.3.0-dev-39

* omnisearch use keepPreviousData

* Fix omni search auto highlight first item

* drop resource action guard / reset action state before pushing updates to clients for requery

* deploy 2.3.0-dev-40

* builder instance type use divided children

* default pagination limit 50

* UI: useDebouncedTermSearch hook

* stats page

* deploy 2.3.0-dev-41

* page description tweak

* dashboard tables show template query mode

* fix stats page legend over tooltip

* setup-periphery.py: add "--core-public-keys" command line argument (#1524)

* only log core public keys if provided

* fix terminal execute echo colliding with output sentinels (#1522)

Build the execute command as a single physical line so the PTY's local
echo cannot reproduce a bare sentinel line before the real output. The
`printf` format strings now use literal `\n` escapes (which printf
expands) instead of embedded newline bytes, keeping the emitted output
byte-identical while making the echoed command one line.

Closes #1289


Claude-Session: https://claude.ai/code/session_01M1ZTEJFKv3BAW36VmBwe2v

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* deploy 2.3.0-dev-42

* nowrap on server stats containers table

* improve Resources tabs with pagination support and multi select executions

* allow simultaneous server container execs

* Container page batch executions

* use controlled selection state for resources for proper clearing selection state after delete

* deploy 2.3.0-dev-43

* server docker resources multi select

* server docker tabs move execute button

* fix: log webhook branch-mismatch skips at debug instead of warn (#1528)

Git providers send push webhooks for every branch and Komodo filters
them server side (docs: Automate > Webhooks > Branch Filtering), so a
push to a non-matching branch is routine operation, not a failure. It
was surfaced through the generic 'Failed at running webhook' warn,
indistinguishable from genuine failures, and a repo with active bot
branches (e.g. Renovate) produces hundreds of these warns per day.

Rename ExtractBranch::verify_branch to branch_matches, returning
Ok(false) on a mismatch (logged at debug) so handlers skip quietly,
matching the existing silent webhook_enabled=false skip. Branch
EXTRACTION failures (e.g. a ref-less payload on a named-branch
listener) still return Err and reach the warn, preserving the
misconfiguration signal.

* fix: parse compose config before sanitizing so secret replacers cannot corrupt service detection (#1529)

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Maxwell Becker <49575486+mbecker20@users.noreply.github.com>

* clean up fix compose parse / sanitize ordering

* deploy 2.3.0-dev-44

* user permissioning fix search

* Fix GetHistoricalServerStats pagination

* server stats list (avoiding lots of queries) and server side sorting by stats (cpu, mem, disk)

* fix clippy lint

* deploy 2.3.0-dev-45

* fix ui build after monaco bump

* load average chart first in stats

* Fix deployment build version selector

* remove build version console log

* provide logical core count, update load to reflect

* deploy 2.3.0-dev-46

* add basic reporting functionality

* Add / fix shell escaping for periphery commands

* safe stdin token and prevent args passed through resource names on shell

* deploy 2.3.0-dev-47

* Delete only batch executions just use delete button

* add New / batch executions to dashboard tables

* rust 1.97.1

* deploy 2.3.0-dev-48

* default key reporting.key

* default reporting.key

* redeploy on build for all running unhealthy states (rebuild to fix eg restarting issue)

* resource updates query memo missing buildId in deps

* deploy 2.3.0-dev-49

* Fix reporting signature

* deploy 2.3.0-dev-50

---------

Co-authored-by: Remco Jongschaap <hey@dougley.com>
Co-authored-by: ChanningHe <52875777+ChanningHe@users.noreply.github.com>
Co-authored-by: Amos Shapira <amos.shapira@gmail.com>
Co-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Christopher Plieger <917744+cplieger@users.noreply.github.com>
2026-07-29 15:08:28 -07:00
mbecker20 5c301020c1 Docs: Community: Terraform Provider Komodo + Scaling Renovate 2026-05-11 18:44:37 -07:00
mbecker20 f66754ef9e 2.2.0 ui quickfix - bump mantine, fix tag height on chrome/safari 2026-05-08 13:52:25 -07:00
mbecker20 2aa8a441df Actually bump mogh ui version for oidc auto redirect 2026-05-07 14:31:05 -07:00
mbecker20 1674ce2d51 fix docker build requires xtask folder copy 2026-05-07 14:30:07 -07:00
0d462fb2e0 2.2.0 (#1387)
* fix login page import

* update to mantine v9, reduce the border radius on some things

* deploy 2.2.0-dev-1

* move theme to mogh ui

* start moving stuff to mogh_ui

* move more components to mogh_ui

* move section to mogh_ui

* move confirm button

* move colors and formatting to mogh_ui

* move more to mogh_ui

* move more components to mogh ui

* move entity header / page to mogh ui

* move monaco to mogh_monaco

* move config / monaco to mogh ui

* move text update modal to mogh ui, empty ui folder

* deploy 2.2.0-dev-2

* move auth hooks to mogh_ui

* move login to mogh ui

* move profile management to mogh ui

* add missing dedupe deps

* bump mogh ui deps

* expand shell process path static load

* move filterBySplit to mogh_ui

* align core aio dockerfile

* bump mogh ui

* add published mogh_auth_client

* comment about onboarding key

* fix lockfile

* bump mogh ui

* improve doc comment on CoreInfo public_key

* UI: topbar home butotn use Link instead of nav, cmd click open in new tab

* Minor: Update periphery.config.toml (#1365)

* Minor: Update periphery.config.toml

Make docs on ROOT_DIRECTORY more clear

This change relates to line:
> ## Each specific sub-directory (like ./stacks) can be overridden below.

The change includes `./builds` directory to the list as it also can be overridden. Thus, a reader will have better understanding of the resulting root directory structure

* Update compose.env

* fix cli multi arch dockerfile and bump other dockerfiles

* Add OIDC auto-redirect config to Core (#1339)

* Add OIDC auto-redirect config to Core

When KOMODO_OIDC_AUTO_REDIRECT=true, the login options response
includes oidc_auto_redirect, enabling the UI (via mogh-lib) to
automatically redirect unauthenticated users to the OIDC provider.
Users can bypass by appending ?disableAutoLogin to the login URL.

Depends on moghtech/lib UI change adding auto-redirect to LoginPage.

Closes #311

* Skip OIDC auto-redirect for Add Account link

The user dropdown's Add Account button navigates to /login to add
another account. When OIDC auto-redirect is enabled, this link must
opt out so the user can actually sign in as a different user instead
of being redirected back to the already-authenticated OIDC session.

* bump deps

* deploy 2.2.0-dev-3

* Fix swarm deployment Tasks and Inspect tabs

* Fix build cancel button variant default

* RunSync: Make sure resource sync changes run after repo changes due to dependency

* deploy 2.2.0-dev-4

* deploy 2.2.0-dev-5

* fmt

* impl mogh server 1.5.0 configuration security headers

* to the user

* agents send

* let user specify if they want command to be run in shell-mode (#1265)

* allow users to add commands as shell-mode

* revert package.json yarn auto-change

* cargo fmt

* bump bollard and other deps

* gen client

* deploy 2.2.0-dev-6

* bump dockerfile rust version 1.95.0

* document new core configurations

* deploy 2.2.0-dev-7

* 2 small UI fixes (#1379)

* fix: prevent horizontal page scroll on long action log lines

Add white-space: pre-wrap to Code component pre elements rendering
stdout/stderr in action last-run, update details, and build info views.

* fix: UI Defined and Files on Server text too large in stacks table

---------

Co-authored-by: twalts <t.mwalton@yahoo.com>

* X-Frame-Options

* Add support to generate resource json schema (#1337)

* add support to generate resource json schema

* remove unnecessary renames

---------

Co-authored-by: Maxwell Becker <49575486+mbecker20@users.noreply.github.com>

* generate resource schema

* resources.json

* deploy 2.2.0-dev-8

* fix build.version schema (can be string)

* add resource toml comments

* deploy 2.2.0-dev-9

* use draft 07 for better taplo compat

* deploy 2.2.0-dev-10

* builder fix exported toml server_id -> server

* deploy 2.2.0-dev-11

* bump mogh auth

* deploy 2.2.0-dev-12

* Allow excluding services from global auto update  (#1279)

* add ignore_polling_services for global auto update

* rename ignore_polling_services to auto_update_skip_services and reorganize

* fix stack auto_update check show skip services

* deploy 2.2.0-dev-13

* 2.2.0

* bold "relative to the root of the repo"

---------

Co-authored-by: Andrii Bratanin <20169213+andriibratanin@users.noreply.github.com>
Co-authored-by: John Maguire <contact@johnmaguire.me>
Co-authored-by: Shen Li <dustet@gmail.com>
Co-authored-by: T <github@mail.taylor.media>
Co-authored-by: twalts <t.mwalton@yahoo.com>
Co-authored-by: bytedream <git+github@bytedream.dev>
Co-authored-by: mateuszziolkowski <88323671+mateuszziolkowski@users.noreply.github.com>
2026-05-07 11:19:55 -07:00
John MaguireandGitHub ca01407090 Add per-provider user registration control (#1321)
Add granular registration flags:
- KOMODO_DISABLE_LOCAL_USER_REGISTRATION — blocks local signups,
  hides Sign Up button, while allowing OIDC registration
- KOMODO_DISABLE_OIDC_USER_REGISTRATION — blocks OIDC signups
  while allowing local registration

Both are optional and fall back to the existing
KOMODO_DISABLE_USER_REGISTRATION when not set. This enables the
common pattern of letting your OIDC provider control access while
preventing direct local account creation.

Depends on mogh-lib change adding per-provider registration methods
to the AuthImpl trait.

Closes #1087
2026-05-06 16:06:55 -07:00
Maxwell BeckerandGitHub 20b9d16d4b 2.1.2 (#1347)
* 2.1.2 fix multi file service override UI crash

* deploy 2.1.2-dev-1

* 2.1.2
2026-04-10 11:32:17 -07:00
Maxwell BeckerandGitHub 7c95823eb0 2.1.1 (#1314)
* start 2.1.1 to fix swarm auto updates

* correctly extract image and digest from swarm stacks / deployments

* 2.1.1 fix swarm auto updates not picking up

* fmt
2026-04-02 14:55:44 -07:00
mbecker20 254622078e rust 1.94.1 2026-04-01 16:11:27 -07:00
38421d6719 2.1.0 (#1285)
* fix swarm config content hidden

* 2.0.1-dev-1

* bump ts deps

* align swarm config links

* StackServiceRun command is actually optional

* fix swarm not included in read resource toml

* deploy 2.0.1-dev-2

* logger support disabling timestamps (eg if they are already provided by docker logs)

* full opacity tag color selector

* Add UpdateSwarmNode execution and convert several large repetive blocks to macro handling

* add node update functionality

* deploy 2.0.1-dev-3

* deploy 2.1.0-dev-1

* toml variable export sorted by name

* deploy 2.1.0-dev-2

* dont use detach=false because it can hang indefinitely on misconfigured stacks

* deploy 2.1.0-dev-3

* swarm stack deploy explicitly '--detach=true' to avoid future changes in behavior

* fmt

* UI: Stack config: Fix Add Env File button

* fix swarm error propogation, add server connection error hover message

* deploy 2.1.0-dev-4

* fix service rm (should be stack rm

* fix container ports not displaying when server address not available for link

* fix build registry custom org configuration

* github action: comment out cache step (leads to no space on device)

* fix missing nullish check when selecting stack (#1287)

* fix container selector null crash

* provider selector custom input label adheres to showLabel

* fix: show/hide button non-functional in stack, sync, and build info views (#1267)

Co-authored-by: twalts <t.mwalton@yahoo.com>

* swarm stack support env vars through shell source file method

* tweaks

* bump deps

* deploy 2.1.0-dev-5

* auto update should work with swarm

* deploy 2.1.0-dev-6

* 2.1.0

---------

Co-authored-by: Shen Li <dustet@gmail.com>
Co-authored-by: T <github@mail.taylor.media>
Co-authored-by: twalts <t.mwalton@yahoo.com>
2026-04-01 16:08:29 -07:00
mbecker20 0f96794559 add ghpages CNAME 2026-03-26 13:05:06 -07:00
mbecker20 d79fefff17 add breaking notice about bullseye and periphery compose example with correct key path 2026-03-26 13:02:39 -07:00
mbecker20 4626b4a477 fix deep compare short path for falsy / null 2026-03-25 02:27:46 -07:00
mbecker20 150a8a160d fix terminals page new terminals menu 2026-03-25 01:56:20 -07:00
mbecker20 5b3bb9b8bb Use :2 to stay up to date
with all future Komodo v2 releases.
2026-03-24 18:40:19 -07:00
mbecker20 79831f29af Improved auto updates 2026-03-24 18:38:03 -07:00
mbecker20 b68f5abd25 fix: max width on git commit messages 2026-03-24 17:46:00 -07:00
mbecker20 2325899ebf quick fix swarm config (UI only) 2026-03-24 17:42:09 -07:00
mbecker20 23fd208f0f cli docker alias need :2 in the image 2026-03-24 16:53:45 -07:00
mbecker20 19904e00b9 docs on terminals 2026-03-24 16:48:58 -07:00
mbecker20 8880b1c948 km exec finds server name for connection log 2026-03-24 16:45:33 -07:00
mbecker20 e47c8d2b62 docsite fix privileged typo 2026-03-24 16:17:31 -07:00
mbecker20 1addb8fccd update roadmap with swarm support 2026-03-24 07:04:25 -07:00
08e55b5180 2.0.0 (#889)
* modularize openapi structs

* most of execute openapi

* server / stack exec openapi

* most of write openapi

* more write openapi

* add the write openapi definitions

* fmt and bump mogh auth

* gen types

* remove user api, mogh auth handles api keys

* clean up

* cache latest image digests and use this for image update alerting / auto update

* deploy 2.0.0-dev-108

* deploy 2.0.0-dev-109

* add back legacy Action "exec" method calling convention

* typegen

* improve config quick links styling

* improve config styling a bit more

* deploy 2.0.0-dev-110

* finish version upgrades page, and use prism theme oneLight and oneDark

* stack level terminal page

* fix Action import from terminal

* clean up dangling action api keys on startup

* improve swarm service / stack state inference

* deploy 2.0.0-dev-111

* add api docs link

* bump ts types and fix SwarmConfig type name conflict

* use mogh auth for passkey conversion

* align dashboard icon with sidebar

* bump frontend node version

* bump node version in all the dockerfiles

* fix tailwind config module

* fix require to import

* skip auto update for images with pinned digest

* dev 112

* cleanup

* batch check for updates, add check to table multi select actions

* dev-113

* mogh_server = "1.2.0"

* serve static ui index.html with ETag / no-cache

* check update available against all digests for image

* deploy 2.0.0-dev-114

* bump rust to 1.93.0

* bump mogh server and auth

* configure session allow cross site

* deploy 2.0.0-dev-115

* add new config value to example config

* Stack check for update prefers check against deployed service image

* deploy 2.0.0-dev-116

* only send auto updated alert after verifying the deploy was successful

* 2.0.0 UI (#1220)

* new ui using mantine

* resources page

* prog on resource page

* resources and resource layouts

* confirm button and modal

* tweaks

* update details

* topbar updates

* add skeletons for resource implementations

* add resource tables

* add tags to recents cards

* resource page table scrolling

* table component + tags filter

* export toml

* New Resource button

* Fix update details capture closing

* tweaks

* omni search

* refine config

* config tweaks

* implement more configs / resource selector

* add profile page

* provider / account selectors

* container table page

* build config

* deployment config

* fix deployment build version selector

* fix secrets selector

* resource sync config

* mobile topbar and updates

* update details fz sm

* stack config

* terminals page

* create terminal in prog

* create terminal menu

* finish create terminal menu

* terminal pages working

* stack tabs / info

* add executions

* add server header info

* confirm pubkey modal

* improve resource header styling

* FileSource component

* stack service table, move icons.ts

* basic procedure config

* tweak procedure config

* container / image pages

* network / volume pages

* clean up docker resource pages

*  basic log / terminal ui

* reusable log section

* styling

* clean up resource components

* delete in resource header

* log auto select stderr

* fix some bgs

* stack logs with service selector

* stack terminals

* add deployment executions

* use correct icon

* useResource hooks

* build info

* build info

* tweaks

* server tabs

* fix terminal section target

* prog on server tabs

* server stats

* light theme

* start on historical stats

* stack service page

* resource sync tabs

* sync tabs

* more topbar icons

* add settings basic

* add topbar alerts

* tweak stream selector behavior

* tweak alert icon topbar

* improve styling smaller screen

* schedules page and other progress

* onboarding keys

* improve schedule page descriptions

* improve update notifications

* schedule timezone selector

* tag color selector

* finish settings / providers

* use shared-text-update component so settings tables aren't janky

* updates page

* refine updates page

* alert page

* standardize borders

* theme and swarm

* swarm tabs

* swarm node page

* swarm config page

* swarm pages

* swarm task and secret pages

* swarm stack page

* fix stack log service selector in swarm mode

* standard inspect section

* swarm inspect tab

* server and swarm resources tab

* add disable confirm dialog (modal) option for executions

* stack update available indicator

* deployment update available

* add template switch to resource headers

* ResourceHeader + rename

* set editing name onclick

* repo tabs

* server stats table

* refine a bit

* refine deployment / stack header info

* show server stats dashboard. dashboard tables

* action last run in config

* SettingsUsers page

* user page etc

* manage api key

* user base permissions

* color the table multi select

* user group page

* UserAddUserGroup

* active includes deployments / stacks

* improve small screen view

* fix docker pages execution showing

* clean up

* rename frontend to UI

* align profile page styling

* config maintenance windows

* finish maintenance windows

* builder config

* add batch execute dropdown / confirm menu

* batch execute styling

* deploy 2.0.0-dev-117

* improve stats card light theme

* add update page

* improve mobile

* terminal group nowrap

* mobile improvements

* allow unused again

* improve mobile font sizing

* improve mobile updates / alerts

* mobile tabs

* alert page

* add server version mismatch color

* new resource, clearable selector

* Fix build show info tab

* copy resources

* keyboard shortcuts

* server resource header version mismatch

* fix type errors

* container page server multi select

* confirm button clear timeout

* hash compare force uses first 8 for short hash

* fix log height

* copy webhooks

* responsive tweaks

* add icons to server stat sections

* add historical server stats charts

* server stat current card shows usage numbers

* refine current stats more

* fix shortcuts interfering with monaco brave

* clean up unused

* remove v1 frontend

* bump rust version to 1.93.1 and dep versions

* deploy 2.0.0-dev-118

* bump chef rust version

* improve login no auth configured and passkey pending

* Load Average is first historical stat

* procedure / action webhook branch mobile style

* dashboard active styling

* hide actions when none

* Select Template

* execution buttons disabled when loading

* Fix config input issue

* improve tab styling

* rename ConfigSwitch onChange -> onCheckedChange

* ensure section headers consistent spacing

* edit swarm join command

* fix batch executions width

* stack stopped and deployment exited warning instead of critical

* smaller more consistent gaps

* add close button to update / alert details drawer

* stack and deployment state color include update available. Ensure server version mismatch color applied everywhere

* deploy 2.0.0-dev-120

* topbar user dropdown shows user avatar if available

* post link redirect should be to profile

* deploy 2.0.0-dev-121

* improve profile delete styling

* standard api key modal size

* improve login styling

* fix login github / google icon color

* fix some wrapping stuff in tables and tag text disappear

* fix terminal height - same as logs

* single delete terminal

* Update / Alert table filter selector formatted

* tweaks for lg size

* taller data table and blue omnisearch

* refine lg screen size view

* fix sidebar margin right

* "never" -> "Never"

* Add hoverable disk info

* improve disk usage hover card styling

* rename for clarity

* thinner topbar

* config sidebar save

* setters use maps instead of mutations

* notification green contents written success

* fmt

* read request are trace

* deploy 2.0.0-dev-122

* debug level core <> periphery auth identifiers logs

* mogh auth server 1.2.10

* mogh auth 1.2.11

* deploy 2.0.0-dev-123

* Deploy / DeployStack updates invalidate corresponding list query

* confirm action / save modals don't need ConfirmButton

* deploy 2.0.0-dev-124

* proper base64url decode

* fix init admin user

* improve mobile friendly tabs width, and onboardng key copy

* rename resource invalidates

* better maxheight for mobile friendly tabs

* km cli needs to install crypto provider for tls ws

* better responsive confirm save width

* confirm modal better responsiveness

* Fix api key modal too thin

* better api key create

* improve batch execs

* improve tabs

* sidebar more compact

* add missing Repo header Info

* Fix Pull repo

* fix repo Links config

* improve procedure config UI including run stack service

* improve deployment network, restart, termination signal config

* fix confirm update showing entries which have not changed

* example execute terminal uses bash

* Update deployments description

* move build server

* [Docs] Update connect-servers.mdx (#1256)

* Update connect-servers.mdx

* Update connect-servers.mdx

* clean up connect servers

* feat(ci): build (#1018)

* fmt

* fix: more verbose logging (#1017)

* use .with_context for stack run directory canonicalize log

* fix failing doc test

* Improve server resource header hover info

* service selector support swarm stack icon

* fix stack terminals sometimes not disabled when it should be

* add terminal create and delete messages

* bump packages and add Mogh Tech copyright

* revamp docsite

* soften the borders

* clean up stack config

* fix config group header too much gap

* procedure stage menu easier to reach

* deploy 2.0.0-dev-125

* increase universal resource polling

* improve server stats

* fix data table

* down node is critical

* cli add print core info

* add docker swarm feature card

* improve toml resource repetition using macros, and fix Swarm toml support

* swarm config: configure alerting options

* add swarm header info

* Implement New Swarm Config and New Swarm Secret

* brighten tag colors

* continue docs revamp

* fmt

* set more refetch intervals to keep display data fresh

* fix copy not showing copy source when there are no templates

* rename onboarding key fix_existing_servers to privileged

* fix Privileged spelling

* fmt

* more docs improvement

* improve docs intro

* update the curl instructions with easier call method

* fix clippy lints

* refresh the server cache after every server connection successful login

* deploy 2.0.0-dev-126

* add polling to dashboard summary data

* tweak tag opacity

* improve server stat table disk hover

* fix change historical stat length collapse stats

* KOMODO_DISABLE_INIT_RESOURCES

* stack Project Missing should be red

* Fix files on host stacks showing down after reboot until refresh cache

* silence user level write logs SetLastSeenUpdate and PushRecentlyViewed

* See the v2 migration guide

* Improve api logging using more fields

* deploy 2.0.0-dev-127

* cli create api key on database, can use with docker exec into core container

* deploy 2.0.0-dev-128

* advanced km create api-key options

* create onboarding key with cli

* tweak

* onboarding keys use tag name

* deploy 2.0.0-dev-129

* stack procedure stages can select specific services to apply to (default all services)

* docsite dockerfile

* ./docusaurus.config.ts

* need to yarn build

* fix broken link

* improve alert (dropdown) icons

* fix docs sidebar collapse

* add local search functionality

* docs search nice

* homepage features navigate to docs

* only show build cancel when canCancel

* more confirm button confirmprops red

* fix docsite buttons and list more features

* fix sidebar cmd click opens in new tab (is a link)

* execute withBorder

* cleaner execute section

* swap docker compose and deploy containers

* make docs logo align with app

* better mobile button layout

* linked logins / 2fa confirm red styling

* bump rust version to 1.94.0

* move bollard::secret to bollard::config

* deploy 2.0.0-dev-130

* feat: add compose_cmd_wrapper_include for selective command wrapping (#1124)

- Add compose_cmd_wrapper_include field to StackConfig.
- Fix wrapper placeholder text not displaying in MonacoEditor.

* align configuration by removing bold label stuff

* confirm update monaco readonly

* dockerfile binaries / ui images default to :2

* fmt

* refresh server cache lint

* fmt

* disabling send alerts should also disable in UI

* clean up disabled alerting

* deploy 2.0.0-dev-131

* mogh_pki 1.1.3 safer copy from slice

* fix dev container issues on 2.0.0 (#1238)

* fix node version and yarn build in dev container

* ensure keys directories exist and are writable in dev container

* update dev container  image (necessary to fix compiler error)

* fix CORS error in dev container

* more dev container fixes

KOMODO_SESSION_ALLOW_CROSS_SITE: true needed to properly run on Firefox

* fix devcontainer port

* update config pages to use "Webhooks" consistently and fix acronym casing (#1239)

Co-authored-by: Maxwell Becker <49575486+mbecker20@users.noreply.github.com>

* update local dev setup instructions (#1240)

* example mongo deploy dev

* check binary URL (#1116)

* install script improve failed download binary log

* align cli installer with periphery installer

* improve terminal page create experience

* deploy 2.0.0-dev-132

* improve mobile updates with full size query

* fix some internal table wrapping

* fix schedule expression examples

* Add swarm updates / alerts filter

* filter by update available uses location hash instead of localstorage

* sync commit preserve meta "deploy" and "after"

* UI fixes and tweaks

* deploy 2.0.0-dev-133

* add error to warn log

* Stack / deployment should inherit specific Swarm permissions

* Fix inline span formatting (used in logs / errors)

* refactor resource sync pending deploy for better display

* deploy 2.0.0-dev-134

* improve terminal error handling

* deploy 2.0.0-dev-135

* dedicated docs page for v2

* warning about failing to include init: true

* Limit Periphery IPs in Advanced config

* refine setup guide

* 2.0.0

---------

Co-authored-by: Shlee <github@shl.ee>
Co-authored-by: Yujia Qiao <code@rapiz.me>
Co-authored-by: ChanningHe <52875777+ChanningHe@users.noreply.github.com>
Co-authored-by: Steven Loria <sloria1@pm.me>
Co-authored-by: Andreas Brett <andreasbrett@users.noreply.github.com>
2026-03-24 05:50:10 -07:00
697 changed files with 36706 additions and 24126 deletions
+4 -1
View File
@@ -1,2 +1,5 @@
[alias]
xtask = "run --package xtask --"
[build]
rustflags = ["-Wunused-crate-dependencies"]
rustflags = ["-Wunused-crate-dependencies"]
+9 -3
View File
@@ -1,6 +1,6 @@
services:
dev:
image: mcr.microsoft.com/devcontainers/rust:1-1-bullseye
image: mcr.microsoft.com/devcontainers/rust:1-bookworm
volumes:
# Mount the root folder that contains .git
- ../:/workspace:cached
@@ -10,13 +10,19 @@ services:
- stacks:/etc/komodo/stacks
command: sleep infinity
ports:
- "9121:9121"
- "9120:9120"
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
# ...
@@ -30,4 +36,4 @@ volumes:
data:
repo-cache:
repos:
stacks:
stacks:
+9 -3
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": "20.12.2"
"version": "22.12.0"
},
"ghcr.io/devcontainers-community/features/deno:1": {
@@ -28,7 +28,8 @@
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
9121
5173,
9120
],
// Use 'postCreateCommand' to run commands after the container is created.
@@ -36,11 +37,16 @@
"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"
}
}
+7 -1
View File
@@ -1,3 +1,9 @@
#!/bin/sh
cargo install typeshare-cli
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
+56
View File
@@ -0,0 +1,56 @@
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
Generated
+1911 -981
View File
File diff suppressed because it is too large Load Diff
+57 -53
View File
@@ -3,21 +3,18 @@ resolver = "2"
members = [
"bin/*",
"lib/*",
"xtask",
"client/core/rs",
"client/periphery/rs",
]
[workspace.package]
version = "2.0.0-dev-123"
version = "2.3.0"
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" }
@@ -33,106 +30,113 @@ git = { path = "lib/git" }
# MOGH
slack = { version = "2.0.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
mogh_error = { version = "1.0.3", default-features = false }
mogh_error = { version = "1.0.4", default-features = false }
derive_default_builder = "0.1.8"
async_timing_util = "1.1.0"
mogh_auth_client = "1.2.2"
mogh_auth_server = "1.2.11"
mogh_auth_client = "1.7.0"
mogh_auth_server = "1.7.0"
mogh_secret_file = "1.0.1"
mogh_validations = "1.0.1"
mogh_rate_limit = "1.0.1"
partial_derive2 = "0.4.5"
mongo_indexed = "2.0.2"
partial_derive2 = "0.5.0"
mongo_indexed = "2.1.0"
mogh_resolver = "1.0.0"
mogh_config = "1.0.2"
mogh_logger = "1.3.1"
mogh_server = "1.4.5"
mogh_config = "1.1.0"
mogh_logger = "1.3.4"
mogh_server = "1.5.0"
toml_pretty = "2.0.0"
mogh_cache = "1.1.1"
mogh_pki = "1.1.0"
mungos = "3.2.2"
mogh_cache = "1.1.2"
mogh_pki = "1.1.3"
mungos = "3.7.0"
svi = "1.2.0"
# ASYNC
reqwest = { version = "0.13.2", default-features = false, features = ["json", "stream", "form", "query", "rustls"] }
tokio = { version = "1.50.0", features = ["full"] }
tokio-util = { version = "0.7.18", features = ["io", "codec"] }
tokio-stream = { version = "0.1.18", features = ["sync"] }
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.32"
arc-swap = "1.8.2"
futures-util = "0.3.33"
arc-swap = "1.9.2"
# SERVER
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-native-roots"] }
axum = { version = "0.8.8", features = ["ws", "json", "macros"] }
axum-extra = { version = "0.12.5", features = ["typed-header"] }
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"] }
# OPENAPI
utoipa-scalar = { version = "0.3.0", features = ["axum"] }
utoipa = "5.4.0"
utoipa = "5.5.0"
# SER/DE
ipnetwork = { version = "0.21.1", features = ["serde"] }
indexmap = { version = "2.13.0", features = ["serde"] }
serde = { version = "1.0.227", features = ["derive"] }
indexmap = { version = "2.14.0", features = ["serde"] }
serde = { version = "1.0.229", features = ["derive"] }
strum = { version = "0.28.0", features = ["derive"] }
bson = { version = "2.15.0" } # must keep in sync with mongodb version
toml = "1.0.4"
toml = "1.1.3"
serde_yaml_ng = "0.10.0"
serde_json = "1.0.149"
serde_qs = "1.0.0"
serde_json = "1.0.151"
serde_qs = "1.1.2"
url = "2.5.8"
# ERROR
anyhow = "1.0.102"
thiserror = "2.0.18"
anyhow = "1.0.104"
thiserror = "2.0.19"
# SYSTEM
libc = "0.2.189"
# LOGGING
tracing = "0.1.44"
# CONFIG
clap = { version = "4.5.60", features = ["derive"] }
clap = { version = "4.6.4", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
# CRYPTO / AUTH
uuid = { version = "1.21.0", features = ["v4", "fast-rng", "serde"] }
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
data-encoding = "2.10.0"
uuid = { version = "1.24.0", features = ["v4", "fast-rng", "serde"] }
rustls = { version = "0.23.42", features = ["aws-lc-rs"] }
data-encoding = "2.11.0"
urlencoding = "2.1.3"
bcrypt = "0.19.0"
hmac = "0.12.1"
sha1 = "0.10.6"
sha2 = "0.10.9"
rand = "0.10.0"
bcrypt = "0.19.2"
hmac = "0.13.0"
sha1 = "0.11.0"
sha2 = "0.11.0"
rand = "0.10.2"
hex = "0.4.3"
# SYSTEM
hickory-resolver = "0.25.2"
hickory-resolver = "0.26.1"
portable-pty = "0.9.0"
shell-escape = "0.1.5"
crossterm = "0.29.0"
bollard = "0.20.1"
sysinfo = "0.38.3"
shlex = "1.3.0"
bollard = "0.21.0"
sysinfo = "0.39.6"
shlex = "2.0.1"
# CLOUD
aws-config = "1.8.15"
aws-sdk-ec2 = "1.216.0"
aws-credential-types = "1.2.14"
aws-config = "1.10.0"
aws-sdk-ec2 = "1.241.0"
aws-credential-types = "1.3.0"
## CRON
english-to-cron = "0.1.7"
chrono-tz = "0.10.4"
chrono = "0.4.44"
chrono = "0.4.45"
croner = "3.0.1"
# MISC
async-compression = { version = "0.4.41", features = ["tokio", "gzip"] }
async-compression = { version = "0.4.42", features = ["tokio", "gzip"] }
schemars = { version = "1.2.1", features = ["indexmap2"] }
derive_builder = "0.20.2"
comfy-table = "7.2.2"
typeshare = "1.0.5"
wildcard = "0.3.0"
colored = "3.1.1"
bytes = "1.11.1"
regex = "1.12.3"
bytes = "1.12.1"
regex = "1.13.1"
[profile.release]
strip = "debuginfo"
+1 -1
View File
@@ -1,4 +1,4 @@
const cmd = "km run -y action deploy-komodo-fe-change";
const cmd = "km run -y action deploy-komodo-ui-change";
new Deno.Command("bash", {
args: ["-c", cmd],
}).spawn();
+2 -1
View File
@@ -1,7 +1,7 @@
## Builds the Komodo Core, Periphery, and Util binaries
## for a specific architecture. Requires OpenSSL 3 or later.
FROM rust:1.93.1-bookworm AS builder
FROM rust:1.97.1-bookworm AS builder
RUN cargo install cargo-strip
WORKDIR /builder
@@ -12,6 +12,7 @@ COPY ./client/periphery ./client/periphery
COPY ./bin/core ./bin/core
COPY ./bin/periphery ./bin/periphery
COPY ./bin/cli ./bin/cli
COPY ./xtask ./xtask
# Compile bin
RUN \
+1 -1
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.93.1-bookworm AS chef
FROM lukemathwalker/cargo-chef:latest-rust-1.97.1-bookworm AS chef
WORKDIR /builder
# Plan just the RECIPE to see if things have changed
+2 -1
View File
@@ -3,7 +3,6 @@ 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
@@ -32,7 +31,9 @@ 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
+1 -1
View File
@@ -1,4 +1,4 @@
FROM rust:1.93.1-bullseye AS builder
FROM rust:1.97.1-trixie AS builder
RUN cargo install cargo-strip
WORKDIR /builder
+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:latest
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:2
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:bullseye-slim
FROM debian:trixie-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:latest
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:2
# This is required to work with COPY --from
FROM ${BINARIES_IMAGE} AS binaries
+25 -27
View File
@@ -7,16 +7,16 @@ use futures_util::{
FutureExt, TryStreamExt, stream::FuturesUnordered,
};
use komodo_client::{
api::read::{
InspectDockerContainer, ListAllDockerContainers, ListServers,
},
api::read::{self, ListAllContainers, ListServers},
entities::{
config::cli::args::container::{
Container, ContainerCommand, InspectContainer,
},
docker::{
self,
container::{ContainerListItem, ContainerStateStatusEnum},
container::{
ContainerListItem, ContainerSortBy, ContainerStateStatusEnum,
},
},
},
};
@@ -44,11 +44,12 @@ async fn list_containers(
down,
links,
reverse,
containers: names,
containers: terms,
images,
networks,
servers,
format,
page,
command: _,
}: &Container,
) -> anyhow::Result<()> {
@@ -60,9 +61,16 @@ async fn list_containers(
.into_iter()
.map(|s| (s.id.clone(), s))
.collect::<HashMap<_, _>>())),
client.read(ListAllDockerContainers {
client.read(ListAllContainers {
servers: Default::default(),
containers: 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,
}),
)?;
@@ -78,7 +86,6 @@ 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);
@@ -105,7 +112,6 @@ async fn list_containers(
);
state_check
&& network_check
&& matches_wildcards(&names, &[c.name.as_str()])
&& matches_wildcards(
&servers,
&server_name
@@ -145,9 +151,15 @@ pub async fn inspect_container(
.into_iter()
.map(|s| (s.id.clone(), s))
.collect::<HashMap<_, _>>())),
client.read(ListAllDockerContainers {
servers: Default::default(),
containers: Default::default()
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,
}),
)?;
@@ -162,25 +174,11 @@ 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(InspectDockerContainer {
.read(read::InspectContainer {
container: c.name,
server: c.server_id.context("No server...")?,
})
+13
View File
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,101 @@
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
@@ -0,0 +1,13 @@
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
}
}
}
@@ -0,0 +1,46 @@
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(())
}
+133 -550
View File
@@ -28,558 +28,141 @@ pub async fn handle(
}
println!("\n{}: Execution", "Mode".dimmed());
match execution {
Execution::None(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunAction(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchRunAction(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunProcedure(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchRunProcedure(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchRunBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CancelBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::Deploy(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeploy(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PullDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RestartDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PauseDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::UnpauseDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StopDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DestroyDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDestroyDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CloneRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchCloneRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PullRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchPullRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BuildRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchBuildRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CancelRepoBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RestartContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PauseContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::UnpauseContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StopContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DestroyContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartAllContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RestartAllContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PauseAllContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::UnpauseAllContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StopAllContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeleteNetwork(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneNetworks(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeleteImage(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneImages(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeleteVolume(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneVolumes(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneDockerBuilders(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneBuildx(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneSystem(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunSync(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CommitSync(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeployStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeployStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeployStackIfChanged(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeployStackIfChanged(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PullStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchPullStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RestartStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PauseStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::UnpauseStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StopStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunStackService(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::TestAlerter(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::SendAlert(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RemoveSwarmNodes(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RemoveSwarmStacks(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RemoveSwarmServices(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CreateSwarmConfig(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RotateSwarmConfig(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RemoveSwarmConfigs(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CreateSwarmSecret(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RotateSwarmSecret(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RemoveSwarmSecrets(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::ClearRepoCache(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BackupCoreDatabase(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::GlobalAutoUpdate(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RotateAllServerKeys(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RotateCoreKeys(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::Sleep(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
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!(),
}
}};
}
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!(),
};
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,
],
);
match res {
Ok(ExecutionResult::Single(update)) => {
+193 -31
View File
@@ -7,7 +7,8 @@ use komodo_client::{
api::read::{
ListActions, ListAlerters, ListBuilders, ListBuilds,
ListDeployments, ListProcedures, ListRepos, ListResourceSyncs,
ListSchedules, ListServers, ListStacks, ListTags, ListTerminals,
ListSchedules, ListServers, ListStacks, ListSwarms, ListTags,
ListTerminals,
},
entities::{
ResourceTargetVariant,
@@ -31,6 +32,7 @@ use komodo_client::{
schedule::Schedule,
server::{ServerListItem, ServerListItemInfo, ServerState},
stack::{StackListItem, StackListItemInfo, StackState},
swarm::{SwarmListItem, SwarmListItemInfo, SwarmState},
sync::{
ResourceSyncListItem, ResourceSyncListItemInfo,
ResourceSyncState,
@@ -52,28 +54,46 @@ 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, false).await
list_resources::<ServerListItem>(filters, list.page, false)
.await
}
Some(ListCommand::Swarms(filters)) => {
list_resources::<SwarmListItem>(filters, list.page, false).await
}
Some(ListCommand::Stacks(filters)) => {
list_resources::<StackListItem>(filters, false).await
list_resources::<StackListItem>(filters, list.page, false).await
}
Some(ListCommand::Deployments(filters)) => {
list_resources::<DeploymentListItem>(filters, false).await
list_resources::<DeploymentListItem>(filters, list.page, false)
.await
}
Some(ListCommand::Builds(filters)) => {
list_resources::<BuildListItem>(filters, false).await
list_resources::<BuildListItem>(filters, list.page, false).await
}
Some(ListCommand::Repos(filters)) => {
list_resources::<RepoListItem>(filters, false).await
list_resources::<RepoListItem>(filters, list.page, false).await
}
Some(ListCommand::Procedures(filters)) => {
list_resources::<ProcedureListItem>(filters, false).await
list_resources::<ProcedureListItem>(filters, list.page, false)
.await
}
Some(ListCommand::Actions(filters)) => {
list_resources::<ActionListItem>(filters, false).await
list_resources::<ActionListItem>(filters, list.page, false)
.await
}
Some(ListCommand::Syncs(filters)) => {
list_resources::<ResourceSyncListItem>(filters, false).await
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
}
Some(ListCommand::Terminals(filters)) => {
list_terminals(filters).await
@@ -81,12 +101,6 @@ 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
}
}
}
@@ -97,6 +111,7 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> {
let (
tags,
mut servers,
mut swarms,
mut stacks,
mut deployments,
mut builds,
@@ -109,14 +124,15 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> {
.into_iter()
.map(|t| (t.id, t.name))
.collect::<HashMap<_, _>>())),
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),
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),
)?;
if !servers.is_empty() {
@@ -125,6 +141,12 @@ 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)?;
@@ -172,6 +194,7 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> {
async fn list_resources<T>(
filters: &ResourceFilters,
page: u64,
minimal: bool,
) -> anyhow::Result<()>
where
@@ -180,7 +203,7 @@ where
{
let client = crate::command::komodo_client().await?;
let (mut resources, tags) = tokio::try_join!(
T::list(client, filters, minimal),
T::list(client, filters, page, minimal),
client.read(ListTags::default()).map(|res| res.map(|res| res
.into_iter()
.map(|t| (t.id, t.name))
@@ -205,6 +228,8 @@ async fn list_terminals(
.read(ListTerminals {
target: None,
use_names: true,
limit: Some(0),
..Default::default()
})
.await?;
if !terminals.is_empty() {
@@ -222,6 +247,8 @@ 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()
@@ -279,6 +306,7 @@ where
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
// For use with root `km ls`
minimal: bool,
) -> anyhow::Result<Vec<ResourceListItem<Self::Info>>>;
@@ -291,6 +319,7 @@ impl ListResources for ServerListItem {
async fn list(
client: &KomodoClient,
filters: &ResourceFilters,
page: u64,
_minimal: bool,
) -> anyhow::Result<Vec<Self>> {
let servers = client
@@ -300,6 +329,10 @@ 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);
@@ -329,17 +362,68 @@ 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()
@@ -351,6 +435,10 @@ 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| {
@@ -406,12 +494,15 @@ 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()
@@ -423,6 +514,10 @@ 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| {
@@ -479,12 +574,15 @@ 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()
@@ -496,6 +594,10 @@ 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| {
@@ -533,10 +635,7 @@ impl ListResources for BuildListItem {
})
.collect::<Vec<_>>();
builds.sort_by(|a, b| {
a.name
.cmp(&b.name)
.then(a.info.builder_id.cmp(&b.info.builder_id))
.then(a.info.state.cmp(&b.info.state))
a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))
});
Ok(builds)
}
@@ -547,6 +646,7 @@ 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);
@@ -557,6 +657,10 @@ 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()
@@ -595,6 +699,7 @@ 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);
@@ -605,6 +710,10 @@ 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()
@@ -632,7 +741,7 @@ impl ListResources for ProcedureListItem {
(Some(a), Some(b)) => return a.cmp(&b),
(None, None) => {}
}
a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))
a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))
});
Ok(procedures)
}
@@ -643,6 +752,7 @@ 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);
@@ -653,6 +763,10 @@ 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()
@@ -680,7 +794,7 @@ impl ListResources for ActionListItem {
(Some(a), Some(b)) => return a.cmp(&b),
(None, None) => {}
}
a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))
a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))
});
Ok(actions)
}
@@ -691,6 +805,7 @@ 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);
@@ -701,6 +816,10 @@ 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()
@@ -725,7 +844,7 @@ impl ListResources for ResourceSyncListItem {
})
.collect::<Vec<_>>();
syncs.sort_by(|a, b| {
a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))
a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))
});
Ok(syncs)
}
@@ -736,6 +855,7 @@ 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);
@@ -746,6 +866,10 @@ 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()
@@ -768,6 +892,7 @@ 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);
@@ -778,6 +903,10 @@ 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()
@@ -832,6 +961,39 @@ 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
View File
@@ -15,6 +15,8 @@ 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;
+99 -44
View File
@@ -2,7 +2,7 @@ use anyhow::{Context, anyhow};
use colored::Colorize;
use komodo_client::{
api::{
read::{ListAllDockerContainers, ListServers},
read::{GetServer, ListAllContainers, ListServers},
terminal::InitTerminal,
},
entities::{
@@ -26,7 +26,7 @@ pub async fn handle_connect(
recreate,
}: &Connect,
) -> anyhow::Result<()> {
handle_terminal_forwarding(async {
handle_terminal_forwarding(server, async {
super::komodo_client()
.await?
.connect_server_terminal(
@@ -56,25 +56,28 @@ pub async fn handle_exec(
}: &Exec,
) -> anyhow::Result<()> {
let server = get_server(server.clone(), container).await?;
handle_terminal_forwarding(async {
super::komodo_client()
.await?
.connect_container_terminal(
server,
container.to_string(),
None,
Some(InitTerminal {
command: Some(shell.to_string()),
recreate: if *recreate {
TerminalRecreateMode::Always
} else {
TerminalRecreateMode::DifferentCommand
},
mode: Some(ContainerTerminalMode::Exec),
}),
)
.await
})
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
},
)
.await
}
@@ -86,25 +89,28 @@ pub async fn handle_attach(
}: &Attach,
) -> anyhow::Result<()> {
let server = get_server(server.clone(), container).await?;
handle_terminal_forwarding(async {
super::komodo_client()
.await?
.connect_container_terminal(
server,
container.to_string(),
None,
Some(InitTerminal {
command: None,
recreate: if *recreate {
TerminalRecreateMode::Always
} else {
TerminalRecreateMode::DifferentCommand
},
mode: Some(ContainerTerminalMode::Attach),
}),
)
.await
})
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
},
)
.await
}
@@ -112,6 +118,8 @@ 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);
}
@@ -119,9 +127,15 @@ async fn get_server(
let client = super::komodo_client().await?;
let mut containers = client
.read(ListAllDockerContainers {
.read(ListAllContainers {
servers: Default::default(),
containers: vec![container.to_string()],
tags: Default::default(),
terms: vec![container.to_string()],
state: Default::default(),
limit: Some(0),
page: 0,
sort_by: Default::default(),
sort_desc: false,
})
.await?;
@@ -132,11 +146,14 @@ async fn get_server(
}
if containers.len() == 1 {
return containers
let server_id = containers
.pop()
.context("Shouldn't happen")?
.server_id
.context("Container doesn't have server_id");
.context("Container doesn't have server_id")?;
let server_name =
client.read(GetServer { server: server_id }).await?.name;
return Ok(server_name);
}
let servers = containers
@@ -147,6 +164,8 @@ async fn get_server(
let servers = client
.read(ListServers {
query: ServerQuery::builder().names(servers).build(),
limit: Some(0),
..Default::default()
})
.await?
.into_iter()
@@ -163,6 +182,7 @@ 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
@@ -247,6 +267,16 @@ 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
{
@@ -297,6 +327,31 @@ 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,6 +264,9 @@ 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,6 +12,9 @@ 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();
@@ -42,6 +45,7 @@ async fn app() -> anyhow::Result<()> {
}
Ok(())
}
args::Command::CoreInfo => command::core_info::handle().await,
args::Command::Container(container) => {
command::container::handle(container).await
}
@@ -52,6 +56,9 @@ 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,7 +2,6 @@
name = "komodo_core"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
+4 -2
View File
@@ -1,7 +1,7 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
# Build Core
FROM rust:1.93.1-trixie AS core-builder
FROM rust:1.97.1-trixie AS core-builder
RUN cargo install cargo-strip
WORKDIR /builder
@@ -11,6 +11,7 @@ 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
# Compile app
RUN cargo build -p komodo_core --release && \
@@ -58,7 +59,8 @@ ENV KOMODO_CLI_CONFIG_PATHS="/config"
# This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*`
ENV KOMODO_CLI_CONFIG_KEYWORDS="*config.*,*komodo.cli*.*"
CMD [ "/bin/bash", "-c", "update-ca-certificates && core" ]
ENTRYPOINT [ "entrypoint.sh" ]
CMD [ "core" ]
# Label to prevent Komodo from stopping with StopAllContainers
LABEL komodo.skip="true"
+2 -2
View File
@@ -2,8 +2,8 @@
## Sets up the necessary runtime container dependencies for Komodo Core.
## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
ARG UI_IMAGE=ghcr.io/moghtech/komodo-ui:latest
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:2
ARG UI_IMAGE=ghcr.io/moghtech/komodo-ui:2
ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64
ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64
+1 -1
View File
@@ -1,7 +1,7 @@
## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).
## Sets up the necessary runtime container dependencies for Komodo Core.
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:2
# This is required to work with COPY --from
FROM ${BINARIES_IMAGE} AS binaries
+32 -30
View File
@@ -20,12 +20,14 @@ 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}"
@@ -34,28 +36,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::ServerUnreachable {
id,
name,
@@ -82,6 +62,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::ServerCpu {
id,
name,
@@ -140,7 +142,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
format!("")
String::new()
};
format!(
"📦 Deployment **{name}** is now **{to}**{target}\nprevious: **{from}**\n{link}"
@@ -161,7 +163,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
format!("")
String::new()
};
format!(
"⬆ Deployment **{name}** has an update available{target}\nimage: **{image}**\n{link}"
@@ -182,7 +184,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
format!("")
String::new()
};
format!(
"⬆ Deployment **{name}** was updated automatically ⏫{target}\nimage: **{image}**\n{link}"
@@ -205,7 +207,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
format!("")
String::new()
};
format!(
"🥞 Stack **{name}** is now {to}{target}\nprevious: **{from}**\n{link}"
@@ -227,7 +229,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
format!("")
String::new()
};
format!(
"⬆ Stack **{name}** has an update available{target}\nservice: **{service}**\nimage: **{image}**\n{link}"
@@ -251,7 +253,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("\nserver: **{server}**")
} else {
format!("")
String::new()
};
format!(
"⬆ Stack **{name}** was updated automatically ⏫{target}\n{images_label}: **{images}**\n{link}"
+32 -30
View File
@@ -260,35 +260,15 @@ 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}")
format!(
"{level} | Swarm {name} is unhealthy ❌\n{link}{err}"
)
}
_ => unreachable!(),
}
}
AlertData::ServerVersionMismatch {
id,
name,
region,
server_version,
core_version,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | {name}{region} | Periphery version now matches Core version ✅\n{link}"
)
}
_ => {
format!(
"{level} | {name}{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}\n{link}"
)
}
}
}
AlertData::ServerUnreachable {
id,
name,
@@ -313,6 +293,28 @@ fn standard_alert_content(alert: &Alert) -> String {
_ => 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,
@@ -371,7 +373,7 @@ fn standard_alert_content(alert: &Alert) -> String {
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
format!("")
String::new()
};
format!(
"📦Deployment {name} is now {to_state}{target}\nprevious: {from}\n{link}",
@@ -392,7 +394,7 @@ fn standard_alert_content(alert: &Alert) -> String {
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
format!("")
String::new()
};
format!(
"⬆ Deployment {name} has an update available{target}\nimage: {image}\n{link}",
@@ -413,7 +415,7 @@ fn standard_alert_content(alert: &Alert) -> String {
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
format!("")
String::new()
};
format!(
"⬆ Deployment {name} was updated automatically{target}\nimage: {image}\n{link}",
@@ -436,7 +438,7 @@ fn standard_alert_content(alert: &Alert) -> String {
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
format!("")
String::new()
};
format!(
"🥞 Stack {name} is now {to_state}{target}\nprevious: {from}\n{link}",
@@ -458,7 +460,7 @@ fn standard_alert_content(alert: &Alert) -> String {
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
format!("")
String::new()
};
format!(
"⬆ Stack {name} has an update available{target}\nservice: {service}\nimage: {image}\n{link}",
@@ -482,7 +484,7 @@ fn standard_alert_content(alert: &Alert) -> String {
} else if let Some(server) = server_name {
format!("\nserver: {server}")
} else {
format!("")
String::new()
};
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,35 +59,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::ServerUnreachable {
id,
name,
@@ -129,6 +100,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::ServerCpu {
id,
name,
@@ -289,7 +289,7 @@ pub async fn send_alert(
} else if let Some(server) = server_name {
format!("server: *{server}*\n")
} else {
format!("")
String::new()
};
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 {
format!("")
String::new()
};
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 {
format!("")
String::new()
};
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 {
format!("")
String::new()
};
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 {
format!("")
String::new()
};
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 {
format!("")
String::new()
};
let blocks = vec![
Block::header(text.clone()),
+105 -18
View File
@@ -4,23 +4,26 @@ use std::{
sync::OnceLock,
};
use anyhow::Context;
use command::run_komodo_standard_command;
use database::mungos::{
by_id::update_one_by_id, mongodb::bson::to_document,
use 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 interpolate::Interpolator;
use komodo_client::{
api::execute::{BatchExecutionResponse, BatchRunAction, RunAction},
api::execute::{
BatchExecutionResponse, BatchRunAction, CancelAction, RunAction,
},
entities::{
FileFormat, JsonObject,
FileFormat, JsonObject, Operation,
action::Action,
alert::{Alert, AlertData, SeverityLevel},
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
random_string,
update::Update,
update::{Update, UpdateStatus},
user::action_user,
},
parsers::parse_key_value_list,
@@ -34,6 +37,7 @@ 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,
@@ -46,7 +50,7 @@ use crate::{
},
permission::get_check_permissions,
resource::refresh_action_state_cache,
state::{action_states, db_client},
state::{action_cancel_cache, action_states, db_client},
};
use super::ExecuteArgs;
@@ -66,18 +70,23 @@ impl Resolve<ExecuteArgs> for BatchRunAction {
"BatchRunAction",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunAction>(&self.pattern, user)
.await?,
super::batch_execute::<BatchRunAction>(
&self.pattern,
self.tags,
user,
)
.await?,
)
}
}
@@ -87,7 +96,7 @@ impl Resolve<ExecuteArgs> for RunAction {
"RunAction",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
action = self.action,
@@ -95,7 +104,11 @@ impl Resolve<ExecuteArgs> for RunAction {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let mut action = get_check_permissions::<Action>(
&self.action,
@@ -112,7 +125,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,
@@ -193,14 +206,20 @@ 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;
@@ -214,7 +233,7 @@ impl Resolve<ExecuteArgs> for RunAction {
update.logs.push(res);
update.finalize();
mogh_error::Ok(update)
mogh_error::Ok(())
}
.await;
@@ -227,7 +246,13 @@ impl Resolve<ExecuteArgs> for RunAction {
);
};
let update = res?;
action_cancel_cache().remove(&update.id).await;
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
res?;
// Need to manually update the update before cache refresh,
// and before broadcast with update_update.
@@ -455,3 +480,65 @@ 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)
}
}
+14 -4
View File
@@ -28,7 +28,7 @@ impl Resolve<ExecuteArgs> for TestAlerter {
"TestAlerter",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
alerter = self.alerter,
@@ -36,7 +36,11 @@ impl Resolve<ExecuteArgs> for TestAlerter {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let alerter = get_check_permissions::<Alerter>(
&self.alerter,
@@ -94,7 +98,7 @@ impl Resolve<ExecuteArgs> for SendAlert {
"SendAlert",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
request = format!("{self:?}"),
@@ -102,10 +106,16 @@ impl Resolve<ExecuteArgs> for SendAlert {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let alerters = list_full_for_user::<Alerter>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
+55 -34
View File
@@ -28,7 +28,7 @@ use komodo_client::{
alert::{Alert, AlertData, SeverityLevel},
all_logs_success,
build::{Build, BuildConfig},
builder::{Builder, BuilderConfig},
builder::Builder,
deployment::DeploymentState,
komodo_timestamp, optional_string,
permission::PermissionLevel,
@@ -75,18 +75,23 @@ impl Resolve<ExecuteArgs> for BatchRunBuild {
"BatchRunBuild",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunBuild>(&self.pattern, user)
.await?,
super::batch_execute::<BatchRunBuild>(
&self.pattern,
self.tags,
user,
)
.await?,
)
}
}
@@ -96,7 +101,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
"RunBuild",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
build = self.build,
@@ -104,7 +109,11 @@ impl Resolve<ExecuteArgs> for RunBuild {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let mut build = get_check_permissions::<Build>(
&self.build,
@@ -144,7 +153,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 {
@@ -171,9 +180,6 @@ 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 {
@@ -182,19 +188,13 @@ impl Resolve<ExecuteArgs> for RunBuild {
id = cancel_recv.recv() => id?
};
if incoming_build_id == build_id {
if is_server_builder {
update.push_error_log("Cancel acknowledged", "Build cancellation is not possible on server builders at this time. Use an AWS builder to enable this feature.");
} else {
update.push_simple_log("Cancel acknowledged", "The build cancellation has been queued, it may still take some time.");
}
update.push_simple_log("Cancel acknowledged", "The build cancellation has been queued, it may still take some time.");
update.finalize();
let id = update.id.clone();
if let Err(e) = update_update(update).await {
warn!("Failed to modify Update {id} on db | {e:#}");
}
if !is_server_builder {
cancel_clone.cancel();
}
cancel_clone.cancel();
return Ok(());
}
}
@@ -212,7 +212,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
build.name.clone(),
Some(build.config.version),
builder,
&mut update,
Some(&mut update),
)
.await
{
@@ -269,11 +269,11 @@ impl Resolve<ExecuteArgs> for RunBuild {
replacers: Default::default(),
}) => res,
_ = cancel.cancelled() => {
debug!("Build cancelled during clone, cleaning up builder");
update.push_error_log("Build cancelled", String::from("user cancelled build during repo clone"));
debug!("Build cancelled during repo clone, cleaning up builder");
update.push_error_log("Build cancelled", String::from("Build cancelled during repo clone"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
info!("Builder cleaned up");
debug!("Builder cleaned up");
return handle_early_return(update, build.id, build.name, true).await
},
};
@@ -319,7 +319,14 @@ impl Resolve<ExecuteArgs> for RunBuild {
}) => res.context("Failed at call to Periphery to build"),
_ = cancel.cancelled() => {
info!("Build cancelled during build, cleaning up builder");
update.push_error_log("Build cancelled", String::from("User cancelled build during docker build"));
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"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
return handle_early_return(update, build.id, build.name, true).await
@@ -369,6 +376,10 @@ 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,
@@ -505,10 +516,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")),
_ => {}
@@ -522,7 +533,7 @@ impl Resolve<ExecuteArgs> for CancelBuild {
"CancelBuild",
skip(user, update),
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
build = self.build,
@@ -530,7 +541,11 @@ impl Resolve<ExecuteArgs> for CancelBuild {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let build = get_check_permissions::<Build>(
&self.build,
@@ -553,8 +568,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?;
@@ -609,7 +624,13 @@ async fn handle_post_build_redeploy(build_id: &str) {
let state = get_deployment_state(&deployment.id)
.await
.unwrap_or_default();
if state == DeploymentState::Running {
if ![
DeploymentState::NotDeployed,
DeploymentState::Exited,
DeploymentState::Unknown,
]
.contains(&state)
{
let req = super::ExecuteRequest::Deploy(Deploy {
deployment: deployment.id.clone(),
stop_signal: None,
@@ -626,7 +647,7 @@ async fn handle_post_build_redeploy(build_id: &str) {
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
task_id: Uuid::new_v4(),
})
.await
}
+156 -61
View File
@@ -32,7 +32,7 @@ use crate::{
swarm::swarm_request,
update::update_update,
},
monitor::{update_cache_for_server, update_cache_for_swarm},
monitor::{refresh_server_cache, refresh_swarm_cache},
resource::{self, setup_deployment_execution},
state::action_states,
};
@@ -55,18 +55,23 @@ impl Resolve<ExecuteArgs> for BatchDeploy {
"BatchDeploy",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeploy>(&self.pattern, user)
.await?,
super::batch_execute::<BatchDeploy>(
&self.pattern,
self.tags,
user,
)
.await?,
)
}
}
@@ -76,7 +81,7 @@ impl Resolve<ExecuteArgs> for Deploy {
"Deploy",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -86,7 +91,11 @@ impl Resolve<ExecuteArgs> for Deploy {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (mut deployment, swarm_or_server) =
setup_deployment_execution(
@@ -106,7 +115,7 @@ 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();
@@ -118,26 +127,16 @@ impl Resolve<ExecuteArgs> for Deploy {
let (version, registry_token) = match &deployment.config.image {
DeploymentImage::Build { build_id, version } => {
let build = resource::get::<Build>(build_id).await?;
let image_names = build.get_image_names();
let image_name = image_names
.first()
.context("No image name could be created")
.context("Failed to create image name")?;
let version = if version.is_none() {
build.config.version
} else {
*version
};
let version_str = version.to_string();
// Potentially add the build image_tag postfix
let version_str = if build.config.image_tag.is_empty() {
version_str
} else {
format!("{version_str}-{}", build.config.image_tag)
};
let image_name = build.get_deployment_image_name();
let image_tag = build.get_deployment_image_tag(version);
// replace image with corresponding build image.
deployment.config.image = DeploymentImage::Image {
image: format!("{image_name}:{version_str}"),
image: format!("{image_name}:{image_tag}"),
};
let first_registry = build
.config
@@ -207,6 +206,12 @@ 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!(),
@@ -222,7 +227,8 @@ impl Resolve<ExecuteArgs> for Deploy {
.await
{
Ok(logs) => {
update_cache_for_swarm(&swarm, true).await;
refresh_swarm_cache(&swarm, true).await;
deployed = logs.iter().all(|log| log.success);
update.logs.extend(logs)
}
Err(e) => {
@@ -246,7 +252,8 @@ impl Resolve<ExecuteArgs> for Deploy {
.await
{
Ok(log) => {
update_cache_for_server(&server, true).await;
refresh_server_cache(&server, true).await;
deployed = log.success;
update.logs.push(log)
}
Err(e) => {
@@ -263,6 +270,11 @@ impl Resolve<ExecuteArgs> for Deploy {
&deployment_id,
&DeploymentInfo {
latest_image_digest: Default::default(),
deployed_name: if deployed {
fresh_name
} else {
prev_deployed_name
},
},
)
.await
@@ -274,6 +286,11 @@ 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)
@@ -401,7 +418,7 @@ pub async fn pull_deployment_inner(
Err(e) => Log::error("Pull image", format_serror(&e.into())),
};
update_cache_for_server(server, true).await;
refresh_server_cache(server, true).await;
anyhow::Ok(log)
}
.await;
@@ -418,7 +435,7 @@ impl Resolve<ExecuteArgs> for PullDeployment {
"PullDeployment",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -426,7 +443,11 @@ impl Resolve<ExecuteArgs> for PullDeployment {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
@@ -450,7 +471,7 @@ 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();
@@ -461,6 +482,10 @@ impl Resolve<ExecuteArgs> for PullDeployment {
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)
@@ -472,7 +497,7 @@ impl Resolve<ExecuteArgs> for StartDeployment {
"StartDeployment",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -480,7 +505,11 @@ impl Resolve<ExecuteArgs> for StartDeployment {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
@@ -504,7 +533,7 @@ 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();
@@ -515,7 +544,7 @@ impl Resolve<ExecuteArgs> for StartDeployment {
let log = match periphery_client(&server)
.await?
.request(api::container::StartContainer {
name: deployment.name,
name: deployment.deployed_name().to_string(),
})
.await
{
@@ -527,8 +556,12 @@ impl Resolve<ExecuteArgs> for StartDeployment {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -540,7 +573,7 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
"RestartDeployment",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -548,7 +581,11 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
@@ -572,7 +609,7 @@ 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();
@@ -583,7 +620,7 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
let log = match periphery_client(&server)
.await?
.request(api::container::RestartContainer {
name: deployment.name,
name: deployment.deployed_name().to_string(),
})
.await
{
@@ -597,8 +634,12 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -610,7 +651,7 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
"PauseDeployment",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -618,7 +659,11 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
@@ -642,7 +687,7 @@ 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();
@@ -653,7 +698,7 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
let log = match periphery_client(&server)
.await?
.request(api::container::PauseContainer {
name: deployment.name,
name: deployment.deployed_name().to_string(),
})
.await
{
@@ -665,8 +710,12 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -678,7 +727,7 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
"UnpauseDeployment",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -686,7 +735,11 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
@@ -710,7 +763,7 @@ 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();
@@ -721,7 +774,7 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
let log = match periphery_client(&server)
.await?
.request(api::container::UnpauseContainer {
name: deployment.name,
name: deployment.deployed_name().to_string(),
})
.await
{
@@ -735,8 +788,12 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -748,7 +805,7 @@ impl Resolve<ExecuteArgs> for StopDeployment {
"StopDeployment",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -758,7 +815,11 @@ impl Resolve<ExecuteArgs> for StopDeployment {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
@@ -782,7 +843,7 @@ 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();
@@ -793,7 +854,7 @@ impl Resolve<ExecuteArgs> for StopDeployment {
let log = match periphery_client(&server)
.await?
.request(api::container::StopContainer {
name: deployment.name,
name: deployment.deployed_name().to_string(),
signal: self
.signal
.unwrap_or(deployment.config.termination_signal)
@@ -813,8 +874,12 @@ impl Resolve<ExecuteArgs> for StopDeployment {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -837,18 +902,20 @@ impl Resolve<ExecuteArgs> for BatchDestroyDeployment {
"BatchDestroyDeployment",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDestroyDeployment>(
&self.pattern,
self.tags,
user,
)
.await?,
@@ -861,7 +928,7 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
"DestroyDeployment",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
deployment = self.deployment,
@@ -871,7 +938,11 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (deployment, swarm_or_server) = setup_deployment_execution(
&self.deployment,
@@ -890,7 +961,7 @@ 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();
@@ -904,13 +975,13 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
match swarm_request(
&swarm.config.server_ids,
api::swarm::RemoveSwarmServices {
services: vec![deployment.name],
services: vec![deployment.deployed_name().to_string()],
},
)
.await
{
Ok(log) => {
update_cache_for_swarm(&swarm, true).await;
refresh_swarm_cache(&swarm, true).await;
log
}
Err(e) => Log::error(
@@ -925,7 +996,7 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
match periphery_client(&server)
.await?
.request(api::container::RemoveContainer {
name: deployment.name,
name: deployment.deployed_name().to_string(),
signal: self
.signal
.unwrap_or(deployment.config.termination_signal)
@@ -938,7 +1009,7 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
.await
{
Ok(log) => {
update_cache_for_server(&server, true).await;
refresh_server_cache(&server, true).await;
log
}
Err(e) => Log::error(
@@ -951,9 +1022,33 @@ 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)
+113 -51
View File
@@ -1,7 +1,7 @@
use std::{fmt::Write as _, sync::OnceLock};
use anyhow::{Context, anyhow};
use command::run_komodo_standard_command;
use command::{CommandOptions, 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,
stack::StackState, swarm::SwarmState,
},
};
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,
stack_status_cache, swarm_status_cache,
},
};
@@ -54,14 +54,18 @@ impl Resolve<ExecuteArgs> for ClearRepoCache {
"ClearRepoCache",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id
)
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
if !user.admin {
return Err(
@@ -133,14 +137,18 @@ impl Resolve<ExecuteArgs> for BackupCoreDatabase {
"BackupCoreDatabase",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
if !user.admin {
return Err(
@@ -159,8 +167,8 @@ impl Resolve<ExecuteArgs> for BackupCoreDatabase {
let res = run_komodo_standard_command(
"Backup Core Database",
None,
"km database backup --yes",
CommandOptions::default(),
)
.await;
@@ -186,14 +194,18 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
"GlobalAutoUpdate",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
if !user.admin {
return Err(
@@ -232,6 +244,7 @@ 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]
@@ -243,7 +256,7 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
continue;
};
// Only pull running stacks.
// Only check running stacks.
if !matches!(status.curr.state, StackState::Running) {
continue;
}
@@ -255,43 +268,58 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
&servers,
)?;
if let SwarmOrServer::None = &swarm_or_server {
continue;
// 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 Some(server) =
servers.iter().find(|s| s.id == stack.config.server_id)
// This check is probably redundant along with running check
// but shouldn't hurt
&& server_status_cache
.get(&server.id)
.await
.map(|s| matches!(s.state, ServerState::Ok))
.unwrap_or_default()
if let Err(e) = check_stack_for_update_inner(
stack.id,
&swarm_or_server,
self.skip_auto_update,
true,
false,
)
.await
{
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.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)
));
}
}
@@ -308,7 +336,7 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
continue;
};
// Only pull running deployments.
// Only check running deployments.
if !matches!(status.curr.state, DeploymentState::Running) {
continue;
}
@@ -320,8 +348,34 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
&servers,
)?;
if let SwarmOrServer::None = &swarm_or_server {
continue;
// 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;
}
}
}
let name = deployment.name.clone();
@@ -368,14 +422,18 @@ impl Resolve<ExecuteArgs> for RotateAllServerKeys {
"RotateAllServerKeys",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
)
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
if !user.admin {
return Err(
@@ -482,7 +540,7 @@ impl Resolve<ExecuteArgs> for RotateCoreKeys {
"RotateCoreKeys",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
force = self.force,
@@ -490,7 +548,11 @@ impl Resolve<ExecuteArgs> for RotateCoreKeys {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
if !user.admin {
return Err(
+73 -24
View File
@@ -13,6 +13,7 @@ use komodo_client::{
entities::{
Operation,
permission::PermissionLevel,
resource::ResourceQuery,
update::{Log, Update},
user::User,
},
@@ -29,7 +30,10 @@ use uuid::Uuid;
use crate::{
auth::KomodoAuthImpl,
helpers::update::{init_execution_update, update_update},
helpers::{
query::get_all_tags,
update::{init_execution_update, update_update},
},
resource::{KomodoResource, list_full_for_user_using_pattern},
state::db_client,
};
@@ -49,9 +53,9 @@ mod sync;
use super::Variant;
pub struct ExecuteArgs {
/// The execution id.
/// Unique for every /execute call.
pub id: Uuid,
/// The task id.
/// Unique for every '/execute' call.
pub task_id: Uuid,
pub user: User,
pub update: Update,
}
@@ -60,7 +64,7 @@ pub struct ExecuteArgs {
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants,
)]
#[strum_discriminants(name(ExecuteRequestVariant), derive(Display))]
#[strum_discriminants(name(ExecuteRequestMethod), derive(Display))]
#[args(ExecuteArgs)]
#[response(JsonString)]
#[error(mogh_error::Error)]
@@ -111,10 +115,12 @@ pub enum ExecuteRequest {
// ==== PROCEDURE ====
RunProcedure(RunProcedure),
BatchRunProcedure(BatchRunProcedure),
CancelProcedure(CancelProcedure),
// ==== ACTION ====
RunAction(RunAction),
BatchRunAction(BatchRunAction),
CancelAction(CancelAction),
// ==== SYNC ====
RunSync(RunSync),
@@ -148,6 +154,7 @@ pub enum ExecuteRequest {
// ==== SWARM ====
RemoveSwarmNodes(RemoveSwarmNodes),
UpdateSwarmNode(UpdateSwarmNode),
RemoveSwarmStacks(RemoveSwarmStacks),
RemoveSwarmServices(RemoveSwarmServices),
CreateSwarmConfig(CreateSwarmConfig),
@@ -249,11 +256,19 @@ pub fn inner_handler(
async move {
let log = match handle.await {
Ok(Err(e)) => {
warn!("/execute request {task_id} task error: {e:#}",);
warn!(
api = "Execute",
task_id = task_id.to_string(),
"/execute request task error: {e:#}",
);
Log::error("Task Error", format_serror(&e.into()))
}
Err(e) => {
warn!("/execute request {task_id} spawn error: {e:?}",);
warn!(
api = "Execute",
task_id = task_id.to_string(),
"/execute request spawn error: {e:?}",
);
Log::error("Spawn Error", format!("{e:#?}"))
}
_ => return,
@@ -268,7 +283,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
@@ -277,7 +292,10 @@ pub fn inner_handler(
if let Err(e) = res {
warn!(
"Failed to update update with task error log | {e:#}"
api = "Execute",
task_id = task_id.to_string(),
update_id,
"Failed to modify Update with task error log | {e:#}"
);
}
}
@@ -293,25 +311,44 @@ async fn task(
user: User,
update: Update,
) -> anyhow::Result<String> {
let variant: ExecuteRequestVariant = (&request).into();
let method: ExecuteRequestMethod = (&request).into();
let user_id = user.id.clone();
let username = user.username.clone();
info!(
"EXECUTE REQUEST {id} | METHOD: {variant} | USER: {} ({})",
user.username, user.id
api = "Execute",
task_id = id.to_string(),
method = method.to_string(),
user_id,
username,
"EXECUTE REQUEST",
);
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),
};
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),
};
if let Err(e) = &res {
warn!("EXECUTE REQUEST {id} | METHOD: {variant} | ERROR: {e:#}");
warn!(
api = "Execute",
task_id = id.to_string(),
method = method.to_string(),
user_id,
username,
"EXECUTE REQUEST | ERROR: {e:#}"
);
}
res
@@ -325,14 +362,26 @@ 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,
Default::default(),
ResourceQuery {
tags,
..Default::default()
},
None,
None,
user,
PermissionLevel::Execute.into(),
&[],
&all_tags,
)
.await?;
+76 -9
View File
@@ -1,12 +1,14 @@
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, RunProcedure,
BatchExecutionResponse, BatchRunProcedure, CancelProcedure,
RunProcedure,
},
entities::{
alert::{Alert, AlertData, SeverityLevel},
@@ -19,13 +21,14 @@ 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},
state::{action_states, db_client, procedure_cancel_cache},
};
use super::{ExecuteArgs, ExecuteRequest};
@@ -48,8 +51,12 @@ impl Resolve<ExecuteArgs> for BatchRunProcedure {
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunProcedure>(&self.pattern, user)
.await?,
super::batch_execute::<BatchRunProcedure>(
&self.pattern,
self.tags,
user,
)
.await?,
)
}
}
@@ -59,7 +66,7 @@ impl Resolve<ExecuteArgs> for RunProcedure {
"RunProcedure",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
procedure = self.procedure,
@@ -67,7 +74,11 @@ impl Resolve<ExecuteArgs> for RunProcedure {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
Ok(
resolve_inner(self.procedure, user.clone(), update.clone())
@@ -113,14 +124,22 @@ 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).await;
let res = execute_procedure(&procedure, &update, cancel).await;
procedure_cancel_cache().remove(&procedure.id).await;
let mut update = update.into_inner();
@@ -136,11 +155,15 @@ 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,
@@ -180,3 +203,47 @@ 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)
}
}
+79 -31
View File
@@ -55,18 +55,23 @@ impl Resolve<ExecuteArgs> for BatchCloneRepo {
"BatchCloneRepo",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchCloneRepo>(&self.pattern, user)
.await?,
super::batch_execute::<BatchCloneRepo>(
&self.pattern,
self.tags,
user,
)
.await?,
)
}
}
@@ -76,7 +81,7 @@ impl Resolve<ExecuteArgs> for CloneRepo {
"CloneRepo",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
repo = self.repo,
@@ -84,7 +89,11 @@ impl Resolve<ExecuteArgs> for CloneRepo {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let mut repo = get_check_permissions::<Repo>(
&self.repo,
@@ -99,7 +108,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();
@@ -170,6 +179,10 @@ impl Resolve<ExecuteArgs> for CloneRepo {
);
};
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
handle_repo_update_return(update).await
}
}
@@ -186,18 +199,23 @@ impl Resolve<ExecuteArgs> for BatchPullRepo {
"BatchPullRepo",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchPullRepo>(&self.pattern, user)
.await?,
super::batch_execute::<BatchPullRepo>(
&self.pattern,
self.tags,
user,
)
.await?,
)
}
}
@@ -207,7 +225,7 @@ impl Resolve<ExecuteArgs> for PullRepo {
"PullRepo",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
repo = self.repo,
@@ -215,7 +233,11 @@ impl Resolve<ExecuteArgs> for PullRepo {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let mut repo = get_check_permissions::<Repo>(
&self.repo,
@@ -230,7 +252,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();
@@ -305,6 +327,10 @@ impl Resolve<ExecuteArgs> for PullRepo {
);
};
// Drop action guard before updating
// clients to requery action state
drop(action_guard);
handle_repo_update_return(update).await
}
}
@@ -363,18 +389,23 @@ impl Resolve<ExecuteArgs> for BatchBuildRepo {
"BatchBuildRepo",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchBuildRepo>(&self.pattern, user)
.await?,
super::batch_execute::<BatchBuildRepo>(
&self.pattern,
self.tags,
user,
)
.await?,
)
}
}
@@ -384,7 +415,7 @@ impl Resolve<ExecuteArgs> for BuildRepo {
"BuildRepo",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
repo = self.repo,
@@ -392,7 +423,11 @@ impl Resolve<ExecuteArgs> for BuildRepo {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let mut repo = get_check_permissions::<Repo>(
&self.repo,
@@ -411,7 +446,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();
@@ -478,7 +513,7 @@ impl Resolve<ExecuteArgs> for BuildRepo {
repo.name.clone(),
None,
builder,
&mut update,
Some(&mut update),
)
.await
{
@@ -489,6 +524,9 @@ 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,
)
@@ -521,6 +559,9 @@ 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
},
};
@@ -568,6 +609,10 @@ 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,
@@ -686,13 +731,12 @@ 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")),
_ => {}
};
@@ -705,7 +749,7 @@ impl Resolve<ExecuteArgs> for CancelRepoBuild {
"CancelRepoBuild",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
repo = self.repo,
@@ -713,7 +757,11 @@ impl Resolve<ExecuteArgs> for CancelRepoBuild {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let repo = get_check_permissions::<Repo>(
&self.repo,
+282 -93
View File
@@ -14,7 +14,7 @@ use periphery_client::api;
use crate::{
helpers::{periphery_client, update::update_update},
monitor::update_cache_for_server,
monitor::refresh_server_cache,
permission::get_check_permissions,
state::action_states,
};
@@ -26,7 +26,7 @@ impl Resolve<ExecuteArgs> for StartContainer {
"StartContainer",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -35,7 +35,11 @@ impl Resolve<ExecuteArgs> for StartContainer {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -52,8 +56,11 @@ 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(|state| state.starting_containers = true)?;
let action_guard = action_state.update_custom(
|state| state.starting_containers += 1,
|state| state.starting_containers -= 1,
false,
)?;
let mut update = update.clone();
@@ -76,9 +83,13 @@ impl Resolve<ExecuteArgs> for StartContainer {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -90,7 +101,7 @@ impl Resolve<ExecuteArgs> for RestartContainer {
"RestartContainer",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -99,7 +110,11 @@ impl Resolve<ExecuteArgs> for RestartContainer {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -116,8 +131,11 @@ 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(|state| state.restarting_containers = true)?;
let action_guard = action_state.update_custom(
|state| state.restarting_containers += 1,
|state| state.restarting_containers -= 1,
false,
)?;
let mut update = update.clone();
@@ -142,9 +160,13 @@ impl Resolve<ExecuteArgs> for RestartContainer {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -156,7 +178,7 @@ impl Resolve<ExecuteArgs> for PauseContainer {
"PauseContainer",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -165,7 +187,11 @@ impl Resolve<ExecuteArgs> for PauseContainer {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -182,8 +208,11 @@ 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(|state| state.pausing_containers = true)?;
let action_guard = action_state.update_custom(
|state| state.pausing_containers += 1,
|state| state.pausing_containers -= 1,
false,
)?;
let mut update = update.clone();
@@ -206,9 +235,13 @@ impl Resolve<ExecuteArgs> for PauseContainer {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -220,7 +253,7 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
"UnpauseContainer",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -229,7 +262,11 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -246,8 +283,11 @@ 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(|state| state.unpausing_containers = true)?;
let action_guard = action_state.update_custom(
|state| state.unpausing_containers += 1,
|state| state.unpausing_containers -= 1,
false,
)?;
let mut update = update.clone();
@@ -272,9 +312,13 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -286,7 +330,7 @@ impl Resolve<ExecuteArgs> for StopContainer {
"StopContainer",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -297,7 +341,11 @@ impl Resolve<ExecuteArgs> for StopContainer {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -314,8 +362,11 @@ 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(|state| state.stopping_containers = true)?;
let action_guard = action_state.update_custom(
|state| state.stopping_containers += 1,
|state| state.stopping_containers -= 1,
false,
)?;
let mut update = update.clone();
@@ -340,9 +391,13 @@ impl Resolve<ExecuteArgs> for StopContainer {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -354,7 +409,7 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
"DestroyContainer",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -365,7 +420,11 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let DestroyContainer {
server,
@@ -388,8 +447,11 @@ 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(|state| state.pruning_containers = true)?;
let action_guard = action_state.update_custom(
|state| state.destroying_containers += 1,
|state| state.destroying_containers -= 1,
false,
)?;
let mut update = update.clone();
@@ -416,9 +478,13 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -430,7 +496,7 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
"StartAllContainers",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -438,7 +504,11 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -455,8 +525,11 @@ 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(|state| state.starting_containers = true)?;
let action_guard = action_state.update_custom(
|state| state.starting_containers += 1,
|state| state.starting_containers -= 1,
false,
)?;
let mut update = update.clone();
@@ -477,8 +550,12 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
);
}
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -490,7 +567,7 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
"RestartAllContainers",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -498,7 +575,11 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -515,8 +596,11 @@ 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(|state| state.restarting_containers = true)?;
let action_guard = action_state.update_custom(
|state| state.restarting_containers += 1,
|state| state.restarting_containers -= 1,
false,
)?;
let mut update = update.clone();
@@ -539,8 +623,12 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
);
}
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -552,7 +640,7 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
"PauseAllContainers",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -560,7 +648,11 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -577,8 +669,11 @@ 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(|state| state.pausing_containers = true)?;
let action_guard = action_state.update_custom(
|state| state.pausing_containers += 1,
|state| state.pausing_containers -= 1,
false,
)?;
let mut update = update.clone();
@@ -599,8 +694,12 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
);
}
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -612,7 +711,7 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
"UnpauseAllContainers",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -620,7 +719,11 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -637,8 +740,11 @@ 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(|state| state.unpausing_containers = true)?;
let action_guard = action_state.update_custom(
|state| state.unpausing_containers += 1,
|state| state.unpausing_containers -= 1,
false,
)?;
let mut update = update.clone();
@@ -661,8 +767,12 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
);
}
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -674,7 +784,7 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
"StopAllContainers",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -682,7 +792,11 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -699,8 +813,11 @@ 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(|state| state.stopping_containers = true)?;
let action_guard = action_state.update_custom(
|state| state.stopping_containers += 1,
|state| state.stopping_containers -= 1,
false,
)?;
let mut update = update.clone();
@@ -721,8 +838,12 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
);
}
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -734,7 +855,7 @@ impl Resolve<ExecuteArgs> for PruneContainers {
"PruneContainers",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -742,7 +863,11 @@ impl Resolve<ExecuteArgs> for PruneContainers {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -759,7 +884,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();
@@ -785,9 +910,13 @@ impl Resolve<ExecuteArgs> for PruneContainers {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -799,7 +928,7 @@ impl Resolve<ExecuteArgs> for DeleteNetwork {
"DeleteNetwork",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -808,7 +937,11 @@ impl Resolve<ExecuteArgs> for DeleteNetwork {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -846,7 +979,7 @@ impl Resolve<ExecuteArgs> for DeleteNetwork {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&server, true).await;
update.finalize();
update_update(update.clone()).await?;
@@ -860,7 +993,7 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
"PruneNetworks",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -868,7 +1001,11 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -885,7 +1022,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();
@@ -909,9 +1046,13 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -923,7 +1064,7 @@ impl Resolve<ExecuteArgs> for DeleteImage {
"DeleteImage",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -932,7 +1073,11 @@ impl Resolve<ExecuteArgs> for DeleteImage {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -967,7 +1112,7 @@ impl Resolve<ExecuteArgs> for DeleteImage {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&server, true).await;
update.finalize();
update_update(update.clone()).await?;
@@ -981,7 +1126,7 @@ impl Resolve<ExecuteArgs> for PruneImages {
"PruneImages",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -989,7 +1134,11 @@ impl Resolve<ExecuteArgs> for PruneImages {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -1006,7 +1155,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();
@@ -1028,9 +1177,13 @@ impl Resolve<ExecuteArgs> for PruneImages {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -1042,7 +1195,7 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
"DeleteVolume",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -1051,7 +1204,11 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -1089,7 +1246,7 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&server, true).await;
update.finalize();
update_update(update.clone()).await?;
@@ -1103,7 +1260,7 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
"PruneVolumes",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -1111,7 +1268,11 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -1128,7 +1289,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();
@@ -1150,9 +1311,13 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -1164,7 +1329,7 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
"PruneDockerBuilders",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -1172,7 +1337,11 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -1189,7 +1358,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();
@@ -1211,9 +1380,13 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -1225,7 +1398,7 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
"PruneBuildx",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -1233,7 +1406,11 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -1250,7 +1427,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();
@@ -1272,9 +1449,13 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -1286,7 +1467,7 @@ impl Resolve<ExecuteArgs> for PruneSystem {
"PruneSystem",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
server = self.server,
@@ -1294,7 +1475,11 @@ impl Resolve<ExecuteArgs> for PruneSystem {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
@@ -1311,7 +1496,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();
@@ -1332,9 +1517,13 @@ impl Resolve<ExecuteArgs> for PruneSystem {
};
update.logs.push(log);
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
+120 -47
View File
@@ -39,7 +39,7 @@ use crate::{
add_update_without_send, init_execution_update, update_update,
},
},
monitor::{update_cache_for_server, update_cache_for_swarm},
monitor::{refresh_server_cache, refresh_swarm_cache},
permission::get_check_permissions,
resource,
stack::{
@@ -69,18 +69,23 @@ impl Resolve<ExecuteArgs> for BatchDeployStack {
"BatchDeployStack",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeployStack>(&self.pattern, user)
.await?,
super::batch_execute::<BatchDeployStack>(
&self.pattern,
self.tags,
user,
)
.await?,
)
}
}
@@ -90,7 +95,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
"DeployStack",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -100,7 +105,11 @@ impl Resolve<ExecuteArgs> for DeployStack {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (mut stack, swarm_or_server) = setup_stack_execution(
&self.stack,
@@ -127,7 +136,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();
@@ -313,14 +322,18 @@ impl Resolve<ExecuteArgs> for DeployStack {
match swarm_or_server {
SwarmOrServer::None => unreachable!(),
SwarmOrServer::Swarm(swarm) => {
update_cache_for_swarm(&swarm, true).await;
refresh_swarm_cache(&swarm, true).await;
}
SwarmOrServer::Server(server) => {
update_cache_for_server(&server, true).await;
refresh_server_cache(&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)
@@ -342,18 +355,20 @@ impl Resolve<ExecuteArgs> for BatchDeployStackIfChanged {
"BatchDeployStackIfChanged",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeployStackIfChanged>(
&self.pattern,
self.tags,
user,
)
.await?,
@@ -366,7 +381,7 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
"DeployStackIfChanged",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -375,7 +390,11 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let stack = get_check_permissions::<Stack>(
&self.stack,
@@ -433,7 +452,7 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
.resolve(&ExecuteArgs {
user: user.clone(),
update,
id: *id,
task_id: *task_id,
})
.await
}
@@ -572,7 +591,7 @@ async fn deploy_services(
.resolve(&ExecuteArgs {
user: user.clone(),
update,
id: Uuid::new_v4(),
task_id: Uuid::new_v4(),
})
.await
}
@@ -603,7 +622,7 @@ async fn restart_services(
.resolve(&ExecuteArgs {
user: user.clone(),
update,
id: Uuid::new_v4(),
task_id: Uuid::new_v4(),
})
.await
}
@@ -766,18 +785,23 @@ impl Resolve<ExecuteArgs> for BatchPullStack {
"BatchPullStack",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchPullStack>(&self.pattern, user)
.await?,
super::batch_execute::<BatchPullStack>(
&self.pattern,
self.tags,
user,
)
.await?,
)
}
}
@@ -878,7 +902,7 @@ pub async fn pull_stack_inner(
.await?;
// Ensure cached stack state up to date by updating server cache
update_cache_for_server(server, true).await;
refresh_server_cache(server, true).await;
Ok(res)
}
@@ -888,7 +912,7 @@ impl Resolve<ExecuteArgs> for PullStack {
"PullStack",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -897,7 +921,11 @@ impl Resolve<ExecuteArgs> for PullStack {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (stack, swarm_or_server) = setup_stack_execution(
&self.stack,
@@ -931,7 +959,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();
@@ -948,6 +976,10 @@ 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)
@@ -959,7 +991,7 @@ impl Resolve<ExecuteArgs> for StartStack {
"StartStack",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -968,7 +1000,11 @@ impl Resolve<ExecuteArgs> for StartStack {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
execute_compose::<StartStack>(
&self.stack,
@@ -988,7 +1024,7 @@ impl Resolve<ExecuteArgs> for RestartStack {
"RestartStack",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -997,7 +1033,11 @@ impl Resolve<ExecuteArgs> for RestartStack {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
execute_compose::<RestartStack>(
&self.stack,
@@ -1019,7 +1059,7 @@ impl Resolve<ExecuteArgs> for PauseStack {
"PauseStack",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -1028,7 +1068,11 @@ impl Resolve<ExecuteArgs> for PauseStack {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
execute_compose::<PauseStack>(
&self.stack,
@@ -1048,7 +1092,7 @@ impl Resolve<ExecuteArgs> for UnpauseStack {
"UnpauseStack",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -1057,7 +1101,11 @@ impl Resolve<ExecuteArgs> for UnpauseStack {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
execute_compose::<UnpauseStack>(
&self.stack,
@@ -1077,7 +1125,7 @@ impl Resolve<ExecuteArgs> for StopStack {
"StopStack",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -1086,7 +1134,11 @@ impl Resolve<ExecuteArgs> for StopStack {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
execute_compose::<StopStack>(
&self.stack,
@@ -1118,18 +1170,23 @@ impl Resolve<ExecuteArgs> for BatchDestroyStack {
"BatchDestroyStack",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
pattern = self.pattern,
tags = self.tags.join(","),
)
)]
async fn resolve(
self,
ExecuteArgs { user, id, .. }: &ExecuteArgs,
ExecuteArgs { user, task_id, .. }: &ExecuteArgs,
) -> mogh_error::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDestroyStack>(&self.pattern, user)
.await
.map_err(Into::into)
super::batch_execute::<BatchDestroyStack>(
&self.pattern,
self.tags,
user,
)
.await
.map_err(Into::into)
}
}
@@ -1138,7 +1195,7 @@ impl Resolve<ExecuteArgs> for DestroyStack {
"DestroyStack",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -1149,7 +1206,11 @@ impl Resolve<ExecuteArgs> for DestroyStack {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (stack, swarm_or_server) = setup_stack_execution(
&self.stack,
@@ -1178,7 +1239,7 @@ 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();
@@ -1205,9 +1266,13 @@ impl Resolve<ExecuteArgs> for DestroyStack {
}
// Ensure cached stack state up to date by updating swarm cache
update_cache_for_swarm(&swarm, true).await;
refresh_swarm_cache(&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)
@@ -1233,7 +1298,7 @@ impl Resolve<ExecuteArgs> for RunStackService {
"RunStackService",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
stack = self.stack,
@@ -1243,7 +1308,11 @@ impl Resolve<ExecuteArgs> for RunStackService {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let (mut stack, swarm_or_server) = setup_stack_execution(
&self.stack,
@@ -1274,7 +1343,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();
@@ -1334,6 +1403,10 @@ 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)
+131 -33
View File
@@ -1,10 +1,6 @@
use formatting::format_serror;
use komodo_client::{
api::execute::{
CreateSwarmConfig, CreateSwarmSecret, RemoveSwarmConfigs,
RemoveSwarmNodes, RemoveSwarmSecrets, RemoveSwarmServices,
RemoveSwarmStacks, RotateSwarmConfig, RotateSwarmSecret,
},
api::execute::*,
entities::{permission::PermissionLevel, swarm::Swarm},
};
use mogh_resolver::Resolve;
@@ -12,7 +8,7 @@ use mogh_resolver::Resolve;
use crate::{
api::execute::ExecuteArgs,
helpers::{swarm::swarm_request, update::update_update},
monitor::update_cache_for_swarm,
monitor::refresh_swarm_cache,
permission::get_check_permissions,
};
@@ -21,7 +17,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmNodes {
"RemoveSwarmNodes",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -31,7 +27,11 @@ impl Resolve<ExecuteArgs> for RemoveSwarmNodes {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_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);
update_cache_for_swarm(&swarm, true).await;
refresh_swarm_cache(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Remove Swarm Nodes",
@@ -72,12 +72,78 @@ 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(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -87,7 +153,11 @@ impl Resolve<ExecuteArgs> for RemoveSwarmStacks {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -111,7 +181,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmStacks {
{
Ok(log) => {
update.logs.push(log);
update_cache_for_swarm(&swarm, true).await;
refresh_swarm_cache(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Remove Swarm Stacks",
@@ -133,7 +203,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmServices {
"RemoveSwarmServices",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -142,7 +212,11 @@ impl Resolve<ExecuteArgs> for RemoveSwarmServices {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -165,7 +239,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmServices {
{
Ok(log) => {
update.logs.push(log);
update_cache_for_swarm(&swarm, true).await;
refresh_swarm_cache(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Remove Swarm Services",
@@ -187,7 +261,7 @@ impl Resolve<ExecuteArgs> for CreateSwarmConfig {
"CreateSwarmConfig",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -196,7 +270,11 @@ impl Resolve<ExecuteArgs> for CreateSwarmConfig {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -222,7 +300,7 @@ impl Resolve<ExecuteArgs> for CreateSwarmConfig {
{
Ok(log) => {
update.logs.push(log);
update_cache_for_swarm(&swarm, true).await;
refresh_swarm_cache(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Create Swarm Config",
@@ -244,7 +322,7 @@ impl Resolve<ExecuteArgs> for RotateSwarmConfig {
"RotateSwarmConfig",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -253,7 +331,11 @@ impl Resolve<ExecuteArgs> for RotateSwarmConfig {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -277,7 +359,7 @@ impl Resolve<ExecuteArgs> for RotateSwarmConfig {
{
Ok(logs) => {
update.logs.extend(logs);
update_cache_for_swarm(&swarm, true).await;
refresh_swarm_cache(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Rotate Swarm Config",
@@ -299,7 +381,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmConfigs {
"RemoveSwarmConfigs",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -308,7 +390,11 @@ impl Resolve<ExecuteArgs> for RemoveSwarmConfigs {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -331,7 +417,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmConfigs {
{
Ok(log) => {
update.logs.push(log);
update_cache_for_swarm(&swarm, true).await;
refresh_swarm_cache(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Remove Swarm Configs",
@@ -353,7 +439,7 @@ impl Resolve<ExecuteArgs> for CreateSwarmSecret {
"CreateSwarmSecret",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -362,7 +448,11 @@ impl Resolve<ExecuteArgs> for CreateSwarmSecret {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -389,7 +479,7 @@ impl Resolve<ExecuteArgs> for CreateSwarmSecret {
{
Ok(log) => {
update.logs.push(log);
update_cache_for_swarm(&swarm, true).await;
refresh_swarm_cache(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Create Swarm Secret",
@@ -411,7 +501,7 @@ impl Resolve<ExecuteArgs> for RotateSwarmSecret {
"RotateSwarmSecret",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -420,7 +510,11 @@ impl Resolve<ExecuteArgs> for RotateSwarmSecret {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -444,7 +538,7 @@ impl Resolve<ExecuteArgs> for RotateSwarmSecret {
{
Ok(logs) => {
update.logs.extend(logs);
update_cache_for_swarm(&swarm, true).await;
refresh_swarm_cache(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Rotate Swarm Secret",
@@ -466,7 +560,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmSecrets {
"RemoveSwarmSecrets",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
swarm = self.swarm,
@@ -475,7 +569,11 @@ impl Resolve<ExecuteArgs> for RemoveSwarmSecrets {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
@@ -498,7 +596,7 @@ impl Resolve<ExecuteArgs> for RemoveSwarmSecrets {
{
Ok(log) => {
update.logs.push(log);
update_cache_for_swarm(&swarm, true).await;
refresh_swarm_cache(&swarm, true).await;
}
Err(e) => update.push_error_log(
"Remove Swarm Secrets",
+106 -198
View File
@@ -21,6 +21,7 @@ use komodo_client::{
repo::Repo,
server::Server,
stack::Stack,
swarm::Swarm,
sync::ResourceSync,
update::{Log, Update},
user::sync_user,
@@ -53,7 +54,7 @@ impl Resolve<ExecuteArgs> for RunSync {
"RunSync",
skip_all,
fields(
id = id.to_string(),
task_id = task_id.to_string(),
operator = user.id,
update_id = update.id,
sync = self.sync,
@@ -63,7 +64,11 @@ impl Resolve<ExecuteArgs> for RunSync {
)]
async fn resolve(
self,
ExecuteArgs { user, update, id }: &ExecuteArgs,
ExecuteArgs {
user,
update,
task_id,
}: &ExecuteArgs,
) -> mogh_error::Result<Update> {
let RunSync {
sync,
@@ -93,7 +98,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();
@@ -134,56 +139,36 @@ impl Resolve<ExecuteArgs> for RunSync {
let Some(resource_type) = match_resource_type else {
return Some(name_or_id);
};
match ObjectId::from_str(&name_or_id) {
Ok(_) => match resource_type {
ResourceTargetVariant::Swarm => all_resources
.swarms
.get(&name_or_id)
.map(|s| s.name.clone()),
ResourceTargetVariant::Server => all_resources
.servers
.get(&name_or_id)
.map(|s| s.name.clone()),
ResourceTargetVariant::Stack => all_resources
.stacks
.get(&name_or_id)
.map(|s| s.name.clone()),
ResourceTargetVariant::Deployment => all_resources
.deployments
.get(&name_or_id)
.map(|d| d.name.clone()),
ResourceTargetVariant::Build => all_resources
.builds
.get(&name_or_id)
.map(|b| b.name.clone()),
ResourceTargetVariant::Repo => all_resources
.repos
.get(&name_or_id)
.map(|r| r.name.clone()),
ResourceTargetVariant::Procedure => all_resources
.procedures
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::Action => all_resources
.actions
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::ResourceSync => all_resources
.syncs
.get(&name_or_id)
.map(|s| s.name.clone()),
ResourceTargetVariant::Builder => all_resources
.builders
.get(&name_or_id)
.map(|b| b.name.clone()),
ResourceTargetVariant::Alerter => all_resources
.alerters
.get(&name_or_id)
.map(|a| a.name.clone()),
ResourceTargetVariant::System => None,
},
Err(_) => Some(name_or_id),
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),
}
};
}
// 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<_>>()
});
@@ -231,136 +216,39 @@ impl Resolve<ExecuteArgs> for RunSync {
let delete = sync.config.managed || sync.config.delete;
let server_deltas = if sync.config.include_resources {
get_updates_for_execution::<Server>(
resources.servers,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let stack_deltas = if sync.config.include_resources {
get_updates_for_execution::<Stack>(
resources.stacks,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let deployment_deltas = if sync.config.include_resources {
get_updates_for_execution::<Deployment>(
resources.deployments,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let build_deltas = if sync.config.include_resources {
get_updates_for_execution::<Build>(
resources.builds,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let repo_deltas = if sync.config.include_resources {
get_updates_for_execution::<Repo>(
resources.repos,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let procedure_deltas = if sync.config.include_resources {
get_updates_for_execution::<Procedure>(
resources.procedures,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let action_deltas = if sync.config.include_resources {
get_updates_for_execution::<Action>(
resources.actions,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let builder_deltas = if sync.config.include_resources {
get_updates_for_execution::<Builder>(
resources.builders,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let alerter_deltas = if sync.config.include_resources {
get_updates_for_execution::<Alerter>(
resources.alerters,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let resource_sync_deltas = if sync.config.include_resources {
get_updates_for_execution::<entities::sync::ResourceSync>(
resources.resource_syncs,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
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 (
variables_to_create,
@@ -395,9 +283,11 @@ 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()
@@ -421,11 +311,19 @@ 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(
@@ -446,10 +344,7 @@ 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,
@@ -463,34 +358,43 @@ impl Resolve<ExecuteArgs> for RunSync {
Action::execute_sync_updates(action_deltas).await,
);
// Dependent on server
// Depends on server
maybe_extend(
&mut update.logs,
Swarm::execute_sync_updates(swarm_deltas).await,
);
// Depends 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,
);
// Dependant on builder
// Depends on builder / repo
maybe_extend(
&mut update.logs,
Build::execute_sync_updates(build_deltas).await,
);
// Dependant on server / build
maybe_extend(
&mut update.logs,
Deployment::execute_sync_updates(deployment_deltas).await,
);
// stack only depends on server, but maybe will depend on build later.
// Depends on server / repo
maybe_extend(
&mut update.logs,
Stack::execute_sync_updates(stack_deltas).await,
);
// Dependant on everything
// Depends on repo
maybe_extend(
&mut update.logs,
ResourceSync::execute_sync_updates(resource_sync_deltas).await,
);
// Depends on server / build
maybe_extend(
&mut update.logs,
Deployment::execute_sync_updates(deployment_deltas).await,
);
// Depends on everything
maybe_extend(
&mut update.logs,
Procedure::execute_sync_updates(procedure_deltas).await,
@@ -542,6 +446,10 @@ 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, Mac};
use hmac::{Hmac, KeyInit as _, Mac};
use serde::Deserialize;
use sha2::Sha256;
+12 -4
View File
@@ -1,6 +1,5 @@
use std::sync::Arc;
use anyhow::anyhow;
use axum::{Router, http::HeaderMap};
use komodo_client::entities::resource::Resource;
use mogh_cache::CloneCache;
@@ -41,12 +40,21 @@ trait VerifySecret {
/// Implemented on the integration struct, eg [integrations::github::Github]
trait ExtractBranch {
fn extract_branch(body: &str) -> anyhow::Result<String>;
fn verify_branch(body: &str, expected: &str) -> anyhow::Result<()> {
/// 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> {
let branch = Self::extract_branch(body)?;
if branch == expected {
Ok(())
Ok(true)
} else {
Err(anyhow!("request branch does not match expected"))
debug!(
"Ignoring webhook | push to branch '{branch}' does not match expected branch '{expected}'"
);
Ok(false)
}
}
}
+90 -32
View File
@@ -1,4 +1,4 @@
use std::{str::FromStr, sync::OnceLock};
use std::{str::FromStr, sync::OnceLock, time::Duration};
use anyhow::{Context, anyhow};
use komodo_client::{
@@ -23,6 +23,7 @@ use crate::{
},
helpers::update::init_execution_update,
resource,
state::action_states,
};
use super::{ANY_BRANCH, ListenerLockCache};
@@ -37,11 +38,6 @@ 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,
@@ -50,12 +46,6 @@ 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
@@ -67,25 +57,82 @@ pub async fn handle_build_webhook<B: super::ExtractBranch>(
.branch
};
B::verify_branch(&body, &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?;
}
let user = git_webhook_user().to_owned();
let req = ExecuteRequest::RunBuild(RunBuild { build: build.id });
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunBuild(req) = req else {
let run = ExecuteRequest::RunBuild(RunBuild { build: build.id });
let update = init_execution_update(&run, &user).await?;
let ExecuteRequest::RunBuild(run) = run else {
unreachable!()
};
req
run
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
task_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
// ======
@@ -121,7 +168,7 @@ impl RepoExecution for CloneRepo {
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
task_id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
@@ -145,7 +192,7 @@ impl RepoExecution for PullRepo {
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
task_id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
@@ -169,7 +216,7 @@ impl RepoExecution for BuildRepo {
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
task_id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
@@ -220,7 +267,9 @@ async fn handle_repo_webhook_inner<
let lock = repo_locks().get_or_insert_default(&repo.id).await;
let _lock = lock.lock().await;
B::verify_branch(&body, &repo.config.branch)?;
if !B::branch_matches(&body, &repo.config.branch)? {
return Ok(());
}
E::resolve(repo).await
}
@@ -272,7 +321,7 @@ impl StackExecution for DeployStack {
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
task_id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
@@ -290,7 +339,7 @@ impl StackExecution for DeployStack {
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
task_id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
@@ -351,7 +400,9 @@ pub async fn handle_stack_webhook_inner<
.branch
};
B::verify_branch(&body, &branch)?;
if !B::branch_matches(&body, &branch)? {
return Ok(());
}
E::resolve(stack).await.map_err(|e| e.error)
}
@@ -403,7 +454,7 @@ impl SyncExecution for RunSync {
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
task_id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
@@ -464,7 +515,9 @@ async fn handle_sync_webhook_inner<
.branch
};
B::verify_branch(&body, &branch)?;
if !B::branch_matches(&body, &branch)? {
return Ok(());
}
E::resolve(sync).await
}
@@ -501,8 +554,10 @@ 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::verify_branch(&body, target_branch)?;
if target_branch != ANY_BRANCH
&& !B::branch_matches(&body, target_branch)?
{
return Ok(());
}
let user = git_webhook_user().to_owned();
@@ -517,7 +572,7 @@ pub async fn handle_procedure_webhook<B: super::ExtractBranch>(
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
task_id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
@@ -557,7 +612,10 @@ pub async fn handle_action_webhook<B: super::ExtractBranch>(
let branch = B::extract_branch(&body)?;
if target_branch != ANY_BRANCH && branch != target_branch {
return Err(anyhow!("request branch does not match expected"));
debug!(
"Ignoring webhook | push to branch '{branch}' does not match expected branch '{target_branch}'"
);
return Ok(());
}
let user = git_webhook_user().to_owned();
@@ -583,7 +641,7 @@ pub async fn handle_action_webhook<B: super::ExtractBranch>(
.resolve(&ExecuteArgs {
user,
update,
id: Uuid::new_v4(),
task_id: Uuid::new_v4(),
})
.await
.map_err(|e| e.error)?;
+23
View File
@@ -222,8 +222,31 @@ 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
);
})
}
+61 -12
View File
@@ -3,7 +3,8 @@ use komodo_client::{
api::read::*,
entities::{
action::{
Action, ActionActionState, ActionListItem, ActionState,
Action, ActionActionState, ActionListItem, ActionSortBy,
ActionState,
},
permission::PermissionLevel,
},
@@ -11,13 +12,13 @@ use komodo_client::{
use mogh_resolver::Resolve;
use crate::{
helpers::query::get_all_tags,
helpers::query::{get_action_state, get_all_tags},
permission::get_check_permissions,
resource,
state::{action_state_cache, action_states},
};
use super::ReadArgs;
use super::{ReadArgs, list_limit};
impl Resolve<ReadArgs> for GetAction {
async fn resolve(
@@ -45,15 +46,45 @@ impl Resolve<ReadArgs> for ListActions {
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Action>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.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)
},
)
.await?;
Ok(actions)
}
}
@@ -67,12 +98,28 @@ 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::<Action>(
resource::list_full_for_user_filtered::<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?,
)
@@ -107,6 +154,8 @@ 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 * NUM_ALERTS_PER_PAGE)
.skip(self.page.saturating_mul(NUM_ALERTS_PER_PAGE))
.build(),
)
.await
+26 -3
View File
@@ -4,7 +4,7 @@ use database::mungos::mongodb::bson::doc;
use komodo_client::{
api::read::*,
entities::{
alerter::{Alerter, AlerterListItem},
alerter::{Alerter, AlerterListItem, AlerterSortBy},
permission::PermissionLevel,
},
};
@@ -17,7 +17,7 @@ use crate::{
state::db_client,
};
use super::ReadArgs;
use super::{ReadArgs, list_limit};
impl Resolve<ReadArgs> for GetAlerter {
async fn resolve(
@@ -45,12 +45,30 @@ 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_for_user::<Alerter>(
resource::list_items_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?,
)
@@ -67,9 +85,12 @@ 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,
@@ -85,6 +106,8 @@ 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(),
+68 -12
View File
@@ -11,7 +11,9 @@ use komodo_client::{
api::read::*,
entities::{
Operation,
build::{Build, BuildActionState, BuildListItem, BuildState},
build::{
Build, BuildActionState, BuildListItem, BuildSortBy, BuildState,
},
permission::PermissionLevel,
update::UpdateStatus,
},
@@ -25,7 +27,7 @@ use crate::{
state::{action_states, build_state_cache, db_client},
};
use super::ReadArgs;
use super::{ReadArgs, list_limit};
impl Resolve<ReadArgs> for GetBuild {
async fn resolve(
@@ -53,15 +55,47 @@ impl Resolve<ReadArgs> for ListBuilds {
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Build>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.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),
)
.await?;
Ok(builds)
}
}
@@ -75,12 +109,29 @@ 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::<Build>(
resource::list_full_for_user_filtered::<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?,
)
@@ -115,6 +166,8 @@ 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(),
&[],
@@ -164,7 +217,8 @@ 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 * 30 * ONE_DAY_MS;
let close_ts =
next_day - (self.page as i64).saturating_mul(30 * ONE_DAY_MS);
let open_ts = close_ts - 30 * ONE_DAY_MS;
let mut build_updates = db_client()
@@ -281,6 +335,8 @@ impl Resolve<ReadArgs> for ListCommonBuildExtraArgs {
};
let builds = resource::list_full_for_user::<Build>(
self.query,
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
+31 -3
View File
@@ -4,7 +4,7 @@ use database::mungos::mongodb::bson::doc;
use komodo_client::{
api::read::*,
entities::{
builder::{Builder, BuilderListItem},
builder::{Builder, BuilderListItem, BuilderSortBy},
permission::PermissionLevel,
},
};
@@ -17,7 +17,7 @@ use crate::{
state::db_client,
};
use super::ReadArgs;
use super::{ReadArgs, list_limit};
impl Resolve<ReadArgs> for GetBuilder {
async fn resolve(
@@ -45,12 +45,35 @@ 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_for_user::<Builder>(
resource::list_items_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?,
)
@@ -67,9 +90,12 @@ 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,
@@ -85,6 +111,8 @@ 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(),
+99 -29
View File
@@ -6,8 +6,8 @@ use komodo_client::{
entities::{
SwarmOrServer,
deployment::{
Deployment, DeploymentActionState, DeploymentConfig,
DeploymentListItem, DeploymentState,
Deployment, DeploymentActionState, DeploymentListItem,
DeploymentSortBy, DeploymentState,
},
docker::{
container::{Container, ContainerStats},
@@ -25,7 +25,9 @@ use reqwest::StatusCode;
use crate::{
helpers::{
periphery_client, query::get_all_tags, swarm::swarm_request,
periphery_client,
query::{get_all_tags, get_deployment_state},
swarm::swarm_request,
},
permission::get_check_permissions,
resource::{self, setup_deployment_execution},
@@ -34,7 +36,7 @@ use crate::{
},
};
use super::ReadArgs;
use super::{ReadArgs, list_limit};
impl Resolve<ReadArgs> for GetDeployment {
async fn resolve(
@@ -63,21 +65,66 @@ impl Resolve<ReadArgs> for ListDeployments {
get_all_tags(None).await?
};
let only_update_available = self.query.specific.update_available;
let deployments = resource::list_for_user::<Deployment>(
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>(
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)
}
}
@@ -92,12 +139,32 @@ 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::<Deployment>(
resource::list_full_for_user_filtered::<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?,
)
@@ -154,7 +221,7 @@ impl Resolve<ReadArgs> for GetDeploymentLog {
SwarmOrServer::Swarm(swarm) => swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::GetSwarmServiceLog {
service: deployment.name,
service: deployment.deployed_name().to_string(),
tail,
timestamps,
no_task_ids: false,
@@ -167,7 +234,7 @@ impl Resolve<ReadArgs> for GetDeploymentLog {
SwarmOrServer::Server(server) => periphery_client(&server)
.await?
.request(api::container::GetContainerLog {
name: deployment.name,
name: deployment.deployed_name().to_string(),
tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps,
})
@@ -206,7 +273,7 @@ impl Resolve<ReadArgs> for SearchDeploymentLog {
SwarmOrServer::Swarm(swarm) => swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::GetSwarmServiceLogSearch {
service: deployment.name,
service: deployment.deployed_name().to_string(),
terms,
combinator,
invert,
@@ -221,7 +288,7 @@ impl Resolve<ReadArgs> for SearchDeploymentLog {
SwarmOrServer::Server(server) => periphery_client(&server)
.await?
.request(api::container::GetContainerLogSearch {
name: deployment.name,
name: deployment.deployed_name().to_string(),
terms,
combinator,
invert,
@@ -274,7 +341,7 @@ impl Resolve<ReadArgs> for InspectDeploymentContainer {
periphery_client(&server)
.await?
.request(InspectContainer {
name: deployment.name,
name: deployment.deployed_name().to_string(),
})
.await
.context("Failed to inspect container on server")
@@ -307,7 +374,7 @@ impl Resolve<ReadArgs> for InspectDeploymentSwarmService {
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmService {
service: deployment.name,
service: deployment.deployed_name().to_string(),
},
)
.await
@@ -321,25 +388,24 @@ impl Resolve<ReadArgs> for GetDeploymentStats {
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ContainerStats> {
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = get_check_permissions::<Deployment>(
let deployment = get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read.into(),
)
.await?;
if server_id.is_empty() {
if deployment.config.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>(&server_id).await?;
let server =
resource::get::<Server>(&deployment.config.server_id).await?;
let res = periphery_client(&server)
.await?
.request(api::container::GetContainerStats { name })
.request(api::container::GetContainerStats {
name: deployment.deployed_name().to_string(),
})
.await
.context("failed to get stats from periphery")?;
Ok(res)
@@ -374,6 +440,8 @@ 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(),
&[],
@@ -422,6 +490,8 @@ 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
@@ -0,0 +1,590 @@
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)
}
}
+90 -45
View File
@@ -10,7 +10,7 @@ use komodo_client::{
ResourceTarget,
build::Build,
builder::{Builder, BuilderConfig},
config::{DockerRegistry, GitProvider},
config::{GitProvider, ImageRegistry},
permission::PermissionLevel,
repo::Repo,
server::Server,
@@ -44,6 +44,7 @@ mod alerter;
mod build;
mod builder;
mod deployment;
mod docker;
mod onboarding_key;
mod permission;
mod procedure;
@@ -66,11 +67,17 @@ 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(ReadRequestVariant), derive(Display))]
#[strum_discriminants(name(ReadRequestMethod), derive(Display))]
#[args(ReadArgs)]
#[response(Response)]
#[error(mogh_error::Error)]
@@ -80,7 +87,8 @@ enum ReadRequest {
GetCoreInfo(GetCoreInfo),
ListSecrets(ListSecrets),
ListGitProvidersFromConfig(ListGitProvidersFromConfig),
ListDockerRegistriesFromConfig(ListDockerRegistriesFromConfig),
#[serde(alias = "ListDockerRegistriesFromConfig")]
ListImageRegistriesFromConfig(ListImageRegistriesFromConfig),
// ==== SWARM ====
GetSwarmsSummary(GetSwarmsSummary),
@@ -117,22 +125,33 @@ enum ReadRequest {
// ==== TERMINAL ====
ListTerminals(ListTerminals),
// ==== DOCKER ====
GetDockerContainersSummary(GetDockerContainersSummary),
ListAllDockerContainers(ListAllDockerContainers),
ListDockerContainers(ListDockerContainers),
InspectDockerContainer(InspectDockerContainer),
// ==== CONTAINER ====
#[serde(alias = "GetDockerContainersSummary")]
GetContainersSummary(GetContainersSummary),
#[serde(alias = "ListAllDockerContainers")]
ListAllContainers(ListAllContainers),
#[serde(alias = "ListDockerContainers")]
ListContainers(ListContainers),
#[serde(alias = "InspectDockerContainer")]
InspectContainer(InspectContainer),
GetResourceMatchingContainer(GetResourceMatchingContainer),
GetContainerLog(GetContainerLog),
SearchContainerLog(SearchContainerLog),
ListComposeProjects(ListComposeProjects),
ListDockerNetworks(ListDockerNetworks),
InspectDockerNetwork(InspectDockerNetwork),
ListDockerImages(ListDockerImages),
InspectDockerImage(InspectDockerImage),
ListDockerImageHistory(ListDockerImageHistory),
ListDockerVolumes(ListDockerVolumes),
InspectDockerVolume(InspectDockerVolume),
#[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),
// ==== SERVER STATS ====
GetSystemInformation(GetSystemInformation),
@@ -151,6 +170,7 @@ enum ReadRequest {
ListStacks(ListStacks),
ListFullStacks(ListFullStacks),
ListStackServices(ListStackServices),
ListAllStackServices(ListAllStackServices),
ListCommonStackExtraArgs(ListCommonStackExtraArgs),
ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs),
@@ -258,8 +278,10 @@ enum ReadRequest {
// ==== PROVIDER ====
GetGitProviderAccount(GetGitProviderAccount),
ListGitProviderAccounts(ListGitProviderAccounts),
GetDockerRegistryAccount(GetDockerRegistryAccount),
ListDockerRegistryAccounts(ListDockerRegistryAccounts),
#[serde(alias = "GetDockerRegistryAccount")]
GetImageRegistryAccount(GetImageRegistryAccount),
#[serde(alias = "ListDockerRegistryAccounts")]
ListImageRegistryAccounts(ListImageRegistryAccounts),
// ==== ONBOARDING KEY ====
ListOnboardingKeys(ListOnboardingKeys),
@@ -291,18 +313,28 @@ async fn handler(
Json(request): Json<ReadRequest>,
) -> mogh_error::Result<axum::response::Response> {
let req_id = Uuid::new_v4();
let variant: ReadRequestVariant = (&request).into();
let method: ReadRequestMethod = (&request).into();
let user_id = user.id.clone();
let username = user.username.clone();
trace!(
"READ REQUEST {req_id} | METHOD: {variant} | USER: {} ({})",
user.username, user.id
req_id = req_id.to_string(),
method = method.to_string(),
user_id,
username,
"READ REQUEST",
);
let res = request.resolve(&ReadArgs { user }).await;
if let Err(e) = &res {
trace!(
"READ REQUEST {req_id} | METHOD: {variant} | ERROR: {:#}",
req_id = req_id.to_string(),
method = method.to_string(),
user_id,
username,
"READ REQUEST | ERROR: {:#}",
e.error
);
}
@@ -344,6 +376,7 @@ 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)
@@ -369,7 +402,9 @@ impl Resolve<ReadArgs> for ListSecrets {
ResourceTarget::Builder(id) => {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => None,
BuilderConfig::Server(config) => Some(config.server_id),
BuilderConfig::Server(config) => {
config.server_ids.first().cloned()
}
BuilderConfig::Aws(config) => {
secrets.extend(config.secrets);
None
@@ -424,11 +459,13 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => {}
BuilderConfig::Server(config) => {
merge_git_providers_for_server(
&mut providers,
&config.server_id,
)
.await?;
if let Some(server_id) = config.server_ids.first() {
merge_git_providers_for_server(
&mut providers,
server_id,
)
.await?;
}
}
BuilderConfig::Aws(config) => {
merge_git_providers(
@@ -450,18 +487,24 @@ 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(),
&[]
@@ -513,33 +556,35 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
//
impl Resolve<ReadArgs> for ListDockerRegistriesFromConfig {
impl Resolve<ReadArgs> for ListImageRegistriesFromConfig {
async fn resolve(
self,
_: &ReadArgs,
) -> mogh_error::Result<ListDockerRegistriesFromConfigResponse> {
let mut registries = core_config().docker_registries.clone();
) -> mogh_error::Result<ListImageRegistriesFromConfigResponse> {
let mut registries = core_config().image_registries.clone();
if let Some(target) = self.target {
match target {
ResourceTarget::Server(id) => {
merge_docker_registries_for_server(&mut registries, &id)
merge_image_registries_for_server(&mut registries, &id)
.await?;
}
ResourceTarget::Builder(id) => {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => {}
BuilderConfig::Server(config) => {
merge_docker_registries_for_server(
&mut registries,
&config.server_id,
)
.await?;
if let Some(server_id) = config.server_ids.first() {
merge_image_registries_for_server(
&mut registries,
server_id,
)
.await?;
}
}
BuilderConfig::Aws(config) => {
merge_docker_registries(
merge_image_registries(
&mut registries,
config.docker_registries,
config.image_registries,
);
}
}
@@ -597,14 +642,14 @@ fn merge_git_providers(
}
}
async fn merge_docker_registries_for_server(
registries: &mut Vec<DockerRegistry>,
async fn merge_image_registries_for_server(
registries: &mut Vec<ImageRegistry>,
server_id: &str,
) -> mogh_error::Result<()> {
let server = resource::get::<Server>(server_id).await?;
let more = periphery_client(&server)
.await?
.request(periphery_client::api::ListDockerRegistries {})
.request(periphery_client::api::ListImageRegistries {})
.await
.with_context(|| {
format!(
@@ -612,13 +657,13 @@ async fn merge_docker_registries_for_server(
server.name
)
})?;
merge_docker_registries(registries, more);
merge_image_registries(registries, more);
Ok(())
}
fn merge_docker_registries(
registries: &mut Vec<DockerRegistry>,
more: Vec<DockerRegistry>,
fn merge_image_registries(
registries: &mut Vec<ImageRegistry>,
more: Vec<ImageRegistry>,
) {
for incoming_registry in more {
if let Some(registry) = registries
+63 -12
View File
@@ -3,19 +3,21 @@ use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel,
procedure::{Procedure, ProcedureState},
procedure::{
Procedure, ProcedureListItem, ProcedureSortBy, ProcedureState,
},
},
};
use mogh_resolver::Resolve;
use crate::{
helpers::query::get_all_tags,
helpers::query::{get_all_tags, get_procedure_state},
permission::get_check_permissions,
resource,
state::{action_states, procedure_state_cache},
};
use super::ReadArgs;
use super::{ReadArgs, list_limit};
impl Resolve<ReadArgs> for GetProcedure {
async fn resolve(
@@ -43,15 +45,45 @@ impl Resolve<ReadArgs> for ListProcedures {
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Procedure>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.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)
},
)
.await?;
Ok(procedures)
}
}
@@ -65,12 +97,29 @@ 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::<Procedure>(
resource::list_full_for_user_filtered::<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?,
)
@@ -84,6 +133,8 @@ 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 GetDockerRegistryAccount {
impl Resolve<ReadArgs> for GetImageRegistryAccount {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<GetDockerRegistryAccountResponse> {
) -> mogh_error::Result<GetImageRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can read docker registry accounts")
@@ -83,11 +83,11 @@ impl Resolve<ReadArgs> for GetDockerRegistryAccount {
}
}
impl Resolve<ReadArgs> for ListDockerRegistryAccounts {
impl Resolve<ReadArgs> for ListImageRegistryAccounts {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListDockerRegistryAccountsResponse> {
) -> mogh_error::Result<ListImageRegistryAccountsResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can read docker registry accounts")
+58 -11
View File
@@ -3,7 +3,9 @@ use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel,
repo::{Repo, RepoActionState, RepoListItem, RepoState},
repo::{
Repo, RepoActionState, RepoListItem, RepoSortBy, RepoState,
},
},
};
use mogh_resolver::Resolve;
@@ -15,7 +17,7 @@ use crate::{
state::{action_states, repo_state_cache},
};
use super::ReadArgs;
use super::{ReadArgs, list_limit};
impl Resolve<ReadArgs> for GetRepo {
async fn resolve(
@@ -43,15 +45,41 @@ impl Resolve<ReadArgs> for ListRepos {
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Repo>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.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),
)
.await?;
Ok(repos)
}
}
@@ -65,12 +93,29 @@ 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::<Repo>(
resource::list_full_for_user_filtered::<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?,
)
@@ -105,6 +150,8 @@ 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(),
&[],
+68 -15
View File
@@ -1,13 +1,15 @@
use std::cmp::Ordering;
use futures_util::future::join_all;
use komodo_client::{
api::read::*,
entities::{
ResourceTarget,
action::Action,
action::{Action, ActionQuerySpecifics},
permission::PermissionLevel,
procedure::Procedure,
procedure::{Procedure, ProcedureQuerySpecifics},
resource::{ResourceQuery, TemplatesQueryBehavior},
schedule::Schedule,
schedule::{Schedule, ScheduleSortBy},
},
};
use mogh_resolver::Resolve;
@@ -18,7 +20,7 @@ use crate::{
schedule::get_schedule_item_info,
};
use super::ReadArgs;
use super::{ReadArgs, list_limit};
impl Resolve<ReadArgs> for ListSchedules {
async fn resolve(
@@ -29,24 +31,36 @@ 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(),
specific: Default::default(),
terms: self.terms.clone(),
specific: ActionQuerySpecifics {
scheduled: Some(true),
..Default::default()
},
..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(),
specific: Default::default(),
terms: self.terms,
specific: ProcedureQuerySpecifics {
scheduled: Some(true),
..Default::default()
},
..Default::default()
},
None,
None,
&args.user,
PermissionLevel::Read.into(),
&all_tags,
@@ -96,12 +110,51 @@ impl Resolve<ReadArgs> for ListSchedules {
let (actions, procedures) =
tokio::join!(join_all(actions), join_all(procedures));
Ok(
actions
.into_iter()
.chain(procedures)
.filter(|s| !s.schedule.is_empty())
.collect(),
)
// 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())
}
}
+113 -546
View File
@@ -1,5 +1,5 @@
use std::{
cmp,
cmp::Ordering,
collections::HashMap,
sync::{Arc, OnceLock},
};
@@ -15,47 +15,31 @@ 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, ServerQuery,
Server, ServerActionState, ServerListItem, ServerSortBy,
ServerState,
},
stack::{Stack, StackServiceNames},
stats::{SystemInformation, SystemProcess},
update::Log,
stats::{MinimalSystemStats, SystemInformation, SystemProcess},
},
};
use mogh_error::AddStatusCode;
use mogh_resolver::Resolve;
use periphery_client::api::{
self as periphery,
container::InspectContainer,
docker::{
ImageHistory, InspectImage, InspectNetwork, InspectVolume,
},
};
use periphery_client::api as periphery;
use reqwest::StatusCode;
use tokio::sync::Mutex;
use crate::{
helpers::{periphery_client, query::get_all_tags},
permission::{get_check_permissions, list_resources_for_user},
helpers::{
periphery_client,
query::{get_all_tags, get_cached_server_state},
},
permission::get_check_permissions,
resource,
stack::compose_container_match_regex,
state::{action_states, db_client, server_status_cache},
};
use super::ReadArgs;
use super::{ReadArgs, list_limit};
impl Resolve<ReadArgs> for GetServersSummary {
async fn resolve(
@@ -64,6 +48,8 @@ impl Resolve<ReadArgs> for GetServersSummary {
) -> mogh_error::Result<GetServersSummaryResponse> {
let servers = resource::list_for_user::<Server>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
@@ -125,15 +111,89 @@ impl Resolve<ReadArgs> for ListServers {
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Server>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.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)
},
)
.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
}
}
@@ -147,12 +207,29 @@ 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::<Server>(
resource::list_full_for_user_filtered::<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?,
)
@@ -339,7 +416,7 @@ impl Resolve<ReadArgs> for GetHistoricalServerStats {
let curr_ts = unix_timestamp_ms() as i64;
let mut curr_ts = curr_ts
- curr_ts % granularity
- granularity * STATS_PER_PAGE * page as i64;
- granularity * (page as i64).saturating_mul(STATS_PER_PAGE);
for _ in 0..STATS_PER_PAGE {
ts_vec.push(curr_ts);
curr_ts -= granularity;
@@ -353,7 +430,6 @@ 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(),
)
@@ -368,512 +444,3 @@ 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)
// }
// }
+205 -61
View File
@@ -9,7 +9,10 @@ use komodo_client::{
container::Container, service::SwarmService, stack::SwarmStack,
},
permission::PermissionLevel,
stack::{Stack, StackActionState, StackListItem, StackState},
stack::{
Stack, StackActionState, StackListItem, StackQuery,
StackService, StackSortBy, StackState,
},
},
};
use mogh_error::AddStatusCodeError as _;
@@ -22,7 +25,9 @@ use reqwest::StatusCode;
use crate::{
helpers::{
periphery_client, query::get_all_tags, swarm::swarm_request,
periphery_client,
query::{get_all_tags, get_cached_stack_state},
swarm::swarm_request,
},
permission::get_check_permissions,
resource,
@@ -30,7 +35,7 @@ use crate::{
state::{action_states, stack_status_cache},
};
use super::ReadArgs;
use super::{ReadArgs, list_limit};
impl Resolve<ReadArgs> for GetStack {
async fn resolve(
@@ -48,6 +53,128 @@ 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,
@@ -72,6 +199,75 @@ 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,
@@ -339,6 +535,8 @@ impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
};
let stacks = resource::list_full_for_user::<Stack>(
self.query,
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
@@ -373,6 +571,8 @@ impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
};
let stacks = resource::list_full_for_user::<Stack>(
self.query,
None,
None,
user,
PermissionLevel::Read.into(),
&all_tags,
@@ -395,64 +595,6 @@ 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,
@@ -481,6 +623,8 @@ 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(),
&[],
+30 -3
View File
@@ -3,7 +3,9 @@ use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel,
swarm::{Swarm, SwarmActionState, SwarmListItem, SwarmState},
swarm::{
Swarm, SwarmActionState, SwarmListItem, SwarmSortBy, SwarmState,
},
},
};
use mogh_resolver::Resolve;
@@ -15,7 +17,7 @@ use crate::{
state::{action_states, server_status_cache, swarm_status_cache},
};
use super::ReadArgs;
use super::{ReadArgs, list_limit};
impl Resolve<ReadArgs> for GetSwarm {
async fn resolve(
@@ -43,12 +45,32 @@ 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_for_user::<Swarm>(
resource::list_items_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?,
)
@@ -65,9 +87,12 @@ 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,
@@ -105,6 +130,8 @@ 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(),
&[],
+49 -3
View File
@@ -5,6 +5,7 @@ use komodo_client::{
permission::PermissionLevel,
sync::{
ResourceSync, ResourceSyncActionState, ResourceSyncListItem,
ResourceSyncSortBy,
},
},
};
@@ -15,7 +16,7 @@ use crate::{
resource, state::action_states,
};
use super::ReadArgs;
use super::{ReadArgs, list_limit};
impl Resolve<ReadArgs> for GetResourceSync {
async fn resolve(
@@ -43,12 +44,52 @@ 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_for_user::<ResourceSync>(
resource::list_items_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?,
)
@@ -65,9 +106,12 @@ 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,
@@ -106,6 +150,8 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
let resource_syncs =
resource::list_full_for_user::<ResourceSync>(
Default::default(),
None,
None,
user,
PermissionLevel::Read.into(),
&[],
@@ -120,7 +166,7 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
for resource_sync in resource_syncs {
res.total += 1;
if !(resource_sync.info.pending_deploy.to_deploy == 0
if !(resource_sync.info.pending_deploys.is_empty()
&& resource_sync.info.resource_updates.is_empty()
&& resource_sync.info.variable_updates.is_empty()
&& resource_sync.info.user_group_updates.is_empty())
+118 -56
View File
@@ -1,3 +1,5 @@
use std::cmp::Ordering;
use anyhow::Context as _;
use futures_util::{
FutureExt, StreamExt as _, stream::FuturesUnordered,
@@ -9,7 +11,7 @@ use komodo_client::{
permission::PermissionLevel,
server::Server,
stack::Stack,
terminal::{Terminal, TerminalTarget},
terminal::{Terminal, TerminalSortBy, TerminalTarget},
user::User,
},
};
@@ -22,7 +24,7 @@ use crate::{
resource,
};
use super::ReadArgs;
use super::{ReadArgs, list_limit};
//
@@ -31,57 +33,111 @@ impl Resolve<ReadArgs> for ListTerminals {
self,
ReadArgs { user }: &ReadArgs,
) -> mogh_error::Result<ListTerminalsResponse> {
let Some(target) = self.target else {
return list_all_terminals_for_user(user, self.use_names).await;
let mut terminals = match self.target {
None => {
list_all_terminals_for_user(user, self.use_names).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?
}
},
};
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())
}
}
@@ -92,6 +148,8 @@ 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(),
&[]
@@ -103,12 +161,16 @@ 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(),
&[]
@@ -139,7 +201,7 @@ async fn list_all_terminals_for_user(
}
}
let mut terminals = servers
let terminals = servers
.into_iter()
.map(|(server, server_permission)| async move {
(
@@ -168,6 +230,7 @@ async fn list_all_terminals_for_user(
server_id.clone()
}),
};
terminal.target_name = Some(server_name.clone());
terminal
}),
TerminalTarget::Container { container, .. } => {
@@ -180,6 +243,7 @@ async fn list_all_terminals_for_user(
},
container,
};
terminal.target_name = Some(server_name.clone());
terminal
})
}
@@ -193,6 +257,7 @@ async fn list_all_terminals_for_user(
},
service,
};
terminal.target_name = Some(s.name.clone());
terminal
})
}
@@ -206,6 +271,7 @@ async fn list_all_terminals_for_user(
d.id.clone()
},
};
terminal.target_name = Some(d.name.clone());
terminal
},
)
@@ -220,10 +286,6 @@ 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)
}
+123 -340
View File
@@ -1,5 +1,8 @@
use anyhow::Context;
use database::mungos::find::find_collect;
use database::{
bson::doc,
mungos::{find::find_collect, mongodb::options::FindOptions},
};
use komodo_client::{
api::read::{
ExportAllResourcesToToml, ExportAllResourcesToTomlResponse,
@@ -25,6 +28,7 @@ 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,
@@ -38,122 +42,47 @@ 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?
};
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)),
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,
);
Ok(targets)
}
@@ -187,6 +116,7 @@ impl Resolve<ReadArgs> for ExportAllResourcesToToml {
targets,
user_groups,
include_variables: self.include_variables,
existing: self.existing,
}
.resolve(args)
.await
@@ -202,131 +132,64 @@ 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::Swarm(id) => {
let mut swarm = get_check_permissions::<Swarm>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Swarm::replace_ids(&mut swarm);
res.swarms.push(convert_resource::<Swarm>(
swarm,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Server(id) => {
let mut server = get_check_permissions::<Server>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Server::replace_ids(&mut server);
res.servers.push(convert_resource::<Server>(
server,
false,
vec![],
&id_to_tags,
))
convert_target!(id, Server, servers)
}
ResourceTarget::Swarm(id) => {
convert_target!(id, Swarm, swarms)
}
ResourceTarget::Stack(id) => {
let mut stack = get_check_permissions::<Stack>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Stack::replace_ids(&mut stack);
res.stacks.push(convert_resource::<Stack>(
stack,
false,
vec![],
&id_to_tags,
))
convert_target!(id, Stack, stacks)
}
ResourceTarget::Deployment(id) => {
let mut deployment = get_check_permissions::<Deployment>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Deployment::replace_ids(&mut deployment);
res.deployments.push(convert_resource::<Deployment>(
deployment,
false,
vec![],
&id_to_tags,
))
convert_target!(id, Deployment, deployments)
}
ResourceTarget::Build(id) => {
let mut build = get_check_permissions::<Build>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Build::replace_ids(&mut build);
res.builds.push(convert_resource::<Build>(
build,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Repo(id) => {
let mut repo = get_check_permissions::<Repo>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Repo::replace_ids(&mut repo);
res.repos.push(convert_resource::<Repo>(
repo,
false,
vec![],
&id_to_tags,
))
convert_target!(id, Build, builds)
}
ResourceTarget::Repo(id) => convert_target!(id, Repo, repos),
ResourceTarget::Procedure(id) => {
let mut procedure = get_check_permissions::<Procedure>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Procedure::replace_ids(&mut procedure);
res.procedures.push(convert_resource::<Procedure>(
procedure,
false,
vec![],
&id_to_tags,
));
convert_target!(id, Procedure, procedures)
}
ResourceTarget::Action(id) => {
let mut action = get_check_permissions::<Action>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Action::replace_ids(&mut action);
res.actions.push(convert_resource::<Action>(
action,
false,
vec![],
&id_to_tags,
));
convert_target!(id, Action, actions)
}
ResourceTarget::Builder(id) => {
convert_target!(id, Builder, builders)
}
ResourceTarget::Alerter(id) => {
convert_target!(id, Alerter, alerters)
}
ResourceTarget::ResourceSync(id) => {
let mut sync = get_check_permissions::<ResourceSync>(
@@ -340,7 +203,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|| !sync.config.repo.is_empty()
|| !sync.config.linked_repo.is_empty())
{
ResourceSync::replace_ids(&mut sync);
ResourceSync::replace_ids(&mut sync.config);
res.resource_syncs.push(convert_resource::<ResourceSync>(
sync,
false,
@@ -349,36 +212,6 @@ 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,
};
}
@@ -388,18 +221,21 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
.context("failed to add user groups")?;
if include_variables {
res.variables =
find_collect(&db_client().variables, None, None)
.await
.context("failed to get variables from db")?
.into_iter()
.map(|mut variable| {
if !user.admin && variable.is_secret {
variable.value = "#".repeat(variable.value.len())
}
variable
})
.collect();
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();
}
let toml = serialize_resources_toml(res)
@@ -434,85 +270,32 @@ fn serialize_resources_toml(
) -> anyhow::Result<String> {
let mut toml = String::new();
for server in resources.servers {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[server]]\n");
Server::push_to_toml_string(server, &mut toml)?;
}
for stack in resources.stacks {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[stack]]\n");
Stack::push_to_toml_string(stack, &mut toml)?;
}
for deployment in resources.deployments {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[deployment]]\n");
Deployment::push_to_toml_string(deployment, &mut toml)?;
}
for build in resources.builds {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[build]]\n");
Build::push_to_toml_string(build, &mut toml)?;
}
for repo in resources.repos {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[repo]]\n");
Repo::push_to_toml_string(repo, &mut toml)?;
}
for procedure in resources.procedures {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[procedure]]\n");
Procedure::push_to_toml_string(procedure, &mut toml)?;
}
for action in resources.actions {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[action]]\n");
Action::push_to_toml_string(action, &mut toml)?;
}
for alerter in resources.alerters {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[alerter]]\n");
Alerter::push_to_toml_string(alerter, &mut toml)?;
}
for builder in resources.builders {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[builder]]\n");
Builder::push_to_toml_string(builder, &mut toml)?;
}
for resource_sync in resources.resource_syncs {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[resource_sync]]\n");
ResourceSync::push_to_toml_string(resource_sync, &mut toml)?;
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)?;
}
)*
};
}
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() {
+3 -1
View File
@@ -47,7 +47,9 @@ impl Resolve<ReadArgs> for ListUpdates {
query,
FindOptions::builder()
.sort(doc! { "start_ts": -1 })
.skip(self.page as u64 * UPDATES_PER_PAGE as u64)
.skip(
(self.page as u64).saturating_mul(UPDATES_PER_PAGE as u64),
)
.limit(UPDATES_PER_PAGE)
.build(),
)
+12 -1
View File
@@ -71,9 +71,20 @@ 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,
None,
filter,
FindOptions::builder().sort(doc! { "username": 1 }).build(),
)
.await
+12 -49
View File
@@ -1,10 +1,8 @@
use std::{path::PathBuf, time::Duration};
use std::path::PathBuf;
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::*,
@@ -15,7 +13,6 @@ use komodo_client::{
builder::{Builder, BuilderConfig},
permission::PermissionLevel,
repo::Repo,
server::ServerState,
update::Update,
},
};
@@ -25,12 +22,11 @@ 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, periphery_client,
query::get_server_with_state,
git_token,
update::{add_update, make_update},
},
periphery::PeripheryClient,
@@ -476,48 +472,15 @@ async fn get_on_host_periphery(
.await
.context("Failed to get 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
}
if let BuilderConfig::Aws(_) = builder.config {
return Err(anyhow!(
"Files on host doesn't work with AWS builder"
));
}
connect_builder_periphery(build.name.clone(), None, builder, None)
.await
.map(|(periphery, _)| periphery)
}
/// The successful case will be included as Some(remote_contents).
+67 -13
View File
@@ -15,6 +15,7 @@ use komodo_client::{
docker::container::RestartPolicyNameEnum,
komodo_timestamp, optional_string,
permission::PermissionLevel,
resource::ResourceQuery,
server::{Server, ServerState},
to_container_compatible_name,
update::Update,
@@ -30,7 +31,9 @@ use crate::{
api::execute::{self, ExecuteRequest, ExecutionResult},
helpers::{
periphery_client,
query::{get_deployment_state, get_swarm_or_server},
query::{
get_all_tags, get_deployment_state, get_swarm_or_server,
},
registry_token,
update::{add_update, make_update, poll_update_until_complete},
},
@@ -300,10 +303,24 @@ impl Resolve<WriteArgs> for RenameDeployment {
let name = to_container_compatible_name(&self.name);
let container_state =
get_deployment_state(&deployment.id).await?;
let state = get_deployment_state(&deployment.id).await?;
if container_state == DeploymentState::Unknown {
// 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 {
return Err(
anyhow!(
"Cannot rename Deployment when container status is unknown"
@@ -312,27 +329,51 @@ 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(
doc! { "name": &name, "updated_at": komodo_timestamp() },
),
database::mungos::update::Update::Set(set),
None,
)
.await
.context("Failed to update Deployment name on db")?;
if container_state != DeploymentState::NotDeployed {
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 {
let server =
resource::get::<Server>(&deployment.config.server_id).await?;
let log = periphery_client(&server)
.await?
.request(api::container::RenameContainer {
curr_name: deployment.name.clone(),
curr_name: deployment.deployed_name().to_string(),
new_name: name.clone(),
})
.await
@@ -436,7 +477,7 @@ pub async fn check_deployment_for_update_inner(
update_available: 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 {
@@ -458,13 +499,14 @@ pub async fn check_deployment_for_update_inner(
};
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?;
@@ -621,6 +663,7 @@ impl Resolve<WriteArgs> for BatchCheckDeploymentForUpdate {
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,
)
@@ -629,12 +672,23 @@ impl Resolve<WriteArgs> for BatchCheckDeploymentForUpdate {
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,
Default::default(),
ResourceQuery {
tags: self.tags,
..Default::default()
},
None,
None,
user,
PermissionLevel::Execute.into(),
&[],
&all_tags,
)
.await?;
+30 -11
View File
@@ -54,7 +54,7 @@ pub struct WriteArgs {
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants,
)]
#[strum_discriminants(name(WriteRequestVariant), derive(Display))]
#[strum_discriminants(name(WriteRequestMethod), derive(Display))]
#[args(WriteArgs)]
#[response(Response)]
#[error(mogh_error::Error)]
@@ -211,9 +211,12 @@ pub enum WriteRequest {
CreateGitProviderAccount(CreateGitProviderAccount),
UpdateGitProviderAccount(UpdateGitProviderAccount),
DeleteGitProviderAccount(DeleteGitProviderAccount),
CreateDockerRegistryAccount(CreateDockerRegistryAccount),
UpdateDockerRegistryAccount(UpdateDockerRegistryAccount),
DeleteDockerRegistryAccount(DeleteDockerRegistryAccount),
#[serde(alias = "CreateDockerRegistryAccount")]
CreateImageRegistryAccount(CreateImageRegistryAccount),
#[serde(alias = "UpdateDockerRegistryAccount")]
UpdateImageRegistryAccount(UpdateImageRegistryAccount),
#[serde(alias = "DeleteDockerRegistryAccount")]
DeleteImageRegistryAccount(DeleteImageRegistryAccount),
// ==== ALERT ====
CloseAlert(CloseAlert),
@@ -255,19 +258,35 @@ async fn task(
request: WriteRequest,
user: User,
) -> mogh_error::Result<axum::response::Response> {
let req_id = Uuid::new_v4();
let variant: WriteRequestVariant = (&request).into();
let task_id = Uuid::new_v4();
let method: WriteRequestMethod = (&request).into();
info!(
"WRITE REQUEST {req_id} | METHOD: {variant} | USER: {} ({})",
user.username, user.id
);
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",
);
}
let res = request.resolve(&WriteArgs { user }).await;
if let Err(e) = &res {
warn!(
"WRITE REQUEST {req_id} | METHOD: {variant} | ERROR: {:#}",
task_id = task_id.to_string(),
method = method.to_string(),
user_id,
username,
"WRITE REQUEST | ERROR: {:#}",
e.error
);
}
+38 -5
View File
@@ -15,7 +15,10 @@ use mogh_pki::EncodedKeyPair;
use mogh_resolver::Resolve;
use reqwest::StatusCode;
use crate::{api::write::WriteArgs, state::db_client};
use crate::{
api::write::WriteArgs, helpers::query::get_all_tags,
state::db_client,
};
//
@@ -53,6 +56,21 @@ 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,
@@ -60,8 +78,8 @@ impl Resolve<WriteArgs> for CreateOnboardingKey {
onboarded: Default::default(),
created_at: komodo_timestamp(),
expires: self.expires,
tags: self.tags,
fix_existing_servers: self.fix_existing_servers,
tags,
privileged: self.privileged,
copy_server: self.copy_server,
create_builder: self.create_builder,
};
@@ -139,11 +157,26 @@ 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(fix_existing_servers) = self.fix_existing_servers {
update.insert("fix_existing_servers", fix_existing_servers);
if let Some(privileged) = self.privileged {
update.insert("privileged", privileged);
}
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 server found")?
.context("No matching swarm found")?
.id;
Ok((ResourceTargetVariant::Server, id))
Ok((ResourceTargetVariant::Swarm, 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::{DockerRegistryAccount, GitProviderAccount},
provider::{GitProviderAccount, ImageRegistryAccount},
},
};
use mogh_error::AddStatusCodeError;
@@ -248,7 +248,7 @@ impl Resolve<WriteArgs> for DeleteGitProviderAccount {
}
}
impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
impl Resolve<WriteArgs> for CreateImageRegistryAccount {
#[instrument(
"CreateDockerRegistryAccount",
skip_all,
@@ -261,7 +261,7 @@ impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<CreateDockerRegistryAccountResponse> {
) -> mogh_error::Result<CreateImageRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!(
@@ -271,7 +271,7 @@ impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
);
}
let mut account: DockerRegistryAccount = self.account.into();
let mut account: ImageRegistryAccount = self.account.into();
if account.domain.is_empty() {
return Err(
@@ -326,7 +326,7 @@ impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
}
}
impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
impl Resolve<WriteArgs> for UpdateImageRegistryAccount {
#[instrument(
"UpdateDockerRegistryAccount",
skip_all,
@@ -340,7 +340,7 @@ impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
async fn resolve(
mut self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<UpdateDockerRegistryAccountResponse> {
) -> mogh_error::Result<UpdateImageRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can update docker registry accounts")
@@ -423,7 +423,7 @@ impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
}
}
impl Resolve<WriteArgs> for DeleteDockerRegistryAccount {
impl Resolve<WriteArgs> for DeleteImageRegistryAccount {
#[instrument(
"DeleteDockerRegistryAccount",
skip_all,
@@ -435,7 +435,7 @@ impl Resolve<WriteArgs> for DeleteDockerRegistryAccount {
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<DeleteDockerRegistryAccountResponse> {
) -> mogh_error::Result<DeleteImageRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can delete docker registry accounts")
+42 -10
View File
@@ -19,6 +19,7 @@ use komodo_client::{
all_logs_success, komodo_timestamp,
permission::PermissionLevel,
repo::Repo,
resource::ResourceQuery,
stack::{Stack, StackInfo, StackServiceWithUpdate, StackState},
update::Update,
user::{auto_redeploy_user, stack_user, system_user},
@@ -36,7 +37,7 @@ use crate::{
api::execute::{self, ExecuteRequest, ExecutionResult},
config::core_config,
helpers::{
query::get_swarm_or_server,
query::{get_all_tags, get_swarm_or_server},
stack_git_token, swarm_or_server_request,
update::{add_update, make_update, poll_update_until_complete},
},
@@ -555,15 +556,25 @@ impl Resolve<WriteArgs> for RefreshStackCache {
&mut services,
) {
warn!(
"failed to extract stack services, things won't works correctly. stack: {} | {e:#}",
stack.name
stack = stack.id,
stack_name = stack.name,
"Failed to extract stack services | {e:#}",
);
}
}
(services, Some(contents), Some(errors), None, None)
} else {
(vec![], None, None, None, None)
// 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,
)
}
} else if !repo_empty {
// ================
@@ -766,12 +777,18 @@ pub async fn check_stack_for_update_inner(
if image.is_empty() ||
// Images with a hardcoded digest can't have update.
image.contains('@')
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_digest = None;
continue;
}
match cache.get(&swarm_or_server, image, None, None).await {
match cache.get(swarm_or_server, image, None, None).await {
Ok(digest) => service.image_digest = Some(digest),
Err(e) => {
warn!(
@@ -861,7 +878,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_digests);
if service_with_update.update_available
&& (skip_auto_update || !stack.config.auto_update)
@@ -930,7 +947,10 @@ 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 {
let deploy_services = if stack.config.auto_update_all_services
// Swarm stacks don't support individual service deploy
|| !stack.config.swarm_id.is_empty()
{
Vec::new()
} else {
services_with_update
@@ -1021,6 +1041,7 @@ impl Resolve<WriteArgs> for BatchCheckStackForUpdate {
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,
)
@@ -1029,12 +1050,23 @@ impl Resolve<WriteArgs> for BatchCheckStackForUpdate {
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,
Default::default(),
ResourceQuery {
tags: self.tags,
..Default::default()
},
None,
None,
user,
PermissionLevel::Execute.into(),
&[],
&all_tags,
)
.await?;
+46 -105
View File
@@ -26,7 +26,8 @@ use komodo_client::{
repo::Repo,
server::Server,
stack::Stack,
sync::{ResourceSync, ResourceSyncInfo, SyncDeployUpdate},
swarm::Swarm,
sync::{ResourceSync, ResourceSyncInfo},
to_path_compatible_name,
update::{Log, Update},
user::sync_user,
@@ -515,11 +516,20 @@ 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(),
@@ -775,110 +785,40 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
let mut diffs = Vec::new();
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?;
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),
);
(diffs, deploy_updates)
} else {
(Vec::new(), SyncDeployUpdate::default())
Default::default()
};
let variable_updates = if sync.config.include_variables {
@@ -912,7 +852,7 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
let (
resource_updates,
deploy_updates,
(pending_deploys, pending_deploy_error),
variable_updates,
user_group_updates,
pending_error,
@@ -928,7 +868,7 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
};
let has_updates = !resource_updates.is_empty()
|| !deploy_updates.to_deploy == 0
|| !pending_deploys.is_empty()
|| !variable_updates.is_empty()
|| !user_group_updates.is_empty();
@@ -940,7 +880,8 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
remote_errors: sync.info.remote_errors,
pending_hash: sync.info.pending_hash,
pending_message: sync.info.pending_message,
pending_deploy: deploy_updates,
pending_deploys,
pending_deploy_error,
resource_updates,
variable_updates,
user_group_updates,
+51 -31
View File
@@ -3,8 +3,12 @@ use futures_util::{StreamExt as _, stream::FuturesUnordered};
use komodo_client::{
api::write::*,
entities::{
NoData, deployment::Deployment, permission::PermissionLevel,
server::Server, stack::Stack, terminal::TerminalTarget,
NoData,
deployment::Deployment,
permission::PermissionLevel,
server::Server,
stack::Stack,
terminal::{Terminal, TerminalTarget},
user::User,
},
};
@@ -47,30 +51,35 @@ impl Resolve<WriteArgs> for CreateTerminal {
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<NoData> {
) -> mogh_error::Result<Terminal> {
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?;
create_server_terminal(self, server, user)
.await
.map_err(Into::into)
}
TerminalTarget::Container { server, container } => {
create_container_terminal(self, server, container, user)
.await?;
.await
.map_err(Into::into)
}
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?;
.await
.map_err(Into::into)
}
TerminalTarget::Deployment { deployment } => {
create_deployment_terminal(self, deployment, user).await?;
create_deployment_terminal(self, deployment, user)
.await
.map_err(Into::into)
}
};
Ok(NoData {})
}
}
}
@@ -84,7 +93,7 @@ async fn create_server_terminal(
}: CreateTerminal,
server: String,
user: &User,
) -> anyhow::Result<()> {
) -> anyhow::Result<Terminal> {
let server = get_check_permissions::<Server>(
&server,
user,
@@ -94,7 +103,7 @@ async fn create_server_terminal(
let periphery = periphery_client(&server).await?;
periphery
let mut terminal = periphery
.request(api::terminal::CreateServerTerminal {
name,
command,
@@ -103,7 +112,12 @@ async fn create_server_terminal(
.await
.context("Failed to create Server Terminal on Periphery")?;
Ok(())
// Fix server terminal target with server id
terminal.target = TerminalTarget::Server {
server: Some(server.id),
};
Ok(terminal)
}
async fn create_container_terminal(
@@ -111,7 +125,7 @@ async fn create_container_terminal(
server: String,
container: String,
user: &User,
) -> anyhow::Result<()> {
) -> anyhow::Result<Terminal> {
let server = get_check_permissions::<Server>(
&server,
user,
@@ -127,7 +141,7 @@ async fn create_stack_service_terminal(
stack: String,
service: String,
user: &User,
) -> anyhow::Result<()> {
) -> anyhow::Result<Terminal> {
let (_, periphery, container) =
get_stack_service_periphery_container(&stack, &service, user)
.await?;
@@ -138,7 +152,7 @@ async fn create_deployment_terminal(
req: CreateTerminal,
deployment: String,
user: &User,
) -> anyhow::Result<()> {
) -> anyhow::Result<Terminal> {
let (_, periphery, container) =
get_deployment_periphery_container(&deployment, user).await?;
create_container_terminal_inner(req, &periphery, container).await
@@ -157,10 +171,10 @@ impl Resolve<WriteArgs> for DeleteTerminal {
)
)]
async fn resolve(
self,
mut self,
WriteArgs { user }: &WriteArgs,
) -> mogh_error::Result<NoData> {
let server = match &self.target {
let server = match &mut self.target {
TerminalTarget::Server { server } => {
let server = server
.as_ref()
@@ -181,27 +195,31 @@ impl Resolve<WriteArgs> for DeleteTerminal {
)
.await?
}
TerminalTarget::Stack { stack, .. } => {
let server = get_check_permissions::<Stack>(
stack,
TerminalTarget::Stack {
stack: stack_id, ..
} => {
let stack = get_check_permissions::<Stack>(
stack_id,
user,
PermissionLevel::Read.terminal(),
)
.await?
.config
.server_id;
resource::get::<Server>(&server).await?
.await?;
// Must fix any incoming stack name to id
*stack_id = stack.id;
resource::get::<Server>(&stack.config.server_id).await?
}
TerminalTarget::Deployment { deployment } => {
let server = get_check_permissions::<Deployment>(
deployment,
TerminalTarget::Deployment {
deployment: deployment_id,
} => {
let deployment = get_check_permissions::<Deployment>(
deployment_id,
user,
PermissionLevel::Read.terminal(),
)
.await?
.config
.server_id;
resource::get::<Server>(&server).await?
.await?;
// Must fix any incoming deployment name to id
*deployment_id = deployment.id;
resource::get::<Server>(&deployment.config.server_id).await?
}
};
@@ -275,6 +293,8 @@ 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,6 +36,15 @@ 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,
@@ -79,6 +88,12 @@ 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,6 +24,9 @@ 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
+35 -6
View File
@@ -5,6 +5,7 @@ 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,
@@ -116,9 +117,25 @@ 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
match periphery_receiver.recv().await {
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 {
Ok(Ok(bytes)) => {
if let Err(e) =
client_send.send(ws::Message::Binary(bytes.into())).await
@@ -128,14 +145,26 @@ async fn forward_ws_channel(
};
}
Ok(Err(e)) => {
let _ = client_send
.send(ws::Message::text(format!("{e:#}")))
.await;
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;
}
break;
}
Err(_) => {
let _ =
client_send.send(ws::Message::text("STREAM EOF")).await;
let _ = client_send
.send(ws::Message::text("\r\n\nSTREAM EOF"))
.await;
break;
}
}
+11 -1
View File
@@ -3,6 +3,7 @@ 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,
@@ -38,10 +39,19 @@ 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.
// poll for updates off the receiver / await cancel / send keep-alive ping.
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")}
};
+15
View File
@@ -205,6 +205,20 @@ 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,
@@ -348,6 +362,7 @@ 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: _,
docker_registries: _,
image_registries: _,
secrets: _,
} = config;
let instance_type = handle_unknown_instance_type(
+3 -2
View File
@@ -2,8 +2,9 @@ pub mod aws;
#[derive(Debug)]
pub enum BuildCleanupData {
/// Nothing to clean up
Server,
/// Store the builder id if it needs
/// to be removed from builder_usage_cache.
Server(Option<String>),
/// Cleanup Periphery connection
Url,
/// Clean up AWS instance
+38 -4
View File
@@ -235,6 +235,9 @@ 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
@@ -312,6 +315,21 @@ pub fn core_config() -> &'static CoreConfig {
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),
resource_poll_interval: env
.komodo_resource_poll_interval
.unwrap_or(config.resource_poll_interval),
@@ -330,6 +348,9 @@ 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),
@@ -345,6 +366,12 @@ 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),
@@ -375,6 +402,9 @@ 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),
@@ -403,9 +433,13 @@ pub fn core_config() -> &'static CoreConfig {
ssl_cert_file: env
.komodo_ssl_cert_file
.unwrap_or(config.ssl_cert_file),
ui_path: env
.komodo_ui_path
.unwrap_or(config.ui_path),
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),
@@ -422,7 +456,7 @@ pub fn core_config() -> &'static CoreConfig {
// These can't be overridden on env
secrets: config.secrets,
git_providers: config.git_providers,
docker_registries: config.docker_registries,
image_registries: config.image_registries,
}
})
}
+15
View File
@@ -1,6 +1,7 @@
use std::time::Duration;
use anyhow::{Context, anyhow};
use komodo_client::entities::server::Server;
use periphery_client::{
CONNECTION_RETRY_SECONDS, transport::LoginMessage,
};
@@ -18,6 +19,7 @@ use transport::{
use crate::{
config::{core_config, core_connection_query},
monitor::refresh_server_cache,
periphery::PeripheryClient,
state::periphery_connections,
};
@@ -46,6 +48,7 @@ impl PeripheryConnectionArgs<'_> {
let responses = connection.responses.clone();
let _id = id.clone();
tokio::spawn(async move {
loop {
let ws = tokio::select! {
@@ -94,6 +97,18 @@ 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
}
});
+22 -9
View File
@@ -1,4 +1,4 @@
use std::str::FromStr;
use std::{str::FromStr, time::Duration};
use anyhow::{Context, anyhow};
use axum::{
@@ -42,6 +42,7 @@ 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},
};
@@ -163,7 +164,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 { fix_existing_servers_required: true },
public_key_validator: OnboardingKeyValidator { privileged_required: true },
should_close: true
})
.await
@@ -201,6 +202,13 @@ 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
}))
}
@@ -227,7 +235,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 { fix_existing_servers_required: true },
public_key_validator: OnboardingKeyValidator { privileged_required: true },
should_close: true,
})
.await
@@ -299,6 +307,13 @@ 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() {
@@ -341,7 +356,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 { fix_existing_servers_required: false },
public_key_validator: OnboardingKeyValidator { privileged_required: false },
should_close: true
})
.await
@@ -488,7 +503,7 @@ async fn create_server_maybe_builder(
name: server_query,
config: PartialBuilderConfig::Server(
PartialServerBuilderConfig {
server_id: Some(server.id.clone()),
server_ids: Some(vec![server.id.clone()]),
},
),
})
@@ -505,7 +520,7 @@ async fn create_server_maybe_builder(
}
struct OnboardingKeyValidator {
fix_existing_servers_required: bool,
privileged_required: bool,
}
impl PublicKeyValidator for OnboardingKeyValidator {
@@ -527,9 +542,7 @@ impl PublicKeyValidator for OnboardingKeyValidator {
{
return Err(anyhow!("Onboarding key is invalid"));
}
if self.fix_existing_servers_required
&& !onboarding_key.fix_existing_servers
{
if self.privileged_required && !onboarding_key.privileged {
return Err(anyhow!(
"Onboarding key not able to fix existing servers"
));
+112 -27
View File
@@ -1,16 +1,18 @@
use std::time::Duration;
use std::{collections::HashMap, 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,
server::{Server, ServerState},
update::{Log, Update},
};
use periphery_client::api::{self, GetVersionResponse};
use tokio::sync::Mutex;
use crate::{
cloud::{
@@ -24,6 +26,7 @@ use crate::{
helpers::update::update_update,
periphery::PeripheryClient,
resource,
state::{builder_usage_cache, server_status_cache},
};
use super::periphery_client;
@@ -37,25 +40,26 @@ const BUILDER_POLL_MAX_TRIES: usize = 60;
fields(
resource_name,
builder_id = builder.id,
update_id = update.id
update_id = update.as_ref().map(|u| u.id.as_str())
)
)]
pub async fn connect_builder_periphery(
// build: &Build,
resource_name: String,
version: Option<Version>,
builder: Builder,
update: &mut Update,
update: Option<&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"
));
}
// TODO: Dont use builder id, or will be problems
// with simultaneous spawned builders.
// Builder id no good because it may be active for multiple connections.
let periphery = PeripheryClient::new(
PeripheryConnectionArgs::from_url_builder(
&ObjectId::new().to_hex(),
@@ -64,22 +68,66 @@ pub async fn connect_builder_periphery(
config.insecure_tls,
)
.await?;
periphery
.health_check()
.await
.context("Url Builder failed health check")?;
Ok((periphery, BuildCleanupData::Url))
// 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")?)
}
BuilderConfig::Server(config) => {
if config.server_id.is_empty() {
return Err(anyhow!("Builder has not configured a server"));
if config.server_ids.is_empty() {
return Err(anyhow!(
"Server Builder has no configured Servers"
));
}
let server = resource::get::<Server>(&config.server_id).await?;
// 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 periphery = periphery_client(&server).await?;
Ok((periphery, BuildCleanupData::Server))
}
BuilderConfig::Aws(config) => {
get_aws_builder(&resource_name, version, config, update).await
Ok((periphery, BuildCleanupData::Server(Some(server.id))))
}
}
}
@@ -89,14 +137,14 @@ pub async fn connect_builder_periphery(
skip_all,
fields(
resource_name,
update_id = update.id,
update_id = update.as_ref().map(|u| u.id.as_str()),
)
)]
async fn get_aws_builder(
resource_name: &str,
version: Option<Version>,
config: AwsBuilderConfig,
update: &mut Update,
mut update: Option<&mut Update>,
) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {
let start_create_ts = komodo_timestamp();
@@ -114,9 +162,10 @@ async fn get_aws_builder(
..Default::default()
};
update.logs.push(log);
update_update(update.clone()).await?;
if let Some(update) = &mut update {
update.logs.push(log);
update_update((*update).clone()).await?;
}
let protocol = if config.use_https { "wss" } else { "ws" };
@@ -153,8 +202,10 @@ async fn get_aws_builder(
end_ts: komodo_timestamp(),
..Default::default()
};
update.logs.push(connect_log);
update_update(update.clone()).await?;
if let Some(update) = update {
update.logs.push(connect_log);
update_update(update.clone()).await?;
}
return Ok((
periphery,
BuildCleanupData::Aws {
@@ -194,9 +245,17 @@ pub async fn cleanup_builder_instance(
update: &mut Update,
) {
match cleanup_data {
BuildCleanupData::Server => {
BuildCleanupData::Server(None) => {
// 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;
}
@@ -252,3 +311,29 @@ 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;
}
}
}
+24 -24
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);
} else {
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),
)
.await;
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),
)
.await;
Ok(digest)
}
}
+1 -1
View File
@@ -169,7 +169,7 @@ pub async fn registry_token(
}
Ok(
core_config()
.docker_registries
.image_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
)
}
+33 -1
View File
@@ -45,7 +45,7 @@ use crate::{
state::{
action_state_cache, action_states, db_client,
deployment_status_cache, procedure_state_cache,
stack_status_cache,
server_status_cache, stack_status_cache,
},
};
@@ -97,6 +97,38 @@ 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> {
+1 -23
View File
@@ -1,5 +1,3 @@
use std::time::Duration;
use anyhow::anyhow;
use komodo_client::entities::server::{Server, ServerState};
use mogh_resolver::HasResponse;
@@ -13,23 +11,6 @@ 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,
@@ -54,10 +35,7 @@ where
else {
continue;
};
match periphery
.request_custom_timeout(request.clone(), timeout)
.await
{
match periphery.request(request.clone()).await {
Ok(res) => return Ok(res),
Err(e) => err = Some(e),
}
+10 -11
View File
@@ -6,7 +6,7 @@ use komodo_client::{
permission::PermissionLevel,
server::Server,
stack::Stack,
terminal::{ContainerTerminalMode, TerminalTarget},
terminal::{ContainerTerminalMode, Terminal, 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: terminal.clone(),
name: Some(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: terminal.clone(),
name: Some(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: terminal.clone(),
name: Some(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: terminal.clone(),
name: Some(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<()> {
) -> anyhow::Result<Terminal> {
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,9 +265,8 @@ 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(
@@ -333,7 +332,7 @@ pub async fn get_deployment_periphery_container(
let periphery = periphery_client(&server).await?;
let container = deployment.name.clone();
let container = deployment.deployed_name().to_string();
Ok((
TerminalTarget::Deployment {
+146 -457
View File
@@ -123,464 +123,153 @@ pub async fn init_execution_update(
request: &ExecuteRequest,
user: &User,
) -> anyhow::Result<Update> {
let (operation, target) = match &request {
// Swarm
ExecuteRequest::RemoveSwarmNodes(data) => (
Operation::RemoveSwarmNodes,
ResourceTarget::Swarm(
resource::get::<Swarm>(&data.swarm).await?.id,
),
),
ExecuteRequest::RemoveSwarmStacks(data) => (
Operation::RemoveSwarmStacks,
ResourceTarget::Swarm(
resource::get::<Swarm>(&data.swarm).await?.id,
),
),
ExecuteRequest::RemoveSwarmServices(data) => (
Operation::RemoveSwarmServices,
ResourceTarget::Swarm(
resource::get::<Swarm>(&data.swarm).await?.id,
),
),
ExecuteRequest::CreateSwarmConfig(data) => (
Operation::CreateSwarmConfig,
ResourceTarget::Swarm(
resource::get::<Swarm>(&data.swarm).await?.id,
),
),
ExecuteRequest::RotateSwarmConfig(data) => (
Operation::RotateSwarmConfig,
ResourceTarget::Swarm(
resource::get::<Swarm>(&data.swarm).await?.id,
),
),
ExecuteRequest::RemoveSwarmConfigs(data) => (
Operation::RemoveSwarmConfigs,
ResourceTarget::Swarm(
resource::get::<Swarm>(&data.swarm).await?.id,
),
),
ExecuteRequest::CreateSwarmSecret(data) => (
Operation::CreateSwarmSecret,
ResourceTarget::Swarm(
resource::get::<Swarm>(&data.swarm).await?.id,
),
),
ExecuteRequest::RotateSwarmSecret(data) => (
Operation::RotateSwarmSecret,
ResourceTarget::Swarm(
resource::get::<Swarm>(&data.swarm).await?.id,
),
),
ExecuteRequest::RemoveSwarmSecrets(data) => (
Operation::RemoveSwarmSecrets,
ResourceTarget::Swarm(
resource::get::<Swarm>(&data.swarm).await?.id,
),
),
macro_rules! init_execution_match {
(
resource: [$(($Variant:ident, $ResType:ident, $field:ident)),* $(,)?],
batch: [$($BatchVariant:ident),* $(,)?],
stack_service: [$(($StackVariant:ident, $ServiceOp:ident)),* $(,)?],
system: [$($SysVariant:ident),* $(,)?],
) => {
match &request {
$(
ExecuteRequest::$Variant(data) => (
Operation::$Variant,
ResourceTarget::$ResType(
resource::get::<$ResType>(&data.$field).await?.id,
),
),
)*
$(
ExecuteRequest::$BatchVariant(_data) => {
return Ok(Default::default());
}
)*
$(
ExecuteRequest::$StackVariant(data) => (
if !data.services.is_empty() {
Operation::$ServiceOp
} else {
Operation::$StackVariant
},
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
)*
// DeployStackIfChanged doesn't have a service variant
ExecuteRequest::DeployStackIfChanged(data) => (
Operation::DeployStack,
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
$(
ExecuteRequest::$SysVariant(_data) => {
(Operation::$SysVariant, ResourceTarget::system())
}
)*
}
};
}
// Server
ExecuteRequest::StartContainer(data) => (
Operation::StartContainer,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::RestartContainer(data) => (
Operation::RestartContainer,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::PauseContainer(data) => (
Operation::PauseContainer,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::UnpauseContainer(data) => (
Operation::UnpauseContainer,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::StopContainer(data) => (
Operation::StopContainer,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::DestroyContainer(data) => (
Operation::DestroyContainer,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::StartAllContainers(data) => (
Operation::StartAllContainers,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::RestartAllContainers(data) => (
Operation::RestartAllContainers,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::PauseAllContainers(data) => (
Operation::PauseAllContainers,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::UnpauseAllContainers(data) => (
Operation::UnpauseAllContainers,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::StopAllContainers(data) => (
Operation::StopAllContainers,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::PruneContainers(data) => (
Operation::PruneContainers,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::DeleteNetwork(data) => (
Operation::DeleteNetwork,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::PruneNetworks(data) => (
Operation::PruneNetworks,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::DeleteImage(data) => (
Operation::DeleteImage,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::PruneImages(data) => (
Operation::PruneImages,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::DeleteVolume(data) => (
Operation::DeleteVolume,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::PruneVolumes(data) => (
Operation::PruneVolumes,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::PruneDockerBuilders(data) => (
Operation::PruneDockerBuilders,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::PruneBuildx(data) => (
Operation::PruneBuildx,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
ExecuteRequest::PruneSystem(data) => (
Operation::PruneSystem,
ResourceTarget::Server(
resource::get::<Server>(&data.server).await?.id,
),
),
// Deployment
ExecuteRequest::Deploy(data) => (
Operation::Deploy,
ResourceTarget::Deployment(
resource::get::<Deployment>(&data.deployment).await?.id,
),
),
ExecuteRequest::BatchDeploy(_data) => {
return Ok(Default::default());
}
ExecuteRequest::PullDeployment(data) => (
Operation::PullDeployment,
ResourceTarget::Deployment(
resource::get::<Deployment>(&data.deployment).await?.id,
),
),
ExecuteRequest::StartDeployment(data) => (
Operation::StartDeployment,
ResourceTarget::Deployment(
resource::get::<Deployment>(&data.deployment).await?.id,
),
),
ExecuteRequest::RestartDeployment(data) => (
Operation::RestartDeployment,
ResourceTarget::Deployment(
resource::get::<Deployment>(&data.deployment).await?.id,
),
),
ExecuteRequest::PauseDeployment(data) => (
Operation::PauseDeployment,
ResourceTarget::Deployment(
resource::get::<Deployment>(&data.deployment).await?.id,
),
),
ExecuteRequest::UnpauseDeployment(data) => (
Operation::UnpauseDeployment,
ResourceTarget::Deployment(
resource::get::<Deployment>(&data.deployment).await?.id,
),
),
ExecuteRequest::StopDeployment(data) => (
Operation::StopDeployment,
ResourceTarget::Deployment(
resource::get::<Deployment>(&data.deployment).await?.id,
),
),
ExecuteRequest::DestroyDeployment(data) => (
Operation::DestroyDeployment,
ResourceTarget::Deployment(
resource::get::<Deployment>(&data.deployment).await?.id,
),
),
ExecuteRequest::BatchDestroyDeployment(_data) => {
return Ok(Default::default());
}
// Build
ExecuteRequest::RunBuild(data) => (
Operation::RunBuild,
ResourceTarget::Build(
resource::get::<Build>(&data.build).await?.id,
),
),
ExecuteRequest::BatchRunBuild(_data) => {
return Ok(Default::default());
}
ExecuteRequest::CancelBuild(data) => (
Operation::CancelBuild,
ResourceTarget::Build(
resource::get::<Build>(&data.build).await?.id,
),
),
// Repo
ExecuteRequest::CloneRepo(data) => (
Operation::CloneRepo,
ResourceTarget::Repo(
resource::get::<Repo>(&data.repo).await?.id,
),
),
ExecuteRequest::BatchCloneRepo(_data) => {
return Ok(Default::default());
}
ExecuteRequest::PullRepo(data) => (
Operation::PullRepo,
ResourceTarget::Repo(
resource::get::<Repo>(&data.repo).await?.id,
),
),
ExecuteRequest::BatchPullRepo(_data) => {
return Ok(Default::default());
}
ExecuteRequest::BuildRepo(data) => (
Operation::BuildRepo,
ResourceTarget::Repo(
resource::get::<Repo>(&data.repo).await?.id,
),
),
ExecuteRequest::BatchBuildRepo(_data) => {
return Ok(Default::default());
}
ExecuteRequest::CancelRepoBuild(data) => (
Operation::CancelRepoBuild,
ResourceTarget::Repo(
resource::get::<Repo>(&data.repo).await?.id,
),
),
// Procedure
ExecuteRequest::RunProcedure(data) => (
Operation::RunProcedure,
ResourceTarget::Procedure(
resource::get::<Procedure>(&data.procedure).await?.id,
),
),
ExecuteRequest::BatchRunProcedure(_) => {
return Ok(Default::default());
}
// Action
ExecuteRequest::RunAction(data) => (
Operation::RunAction,
ResourceTarget::Action(
resource::get::<Action>(&data.action).await?.id,
),
),
ExecuteRequest::BatchRunAction(_) => {
return Ok(Default::default());
}
// Resource Sync
ExecuteRequest::RunSync(data) => (
Operation::RunSync,
ResourceTarget::ResourceSync(
resource::get::<ResourceSync>(&data.sync).await?.id,
),
),
// Stack
ExecuteRequest::DeployStack(data) => (
if !data.services.is_empty() {
Operation::DeployStackService
} else {
Operation::DeployStack
},
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::BatchDeployStack(_data) => {
return Ok(Default::default());
}
ExecuteRequest::DeployStackIfChanged(data) => (
Operation::DeployStack,
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::BatchDeployStackIfChanged(_data) => {
return Ok(Default::default());
}
ExecuteRequest::StartStack(data) => (
if !data.services.is_empty() {
Operation::StartStackService
} else {
Operation::StartStack
},
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::PullStack(data) => (
if !data.services.is_empty() {
Operation::PullStackService
} else {
Operation::PullStack
},
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::BatchPullStack(_data) => {
return Ok(Default::default());
}
ExecuteRequest::RestartStack(data) => (
if !data.services.is_empty() {
Operation::RestartStackService
} else {
Operation::RestartStack
},
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::PauseStack(data) => (
if !data.services.is_empty() {
Operation::PauseStackService
} else {
Operation::PauseStack
},
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::UnpauseStack(data) => (
if !data.services.is_empty() {
Operation::UnpauseStackService
} else {
Operation::UnpauseStack
},
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::StopStack(data) => (
if !data.services.is_empty() {
Operation::StopStackService
} else {
Operation::StopStack
},
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::DestroyStack(data) => (
if !data.services.is_empty() {
Operation::DestroyStackService
} else {
Operation::DestroyStack
},
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::BatchDestroyStack(_data) => {
return Ok(Default::default());
}
ExecuteRequest::RunStackService(data) => (
Operation::RunStackService,
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
// Alerter
ExecuteRequest::TestAlerter(data) => (
Operation::TestAlerter,
ResourceTarget::Alerter(
resource::get::<Alerter>(&data.alerter).await?.id,
),
),
ExecuteRequest::SendAlert(_) => {
(Operation::SendAlert, ResourceTarget::system())
}
// Maintenance
ExecuteRequest::ClearRepoCache(_data) => {
(Operation::ClearRepoCache, ResourceTarget::system())
}
ExecuteRequest::BackupCoreDatabase(_data) => {
(Operation::BackupCoreDatabase, ResourceTarget::system())
}
ExecuteRequest::GlobalAutoUpdate(_data) => {
(Operation::GlobalAutoUpdate, ResourceTarget::system())
}
ExecuteRequest::RotateAllServerKeys(_data) => {
(Operation::RotateAllServerKeys, ResourceTarget::system())
}
ExecuteRequest::RotateCoreKeys(_data) => {
(Operation::RotateCoreKeys, ResourceTarget::system())
}
};
let (operation, target) = init_execution_match!(
resource: [
// Swarm
(RemoveSwarmNodes, Swarm, swarm),
(UpdateSwarmNode, Swarm, swarm),
(RemoveSwarmStacks, Swarm, swarm),
(RemoveSwarmServices, Swarm, swarm),
(CreateSwarmConfig, Swarm, swarm),
(RotateSwarmConfig, Swarm, swarm),
(RemoveSwarmConfigs, Swarm, swarm),
(CreateSwarmSecret, Swarm, swarm),
(RotateSwarmSecret, Swarm, swarm),
(RemoveSwarmSecrets, Swarm, swarm),
// Server
(StartContainer, Server, server),
(RestartContainer, Server, server),
(PauseContainer, Server, server),
(UnpauseContainer, Server, server),
(StopContainer, Server, server),
(DestroyContainer, Server, server),
(StartAllContainers, Server, server),
(RestartAllContainers, Server, server),
(PauseAllContainers, Server, server),
(UnpauseAllContainers, Server, server),
(StopAllContainers, Server, server),
(PruneContainers, Server, server),
(DeleteNetwork, Server, server),
(PruneNetworks, Server, server),
(DeleteImage, Server, server),
(PruneImages, Server, server),
(DeleteVolume, Server, server),
(PruneVolumes, Server, server),
(PruneDockerBuilders, Server, server),
(PruneBuildx, Server, server),
(PruneSystem, Server, server),
// Deployment
(Deploy, Deployment, deployment),
(PullDeployment, Deployment, deployment),
(StartDeployment, Deployment, deployment),
(RestartDeployment, Deployment, deployment),
(PauseDeployment, Deployment, deployment),
(UnpauseDeployment, Deployment, deployment),
(StopDeployment, Deployment, deployment),
(DestroyDeployment, Deployment, deployment),
// Build
(RunBuild, Build, build),
(CancelBuild, Build, build),
// Repo
(CloneRepo, Repo, repo),
(PullRepo, Repo, repo),
(BuildRepo, Repo, repo),
(CancelRepoBuild, Repo, repo),
// Procedure
(RunProcedure, Procedure, procedure),
(CancelProcedure, Procedure, procedure),
// Action
(RunAction, Action, action),
(CancelAction, Action, action),
// Resource Sync
(RunSync, ResourceSync, sync),
// Stack (simple)
(RunStackService, Stack, stack),
// Alerter
(TestAlerter, Alerter, alerter),
],
batch: [
BatchDeploy,
BatchDestroyDeployment,
BatchRunBuild,
BatchCloneRepo,
BatchPullRepo,
BatchBuildRepo,
BatchRunProcedure,
BatchRunAction,
BatchDeployStack,
BatchDeployStackIfChanged,
BatchPullStack,
BatchDestroyStack,
],
stack_service: [
(DeployStack, DeployStackService),
(PullStack, PullStackService),
(StartStack, StartStackService),
(RestartStack, RestartStackService),
(PauseStack, PauseStackService),
(UnpauseStack, UnpauseStackService),
(StopStack, StopStackService),
(DestroyStack, DestroyStackService),
],
system: [
SendAlert,
ClearRepoCache,
BackupCoreDatabase,
GlobalAutoUpdate,
RotateAllServerKeys,
RotateCoreKeys,
],
);
let mut update = make_update(target, operation, user);
update.in_progress();
+2
View File
@@ -19,6 +19,7 @@ mod monitor;
mod network;
mod periphery;
mod permission;
mod reporting;
mod resource;
mod schedule;
mod stack;
@@ -71,6 +72,7 @@ async fn app() -> anyhow::Result<()> {
resource::spawn_action_state_refresh_loop();
schedule::spawn_schedule_executor();
helpers::prune::spawn_prune_loop();
reporting::spawn_reporting_loop();
}
.instrument(startup_span)
.await;
+2 -1
View File
@@ -3,7 +3,8 @@ use std::collections::HashMap;
use komodo_client::entities::{
ResourceTarget,
alert::{Alert, AlertData, SeverityLevel},
deployment::{Deployment, DeploymentState}, optional_string,
deployment::{Deployment, DeploymentState},
optional_string,
};
use crate::{
+7 -1
View File
@@ -2,7 +2,9 @@ use std::{collections::HashMap, sync::Mutex};
use anyhow::Context;
use komodo_client::entities::{
alert::AlertDataVariant, permission::PermissionLevel, resource::ResourceQuery, server::Server, swarm::Swarm, user::system_user
alert::AlertDataVariant, permission::PermissionLevel,
resource::ResourceQuery, server::Server, swarm::Swarm,
user::system_user,
};
use crate::resource;
@@ -34,6 +36,8 @@ async fn get_all_swarms_map()
-> anyhow::Result<(HashMap<String, Swarm>, HashMap<String, String>)> {
let swarms = resource::list_full_for_user::<Swarm>(
ResourceQuery::default(),
None,
None,
system_user(),
PermissionLevel::Read.into(),
&[],
@@ -59,6 +63,8 @@ async fn get_all_servers_map()
{
let servers = resource::list_full_for_user::<Server>(
ResourceQuery::default(),
None,
None,
system_user(),
PermissionLevel::Read.into(),
&[],
+164 -64
View File
@@ -51,7 +51,7 @@ pub async fn alert_servers(
let mut alerts_to_open = Vec::<(Alert, SendAlerts)>::new();
let mut alerts_to_update = Vec::<(Alert, SendAlerts)>::new();
let mut alert_ids_to_close = Vec::<(Alert, SendAlerts)>::new();
let mut alerts_to_close = Vec::<(Alert, SendAlerts)>::new();
let buffer = alert_buffer();
@@ -66,15 +66,21 @@ pub async fn alert_servers(
let in_maintenance =
is_in_maintenance(&server.config.maintenance_windows, ts);
// ===================
// SERVER HEALTH
// ===================
// ===============
// SERVER HEALTH
// ===============
let health_alert = server_alerts.as_ref().and_then(|alerts| {
alerts.get(&AlertDataVariant::ServerUnreachable)
});
match (server_status.state, health_alert) {
(ServerState::NotOk, None) => {
// Only open unreachable alert if not in maintenance and buffer is ready
match (
server_status.state,
health_alert,
server.config.send_unreachable_alerts,
) {
// Skip alerting if alerts disabled
(ServerState::NotOk, None, false) => {}
(ServerState::NotOk, None, true) => {
// Only open unreachable alert if unreachable alerts enabled and not in maintenance and buffer is ready
if !in_maintenance
&& buffer.ready_to_open(
server_status.id.clone(),
@@ -99,19 +105,14 @@ pub async fn alert_servers(
.push((alert, server.config.send_unreachable_alerts))
}
}
(ServerState::NotOk, Some(alert)) => {
(ServerState::NotOk, Some(alert), true) => {
// update alert err
let mut alert = alert.clone();
let (id, name, region) = match alert.data {
AlertData::ServerUnreachable {
id, name, region, ..
} => (id, name, region),
data => {
error!(
"got incorrect alert data in ServerStatus handler. got {data:?}"
);
continue;
}
_ => unreachable!(),
};
alert.data = AlertData::ServerUnreachable {
id,
@@ -124,23 +125,30 @@ pub async fn alert_servers(
alerts_to_update.push((alert, false));
}
// Close an open alert
(ServerState::Ok | ServerState::Disabled, Some(alert)) => {
alert_ids_to_close.push((
(ServerState::NotOk, Some(alert), false) => {
alerts_to_close.push((
alert.clone(),
server.config.send_unreachable_alerts,
));
}
(ServerState::Ok | ServerState::Disabled, None) => buffer
// Close an open alert
(ServerState::Ok | ServerState::Disabled, Some(alert), _) => {
alerts_to_close.push((
alert.clone(),
server.config.send_unreachable_alerts,
));
}
(ServerState::Ok | ServerState::Disabled, None, _) => buffer
.reset(
server_status.id.clone(),
AlertDataVariant::ServerUnreachable,
),
}
// ===================
// ========================
// SERVER VERSION MISMATCH
// ===================
// ========================
let core_version = env!("CARGO_PKG_VERSION");
let mismatched_server_version =
if server_status.state != ServerState::Ok {
@@ -158,9 +166,14 @@ pub async fn alert_servers(
alerts.get(&AlertDataVariant::ServerVersionMismatch)
});
match (mismatched_server_version, version_alert) {
(Some(version), None) => {
// Only open version mismatch alert if not in maintenance and buffer is ready
match (
mismatched_server_version,
version_alert,
server.config.send_version_mismatch_alerts,
) {
(Some(_), None, false) => {}
(Some(version), None, true) => {
// Only open version mismatch alert if version mismatch alerts enabled and not in maintenance and buffer is ready
if !in_maintenance
&& buffer.ready_to_open(
server_status.id.clone(),
@@ -187,27 +200,47 @@ pub async fn alert_servers(
.push((alert, server.config.send_version_mismatch_alerts))
}
}
(Some(version), Some(alert)) => {
// Update existing alert with current version info
let mut alert = alert.clone();
alert.data = AlertData::ServerVersionMismatch {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.config.region),
server_version: version.clone(),
core_version: core_version.to_string(),
};
// Don't send notification for updates
alerts_to_update.push((alert, false));
(Some(version), Some(alert), true) => {
// Update existing alert with current version info if changed
let (alert_server_version, alert_core_version) =
match &alert.data {
AlertData::ServerVersionMismatch {
server_version,
core_version,
..
} => (server_version, core_version),
_ => unreachable!(),
};
if alert_server_version != version
|| alert_core_version != core_version
{
let mut alert = alert.clone();
alert.data = AlertData::ServerVersionMismatch {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.config.region),
server_version: version.clone(),
core_version: core_version.to_string(),
};
// Don't send notification for updates
alerts_to_update.push((alert, false));
}
}
(None, Some(alert)) => {
// Version is now correct, close the alert
alert_ids_to_close.push((
(Some(_), Some(alert), false) => {
alerts_to_close.push((
alert.clone(),
server.config.send_version_mismatch_alerts,
));
}
(None, None) => {
(None, Some(alert), _) => {
// Version is now correct, close the alert
alerts_to_close.push((
alert.clone(),
server.config.send_version_mismatch_alerts,
));
}
(None, None, _) => {
// Reset buffer state when no mismatch and no alert
buffer.reset(
server_status.id.clone(),
@@ -227,10 +260,25 @@ pub async fn alert_servers(
.as_ref()
.and_then(|alerts| alerts.get(&AlertDataVariant::ServerCpu))
.cloned();
match (health.cpu.level, cpu_alert, health.cpu.should_close_alert)
{
(SeverityLevel::Warning | SeverityLevel::Critical, None, _) => {
// Only open CPU alert if not in maintenance and buffer is ready
match (
health.cpu.level,
cpu_alert,
health.cpu.should_close_alert,
server.config.send_cpu_alerts,
) {
(
SeverityLevel::Warning | SeverityLevel::Critical,
None,
_,
false,
) => {}
(
SeverityLevel::Warning | SeverityLevel::Critical,
None,
_,
true,
) => {
// Only open CPU alert if cpu alerts enabled and not in maintenance and buffer is ready
if !in_maintenance
&& buffer.ready_to_open(
server_status.id.clone(),
@@ -262,6 +310,7 @@ pub async fn alert_servers(
SeverityLevel::Warning | SeverityLevel::Critical,
Some(mut alert),
_,
true,
) => {
// modify alert level only if it has increased and not in maintenance
if !in_maintenance && alert.level < health.cpu.level {
@@ -280,8 +329,15 @@ pub async fn alert_servers(
.push((alert, server.config.send_cpu_alerts));
}
}
(SeverityLevel::Ok, Some(alert), true) => {
let mut alert = alert.clone();
(
SeverityLevel::Warning | SeverityLevel::Critical,
Some(alert),
_,
false,
) => {
alerts_to_close.push((alert, server.config.send_cpu_alerts));
}
(SeverityLevel::Ok, Some(mut alert), true, _) => {
alert.data = AlertData::ServerCpu {
id: server_status.id.clone(),
name: server.name.clone(),
@@ -292,10 +348,9 @@ pub async fn alert_servers(
.map(|s| s.cpu_perc as f64)
.unwrap_or(0.0),
};
alert_ids_to_close
.push((alert, server.config.send_cpu_alerts))
alerts_to_close.push((alert, server.config.send_cpu_alerts))
}
(SeverityLevel::Ok, _, _) => buffer
(SeverityLevel::Ok, _, _, _) => buffer
.reset(server_status.id.clone(), AlertDataVariant::ServerCpu),
}
@@ -306,10 +361,25 @@ pub async fn alert_servers(
.as_ref()
.and_then(|alerts| alerts.get(&AlertDataVariant::ServerMem))
.cloned();
match (health.mem.level, mem_alert, health.mem.should_close_alert)
{
(SeverityLevel::Warning | SeverityLevel::Critical, None, _) => {
// Only open memory alert if not in maintenance and buffer is ready
match (
health.mem.level,
mem_alert,
health.mem.should_close_alert,
server.config.send_mem_alerts,
) {
(
SeverityLevel::Warning | SeverityLevel::Critical,
None,
_,
false,
) => {}
(
SeverityLevel::Warning | SeverityLevel::Critical,
None,
_,
true,
) => {
// Only open memory alert if memory alerts enabled and not in maintenance and buffer is ready
if !in_maintenance
&& buffer.ready_to_open(
server_status.id.clone(),
@@ -346,6 +416,7 @@ pub async fn alert_servers(
SeverityLevel::Warning | SeverityLevel::Critical,
Some(mut alert),
_,
true,
) => {
// modify alert level only if it has increased and not in maintenance
if !in_maintenance && alert.level < health.mem.level {
@@ -369,7 +440,16 @@ pub async fn alert_servers(
.push((alert, server.config.send_mem_alerts));
}
}
(SeverityLevel::Ok, Some(alert), true) => {
(
SeverityLevel::Warning | SeverityLevel::Critical,
Some(alert),
_,
false,
) => {
alerts_to_close
.push((alert.clone(), server.config.send_mem_alerts));
}
(SeverityLevel::Ok, Some(alert), true, _) => {
let mut alert = alert.clone();
alert.data = AlertData::ServerMem {
id: server_status.id.clone(),
@@ -386,10 +466,9 @@ pub async fn alert_servers(
.map(|s| s.mem_used_gb)
.unwrap_or(0.0),
};
alert_ids_to_close
.push((alert, server.config.send_mem_alerts))
alerts_to_close.push((alert, server.config.send_mem_alerts))
}
(SeverityLevel::Ok, _, _) => buffer
(SeverityLevel::Ok, _, _, _) => buffer
.reset(server_status.id.clone(), AlertDataVariant::ServerMem),
}
@@ -405,13 +484,25 @@ pub async fn alert_servers(
.as_ref()
.and_then(|alerts| alerts.get(path))
.cloned();
match (health.level, disk_alert, health.should_close_alert) {
match (
health.level,
disk_alert,
health.should_close_alert,
server.config.send_disk_alerts,
) {
(
SeverityLevel::Warning | SeverityLevel::Critical,
None,
_,
false,
) => {}
(
SeverityLevel::Warning | SeverityLevel::Critical,
None,
_,
true,
) => {
// Only open disk alert if not in maintenance and buffer is ready
// Only open disk alert if disk alerts enabled and not in maintenance and buffer is ready
if !in_maintenance
&& buffer.ready_to_open(
server_status.id.clone(),
@@ -450,6 +541,7 @@ pub async fn alert_servers(
SeverityLevel::Warning | SeverityLevel::Critical,
Some(mut alert),
_,
true,
) => {
// modify alert level only if it has increased and not in maintenance
if !in_maintenance && health.level < alert.level {
@@ -470,8 +562,16 @@ pub async fn alert_servers(
.push((alert, server.config.send_disk_alerts));
}
}
(SeverityLevel::Ok, Some(alert), true) => {
let mut alert = alert.clone();
(
SeverityLevel::Warning | SeverityLevel::Critical,
Some(alert),
_,
false,
) => {
alerts_to_close
.push((alert, server.config.send_disk_alerts));
}
(SeverityLevel::Ok, Some(mut alert), true, _) => {
let disk =
server_status.system_stats.as_ref().and_then(|stats| {
stats.disks.iter().find(|disk| disk.mount == *path)
@@ -485,10 +585,10 @@ pub async fn alert_servers(
total_gb: disk.map(|d| d.total_gb).unwrap_or_default(),
used_gb: disk.map(|d| d.used_gb).unwrap_or_default(),
};
alert_ids_to_close
alerts_to_close
.push((alert, server.config.send_disk_alerts))
}
(SeverityLevel::Ok, _, _) => buffer.reset(
(SeverityLevel::Ok, _, _, _) => buffer.reset(
server_status.id.clone(),
AlertDataVariant::ServerDisk,
),
@@ -501,7 +601,7 @@ pub async fn alert_servers(
if !health.disks.contains_key(path) {
let mut alert = alert.clone();
alert.level = SeverityLevel::Ok;
alert_ids_to_close
alerts_to_close
.push((alert, server.config.send_disk_alerts));
}
}
@@ -511,7 +611,7 @@ pub async fn alert_servers(
tokio::join!(
open_new_alerts(&alerts_to_open),
update_alerts(&alerts_to_update),
resolve_alerts(&alert_ids_to_close),
resolve_alerts(&alerts_to_close),
);
}

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