Compare commits

..

52 Commits

Author SHA1 Message Date
Maxwell Becker
23f8ecc1d9 1.18.2 (#591)
* feat: add maintenance window management to suppress alerts during planned activities (#550)

* feat: add scheduled maintenance windows to server configuration

- Add maintenance window configuration to server entities
- Implement maintenance window UI components with data table layout
- Add maintenance tab to server interface
- Suppress alerts during maintenance windows

* chore: enhance maintenance windows with types and permission improvements

- Add chrono dependency to Rust client core for time handling
- Add comprehensive TypeScript types for maintenance windows (MaintenanceWindow, MaintenanceScheduleType, MaintenanceTime, DayOfWeek)
- Improve maintenance config component to use usePermissions hook for better permission handling
- Update package dependencies

* feat: restore alert buffer system to prevent noise

* fix yarn fe

* fix the merge with new alerting changes

* move alert buffer handle out of loop

* nit

* fix server version changes

* unneeded buffer clear

---------

Co-authored-by: mbecker20 <becker.maxh@gmail.com>

* set version 1.18.2

* failed OIDC provider init doesn't cause panic, just  error log

* OIDC: use userinfo endpoint to get preffered username for user.

* add profile to scopes and account for username already taken

* search through server docker lists

* move maintenance stuff

* refactor maintenance schedules to have more toml compatible structure

* daily schedule type use struct

* add timezone to core info response

* frontend can build with new maintenance types

* Action monaco expose KomodoClient to init another client

* flatten out the nested enum

* update maintenance schedule types

* dev-3

* implement maintenance windows on alerters

* dev-4

* add IanaTimezone enum

* typeshare timezone enum

* maintenance modes almost done on servers AND alerters

* maintenance schedules working

* remove mention of migrator

* Procedure / Action schedule timezone selector

* improve timezone selector to display configure core TZ

* dev-5

* refetch core version

* add version to server list item info

* add periphery version in server table

* dev-6

* capitalize Unknown server status in cache

* handle unknown version case

* set server table sizes

* default resource_poll_interval 1-hr

* ensure parent folder exists before cloning

* document Build Attach permission

* git actions return absolute path

* stack linked repos

* resource toml replace linked_repo id with name

* validate incoming linked repo

* add linked repo to stack list item info

* stack list item info resolved linked repo information

* configure linked repo stack

* to repo links

* dev-7

* sync: replace linked repo with name for execute compare

* obscure provider tokens in table view

* clean up stack write w/ refactor

* Resource Sync / Build start support Repo attach

* add stack clone path config

* Builds + syncs can link to repos

* dev-9

* update ts

* fix linked repo not included in resource sync list item info

* add linked repo UI for builds / syncs

* fix commit linked repo sync

* include linked repo syncs

* correct Sync / Build config mode

* dev-12 fix resource sync inclusion w/ linked_repo

* remove unneed sync commit todo!()

* fix other config.repo.is_empty issues

* replace ids in all to toml exports

* Ensure git pull before commit for linear history, add to update logs

* fix fe for linked repo cases

* consolidate linked repo config component

* fix resource sync commit behavior

* dev 17

* Build uses Pull or Clone api to setup build source

* capitalize Clone Repo stage

* mount PullOrCloneRepo

* dev-19

* Expand supported container names and also avoid unnecessary name formatting

* dev-20

* add periphery /terminal/execute/container api

* periphery client execute_container_exec method

* implement execute container, deployment, stack exec

* gen types

* execute container exec method

* clean up client / fix fe

* enumerate exec ts methods for each resource type

* fix and gen ts client

* fix FE use connect_exec

* add url log when terminal ws fail to connect

* ts client server allow terminal.js

* FE preload terminal.js / .d.ts

* dev-23 fix stack terminal fail to connect when not explicitly setting container name

* update docs on attach perms

* 1.18.2

---------

Co-authored-by: Samuel Cardoso <R3D2@users.noreply.github.com>
2025-06-15 16:42:36 -07:00
Maxwell Becker
4d401d7f20 1.18.1 (#566)
* 1.18.1

* improve stack header / all resource links

* disable build config selector

* clean up deployment header

* update build header

* builder header

* update repo header

* start adding repo links from api

* implement list item repo link

* clean up fe

* gen client

* repo links across the board

* include state tracking buffer, so alerts are only triggered by consecutive out of bounds conditions

* add runnables-cli link in runfile

* improve frontend first load time through some code splitting

* add services count to stack header

* fix repo on pull

* Add dedicated Deploying state to Deployments and Stacks

* move predeploy script before compose config (#584)

* Periphery / core version mismatch check / red text

* move builders / alerts out of sidebar, into settings

* remove force push

* list schedules api

* dev-1

* actually dev-3

* fix action

* filter none procedures

* fix schedule api

* dev-5

* basic schedules page

* prog on schedule page

* simplify schedule

* use name to sort target

* add resource tags to schedule

* Schedule page working

* dev-6

* remove schedule table type column

* reorder schedule table

* force confirm  dialogs for delete, even if disabled in config

* 1.18.1

---------

Co-authored-by: undaunt <31376520+undaunt@users.noreply.github.com>
2025-06-06 23:08:51 -07:00
mbecker20
4165e25332 further clarify ferretdb setup for existing users 2025-06-01 13:50:03 -04:00
Maxwell Becker
4cc0817b0f Update copy-database.md 2025-05-30 15:08:19 -07:00
mbecker20
51cf1e2b05 clarify mongo / ferret in docs 2025-05-30 17:14:42 -04:00
mbecker20
5309c70929 update runfile 2025-05-30 17:01:15 -04:00
mbecker20
1278c62859 update specific permission in docs 2025-05-30 16:58:28 -04:00
mbecker20
6d6acdbc0b fix permissions list 2025-05-30 16:49:27 -04:00
mbecker20
d22000331e remove logging driver from compose example 2025-05-30 16:14:21 -04:00
Maxwell Becker
31034e5b34 1.18.0 (#555)
* ferretdb v2 now that they support arm64

* remove ignored for sqlite

* tweak

* mongo copier

* 1.17.6

* primary name is ferretdb option

* give doc counts

* fmt

* print document count

* komodo util versioned seperately

* add copy startup sleep

* FerretDB v2 upgrade guide

* tweak docs

* tweak

* tweak

* add link to upgrade guide for ferretdb v1 users

* fix copy batch size

* multi arch util setup

* util use workspace version

* clarify behavior re root_directory

* finished copying database log

* update to rust:1.87.0

* fix: reset rename editor on navigate

* loosen naming restrictions for most resource types

* added support for ntfy email forwarding (#493)

* fix alerter email option docs

* remove logging directive in example compose - can be done at user discretion

* more granular permissions

* fix initial fe type errors

* fix the new perm typing

* add dedicated ws routes to connect to deployment / stack terminal, using the permissioning on those entities

* frontend should convey / respect the perms

* use IndexSet for SpecificPermission

* finish IndexSet

* match regex or wildcard resource  name pattern

* gen ts client

* implement new terminal components which use the container / deployment / stack specific permissioned endpoints

* user group backend "everyone" support

* bump to 1.18.0 for significant permissioning changes

* ts 1.18.0

* permissions FE in prog

* FE permissions assignment working

* user group all map uses ordered IndexMap for consistency

* improve user group toml and fix execute bug

* URL encode names in webhook urls

* UI support configure 'everyone' User Group

* sync handle toggling user group everyone

* user group table show everyone enabled

* sync will update user group "everyone"

* Inspect Deployment / Stack containers directly

* fix InspectStackContainer container name

* Deployment / stack service inspect

* Stack / Deployment inherit Logs, Inspect and Terminal from their attached server for user

* fix compose down not capitalized

* don't use tabs

* more descriptive permission table titles

* different localstorage for permissions show all

* network / image / volume inspect don't require inspect perms

* fix container inspect

* fix list container undefined error

* prcesses list gated UI

* remove localstorage on permission table expansion

* fix ug sync handling of all zero permissions

* pretty log startup config

* implement actually pretty logging initial config

* fix user permissions when api returns string

* fix container info table

* util based on bullseye-slim

* permission toml specific skip_serializing_if = "IndexSet::is_empty"

* container tab permissions reversed

* reorder pretty logging stuff to be together

* update docs with permissioning info

* tweak docs

* update roadmap

---------

Co-authored-by: FelixBreitweiser <felix.breitweiser@uni-siegen.de>
2025-05-30 12:52:58 -07:00
Avalancs
a43e1f3f52 Add Keycloak instructions to OIDC setup (#517) 2025-05-18 15:49:11 -07:00
jeroenvds
7a3b2b542d Removing ServerTemplate in docs (#492)
Removing ServerTemplate from Resources documentation, as it was removed in Release v1.17.5
2025-05-08 02:43:45 -04:00
Cesar Villegas
8d516d6d5f fix: api_key -> key in Typescript client initialization (#485) 2025-05-06 11:22:02 -07:00
Maxwell Becker
3e0d1befbd 1.17.5 (#472)
* API support new calling syntax

* finish /{variant} api to improve network logs in browser console

* update roadmap

* configure the shell used to start the pty

* start on ExecuteTerminal api

* Rename resources less hidden - click on name in header

* update deps

* execute terminal

* BatchPullStack

* add Types import to Actions, and don't stringify the error

* add --reload for cached deps

* type execute terminal response as AsyncIterable

* execute terminal client api

* KOMODO_EXIT_CODE

* Early exit without code

* action configurable deno dep reload

* remove ServerTemplate resource

* kept disabled

* rework exec terminal command wrapper

* debug: print lines in start sentinel loop

* edit debug / remove ref

* echo

* line compare

* log lengths

* use printf again

* check char compare

* leading \n

* works with leading \n

* extra \n after START_OF_OUTPUT

* add variables / secrets finders to ui defined stacks / builds

* isolate post-db startup procedures

* clean up server templates

* disable websocket reconnect from core config

* change periphery ssl enabled to default to true

* git provider selector config pass through disable to http/s button

* disable terminals while allowing container exec

* disable_container_exec in default config

* update ws reconnect implementation

* Don't show delete tag non admin and non owner

* 1.17.5 complete
2025-05-04 14:45:31 -07:00
mbecker20
5dc609b206 add examples for perihery config fields 2025-04-28 18:14:40 -04:00
mbecker20
f1127007c3 update intro with shell features 2025-04-27 19:21:55 -04:00
Maxwell Becker
765e5a0df1 1.17.4 (#446)
* add terminal (ssh) apis

* add core terminal exec method

* terminal typescript client method

* terminals WIP

* backend for pty

* add ts responses

* about wire everything

* add new blog

* credit Skyfay

* working

* regen lock

* 1.17.4-dev-1

* pty history

* replace the test terminal impl with websocket (pty)

* create api and improve frontend

* fix fe

* terminals

* disable terminal api on periphery

* implement write level terminal perms

* remove unneeded

* fix clippy

* delete unneeded

* fix waste cpu cycles

* set TERM and COLORTERM for shell environment

* fix xterm scrolling behavior

* starship promp in periphery container terminal

* kill all terminals on periphery shutdown signal

* improve starship config and enable ssl in compose

* use same scrollTop setter

* fix periphery container distribution link

* support custom command / args to init terminal

* allow fully configurable init command

* docker exec into container

* add permissioning for container exec

* add starship to core container

* add delete all terminals

* dev-2

* finished gen client

* core need curl

* hide Terminal trigger if disabled

* 1.17.4
2025-04-27 15:53:23 -07:00
mbecker20
76f2f61be5 1.17.3 fix Build pre_build functionality. 2025-04-24 22:03:46 -04:00
Maxwell Becker
b43e2918da 1.17.2 (#409)
* start on cron schedules

* rust 1.86.0

* config periphery directories easier with PERIPHERY_ROOT_DIRECTORY

* schedule backend

* fix config switch toggling through disabled

* procedure schedule working

* implement schedules for actions

* update schedule immediately after last run

* improve config update logs using toml diffs backend

* improve the config update logs with TOML diff view

* add schedule alerting

* version 1.17.2

* Set TZ in core env

* dev-1

* better term signal labels

* sync configurable pending alert send

* fix monaco editor height on larger screen

* poll update until complete on client

update lib

* add logger.pretty option for both core and periphery

* fix pretty

* configure schedule alert

* configure failure alert

* dev-3

* 1.17.2

* fmt

* added pushover alerter (#421)

* fix up pushover

* fix some clippy

---------

Co-authored-by: Alex Shore <alex@shore.me.uk>
2025-04-18 23:14:10 -07:00
Etienne
f45205011e Fix compose.env (#415)
Flip "stacks" and "repos" paths
2025-04-15 12:11:48 -07:00
Maxwell Becker
5b211fb8f0 1.17.1 (#383)
* interpolate into slack / discord url

* fix js client docs

* js client should be type: module

* click table tags to toggle tag filter

* git token helper early return when empty provider

* reorder Stack fields

* action support interpolation doc

* Fix for the {account} login fails when the account name contains '$' (#385)

* GetDeploymentsSummary (#386)

* added GetDockerContainersSummary endpoint in rust api

* typescript stuff

* more autogenned typescript stuff

* fixed comments to be in line with actual behaviour

* fixed ReadResponse for GetDockerContainersSummary

* I64 -> u32 for response types

* more accurate error context

* backend for build files on host / ui defined

* core api supports non repo based build

* Ntfy as Alerter (#404)

* add ntfy alerter

* add ntfy alerter

* add ntfy alerter

---------

Co-authored-by: GFXSpeed <github@schafhaupt.com>

* Improve docs around running periphery in a container (#402)

* Add ports that need to be exposed if periphery is remote from core

* Spelling: overide -> override

* Add info about using a custom config file when running periphery in docker

* clean up ntfy alerter

* clean up ResourceSyncConfig

* update build cache after create / update

* refresh stack cache log

* Build UI Defined / file on host frontend

* update clap + rustls

* don't cleanup build files

* clean up dockerfile full path

* update BuildListItemInfo + UI table

* add Other Resources page

* add 5 second ws reconnection timeout

* Make listener address configurable & Add support for IPv6 (#405)

* Make listener address configurable

* Make listener address configurable for periphery

* rename listener_address -> bind_ip

---------

Co-authored-by: Jacky Fong <hello@huzky.dev>
Co-authored-by: Alex Shore <kntrllr@gmail.com>
Co-authored-by: Niklas <108990063+GFXSpeed@users.noreply.github.com>
Co-authored-by: GFXSpeed <github@schafhaupt.com>
Co-authored-by: theRAAPster <theraapster@gmail.com>
Co-authored-by: Daniel Vos <45820840+vosdev@users.noreply.github.com>
2025-04-14 14:48:12 -07:00
Justin Goette
3f04614303 docs: Typo (#392)
FerretDB vs. FerritDB
2025-04-02 00:05:29 -07:00
mbecker20
4c3210f70a update dashboard screenshots with colored tags 2025-03-23 23:17:33 -07:00
mbecker20
2c37ac26a5 ts client 1.17.0 2025-03-23 17:42:47 -07:00
Maxwell Becker
db1cf786ac 1.17.0 (#248)
* resolver v3

add new ec2 instance types

clean up testing config

document the libraries a bit

clean up main

update sysinfo and otel

update client resolver 3.0

resolver v3 prog

clean up gitignore

implement periphery resolver v3

clean up

core read api v3

more prog

execute api

missing apis

compiling

1.16.13

work on more granular traits

prog on crud

* fmt

* format

* resource2 not really a benefit

* axum to 0.8

* bump aws deps

* just make it 1.17.0

* clean up cors

* the komodo env file should be highest priority over additional files

* add entities / message for test alerter

* test alert implementation

* rust 1.84.0

* axum update :param to {param} syntax

* fix last axum updates

* Add test alerter button

* higher quality / colored icons

* komodo-logo

* simplify network stats

* rename Test Alerter button

* escape incoming sync backslashes (BREAKING)

* clean up rust client websocket subscription

* finish oidc comment

* show update available stack table

* update available deployment table

* feature: use the repo path instead of name in GetLatestCommit (#282)

* Update repo path handling in commit fetching

- Changed `name` to `path` for repository identification.
- Updated cache update function to use the new path field.
- Improved error message for non-directory repo paths.

* feat: use optional name and path in GetLatestCommit

* review: don't use optional for name

* review: use helper

* review: remove redundant to_string()

* 1.17.0-dev

* feature: add post_deploy command (#288)

* feature: add post_deploy command

* review: do not run post_deploy if deploy failed

* feature: interpolate secrets in custom alerter (#289)

* feature: interpolate secrets in custom alerter

* fix rust warning

* review: sanitize errors

* review: sanitize error message

* Remove .git from remote_url (#299)

Remove .git from remote_url

Co-authored-by: Deon Marshall <dmarshall@ccp.com.au>

* mbecker20 -> moghtech

* remove example from cargo toml workspace

* dev-1

* fix login screen logo

* more legible favicon

* fix new compose images

* docs new organization

* typescript subscribe_to_update_websocket

* add donate button docsite

* add config save button in desktop sidebar navigator

* add save button to config bottom

* feature: allow docker image text to overflow in table (#301)

* feature: allow docker image text to overflow in table

* review: use break-words

* wip: revert line break in css file

* feature: update devcontainer node release

* improve First Login docs

* FIx PullStack re #302 and record docker compose config on stack deploy

* requery alerts more often

* improve update indicator style and also put on home screen

* Add all services stack log

* 1.17.0-dev-2

* fix api name chnage

* choose which stack services to include in logs

* feature: improve tables quick actions on mobile (#312)

* feature: improve tables quick actions on mobile

* review: fix gap4

* review: use flex-wrap

* improve pull to git init on existing folder without .git

* Fix unclear ComposePull log re #244

* use komodo_client.subscribe_to_update_websocket, and click indicator to reconnect

* dev-3

* ServerTemplate description

* improve WriteComposeContentsToHost instrument fields

* give server stat charts labels

* filters wrap

* show provider usernames from config file

* Stack: Fix git repo new compose file initialization

* init sync file new repo

* set branch on git init folder

* ResourceSync: pending view toggle between "Execute" vs "Commit" sync direction

* Improve resource sync Execute / Pending view selector

* standardize running commands with interpolation / output sanitizations

* fix all clippy lints

* fix rand

* lock certain users username / password, prevent demo creds from being changed.

* revert to login screen whenever the call to check login fails

* ResourceSync state resolution refinement

* make sure parent directories exist whenever writing files

* don't prune images if server not enabled

* update most deps

* update openidconnect dependency, and use reqwest rustls-tls-native-roots

* dev-4

* resource sync only add escaping on toml between the """

* Stacks executions take list of services -- Auto update only redeploys services with update

* auto update all service deploy option

* dev-5 fix the stack service executions

* clean up service_args

* rust 1.85

* store sync edits on localstorage

* stack edits on localstorage and show last deployed config

* add yarn install to runfile

* Fix actions when core on https

* add update_available query parameter to filter for only stacks /deployments with available update

* rust 2024 and fmt

* rename test.compose.yaml to dev.compose.yaml, and update runfile

* update .devcontainer / dev docs for updated runfile

* use png in topbar logo, svg quality sometimes bad

* OIDC: Support PKCE auth (secret optional)

* update docs on OIDC and client secret

* cycle the oidc client on interval to ensure up to date JWKs

* add KOMODO_LOCK_LOGIN_CREDENTIALS_FOR in config doc

* update deps

* resource sync toggle resource / variable / user group inclusion independantly

* use jsonwebtoken

* improve variable value table overflow

* colored tags

* fix sync summary count ok

* default new tag colors to grey

* soften tag opacity a bit

* Update config.tsx (#358)

* isolate stacks / deployments with pending updates

* update some deps

* use Tooltip component instead of HoverCard for mobile compatibility

* batch Build builds

* link to typescript client in the intro

* add link to main docs from client docs

* doc tweaks

* use moghtech/komodo-core and moghtech/komodo-periphery as images

* remove unnecessary explicit network

* periphery.compose.yaml

* clean up periphery compose

* add link to config

* update periphery container compose config

* rust 1.85.1

* update sync docs

* 1.17.0

---------

Co-authored-by: unsync <1211591+unsync@users.noreply.github.com>
Co-authored-by: Deon Marshall <dmarshall@ccp.com.au>
Co-authored-by: komodo <komodo@komo.do>
Co-authored-by: wlatic <jamesoh@gmail.com>
2025-03-23 16:47:06 -07:00
Maarten Kossen
9c841e5bdc Change amd64 to arm64 to prevent installing aarch64 binary on an x86_64 system. (#357) 2025-03-12 19:20:49 -07:00
mbecker20
e385c6e722 use ferretdb:1 2025-02-26 14:55:34 -08:00
Maxwell Becker
9ef25e7575 Create FUNDING.yml 2025-02-13 12:07:48 -08:00
boomam
f945a3014a Update index.mdx (#306)
Added small note on initial login steps.
2025-02-11 00:03:39 -08:00
mbecker20
fdad04d6cb fix KOMODO_DB_USERNAME compose files 2025-02-08 18:45:02 -08:00
mbecker20
c914f23aa8 update compose files re #180 2025-02-08 12:29:26 -08:00
Maarten Kossen
82b2e68cd3 Adding Resource Sync documentation. (#259) 2025-01-11 21:32:02 -08:00
rita7lopes
e274d6f7c8 Network Usage - Ingress Egress per interface and global usage (#229)
* Add network io stats

Add network usage graph and current status

Change network graphs to use network interface from drop down menu

Adjust the type to be able to get general network stats for the general
UI view

Working setup with a working builder

remove changes to these dockerfile

remove lock changes

* change network hashmap to Vector

* adjust all the network_usage_interface to be a vector rather than a hash map

* PR requested changes applied

* Change net_ingress_bytes and egress to network_ingress_bytes egress respectively

* final gen-client types

---------

Co-authored-by: mbecker20 <becker.maxh@gmail.com>
2024-12-21 08:15:21 -08:00
Maxwell Becker
ab8777460d Simplify periphery aio.Dockerfile 2024-12-14 09:13:01 -08:00
Maxwell Becker
7e030e702f Simplify aio.Dockerfile 2024-12-14 09:12:32 -08:00
Maxwell Becker
a869a74002 Fix test.compose.yaml (use aio.Dockerfiles) 2024-12-14 09:11:24 -08:00
mbecker20
1d31110f8c fix multi arch built var reference 2024-12-02 03:33:11 -05:00
mbecker20
bb63892e10 periphery -> periphery-x86_64 setup script 2024-12-02 03:31:55 -05:00
mbecker20
4e554eb2a7 add labels to binary / frontend images 2024-12-02 03:02:00 -05:00
Maxwell Becker
00968b6ea1 1.16.12 (#209)
* inc version

* Komodo interp in ui compose file

* fix auto update when image doesn't specify tag by defaulting to latest

* Pull image buttons don't need safety dialog

* WIP crosscompile

* rename

* entrypoint

* fix copy

* remove example/* from workspace

* add targets

* multiarch pkg config

* use specific COPY

* update deps

* multiarch build command

* pre compile deps

* cross compile

* enable-linger

* remove spammed log when server doesn't have docker

* add multiarch.Dockerfile

* fix casing

* fix tag

* try not let COPY fail

* try

* ARG TARGETPLATFORM

* use /app for consistency

* try

* delete cross-compile approach

* add multiarch core build

* multiarch Deno

* single arch multi arch

* typeshare cli note

* new typeshare

* remove note about aarch64 image

* test configs

* fix config file headers

* binaries dockerfile

* update cargo build

* docs

* simple

* just simple

* use -p

* add configurable binaries tag

* add multi-arch

* allow copy to fail

* fix binary paths

* frontend Dockerfiel

* use dedicated static frontend build

* auto retry getting instance state from aws

* retry 5 times

* cleanup

* simplify binary build

* try alpine and musl

* install alpine deps

* back to debian, try rustls

* move fully to rustls

* single arch builds using single binary image

* default IMAGE_TAG

* cleanup

* try caching deps

* single arch add frontend build

* rustls::crypto::ring::default_provider()

* back to simple

* comment dockerfile

* add select options prop, render checkboxes if present

* add allowSelectedIf to enable / disable rows where necessary

* rename allowSelectIf to isSelectable, allow false as global disable, disable checkboxes when not allowed

* rename isSelectable to disableRow (it works the oppsite way lol)

* selected resources hook, start deployment batch execute component

* add deployment group actions

* add deployment group actions

* add default (empty) group actions for other resources

* fix checkbox header styles

* explicitly check if disableRow is passed (this prop is cursed)

* don't disable row selection for deployments table

* don't need id for groupactions

* add group actions to resources page

* fix row checkbox (prop not cursed, i dumb)

* re-implement group action list using dropdown menu

* only make group actions clickable when at least one row selected

* add loading indicator

* gap betwen new resource and group actions

* refactor group actions

* remove "Batch" from action labels

* add group actions for relevant resources

* fix hardcode

* add selectOptions to relevant tables

* select by name not id

* expect selected to be names

* add note re selection state init for future reference

* multi select working nicely for all resources

* configure server health check timeout

* config message

* refresh processes remove dead processes

* simplify the build args

* default timeout seconds 3

---------

Co-authored-by: kv <karamvir.singh98@gmail.com>
2024-12-01 23:34:07 -08:00
mbecker20
a8050db5f6 1.16.11 bump version 2024-11-14 02:18:26 -05:00
Maxwell Becker
bf0a972ec2 1.16.11 (#187)
* fix discord stack auto updated link

* action only log completion correctly

* add containers to omni search

* periphery build use --push

* use --password-stdin to login

* docker login stdin
2024-11-13 23:17:35 -08:00
mbecker20
23c1a08c87 fix Action success log being triggered even when there is error. 2024-11-12 19:43:55 -05:00
mbecker20
2b6b8a21ec revert monaco scroll past last line 2024-11-08 03:47:35 -05:00
mbecker20
02974b9adb monaco enable scroll beyond last line 2024-11-08 03:44:54 -05:00
Maxwell Becker
64d13666a9 1.16.10 (#178)
* send alert on auto update

* scrolling / capturing monaco editors

* deployed services has correct image

* serde default services for backward compat

* improve auto update config
2024-11-07 23:59:52 -08:00
mbecker20
2b2f354a3c add ImageUpdateAvailable filter to alert page 2024-11-05 01:30:55 -05:00
Maxwell Becker
aea5441466 1.16.9 (#172)
* BatchDestroyDeployment

* periphery image pull api

* Add Pull apis

* Add PullStack / PullDeployment

* improve init deploy from container

* stacks + deployments update_available source

* Fix deploy / destroy stack service

* updates available indicator

* add poll for updates and auto update options

* use interval to handle waiting between resource refresh

* stack auto update deploy whole stack

* format

* clean up the docs

* update available alerts

* update alerting format

* fix most clippy
2024-11-04 20:28:31 -08:00
mbecker20
97ced3b2cb frontend allow Alerter configure StackStateChanged, include Stacks and Repos in whitelist 2024-11-02 21:00:38 -04:00
Maxwell Becker
1f79987c58 1.16.8 (#170)
* update configs

* bump to 1.16.8
2024-11-01 14:35:12 -07:00
Maxwell Becker
e859a919c5 1.16.8 (#169)
* use this to extract from path

* Fix references to __ALL__
2024-11-01 14:33:41 -07:00
mbecker20
2a1270dd74 webhook check will return better status codex 2024-11-01 15:57:36 -04:00
555 changed files with 50744 additions and 24077 deletions

View File

@@ -23,7 +23,7 @@ services:
db:
extends:
file: ../test.compose.yaml
file: ../dev.compose.yaml
service: ferretdb
volumes:

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": "18.18.0"
"version": "20.12.2"
},
"ghcr.io/devcontainers-community/features/deno:1": {

View File

@@ -1,14 +0,0 @@
/target
readme.md
typeshare.toml
LICENSE
*.code-workspace
*/node_modules
*/dist
creds.toml
.core-repos
.repos
.stacks
.ssl

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
open_collective: komodo

8
.gitignore vendored
View File

@@ -1,11 +1,13 @@
target
/frontend/build
/lib/ts_client/build
node_modules
dist
.env
.env.development
.DS_Store
.idea
/frontend/build
/lib/ts_client/build
creds.toml
.komodo
.dev

3037
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,37 +3,34 @@ resolver = "2"
members = [
"bin/*",
"lib/*",
"example/*",
"client/core/rs",
"client/periphery/rs",
]
[workspace.package]
version = "1.16.7"
edition = "2021"
version = "1.18.2"
edition = "2024"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
repository = "https://github.com/mbecker20/komodo"
repository = "https://github.com/moghtech/komodo"
homepage = "https://komo.do"
[patch.crates-io]
# komodo_client = { path = "client/core/rs" }
[workspace.dependencies]
# LOCAL
# komodo_client = "1.15.6"
komodo_client = { path = "client/core/rs" }
periphery_client = { path = "client/periphery/rs" }
environment_file = { path = "lib/environment_file" }
formatting = { path = "lib/formatting" }
response = { path = "lib/response" }
command = { path = "lib/command" }
logger = { path = "lib/logger" }
cache = { path = "lib/cache" }
git = { path = "lib/git" }
# MOGH
run_command = { version = "0.0.6", features = ["async_tokio"] }
serror = { version = "0.4.7", default-features = false }
slack = { version = "0.2.0", package = "slack_client_rs" }
serror = { version = "0.5.0", default-features = false }
slack = { version = "0.4.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
derive_default_builder = "0.1.8"
derive_empty_traits = "0.1.0"
merge_config_files = "0.1.5"
@@ -41,78 +38,92 @@ async_timing_util = "1.0.0"
partial_derive2 = "0.4.3"
derive_variants = "1.0.0"
mongo_indexed = "2.0.1"
resolver_api = "1.1.1"
resolver_api = "3.0.0"
toml_pretty = "1.1.2"
mungos = "1.1.0"
mungos = "3.2.0"
svi = "1.0.1"
# ASYNC
reqwest = { version = "0.12.8", features = ["json"] }
tokio = { version = "1.38.1", features = ["full"] }
tokio-util = "0.7.12"
reqwest = { version = "0.12.20", default-features = false, features = ["json", "stream", "rustls-tls-native-roots"] }
tokio = { version = "1.45.1", features = ["full"] }
tokio-util = { version = "0.7.15", features = ["io", "codec"] }
tokio-stream = { version = "0.1.17", features = ["sync"] }
pin-project-lite = "0.2.16"
futures = "0.3.31"
futures-util = "0.3.31"
arc-swap = "1.7.1"
# SERVER
axum-extra = { version = "0.9.4", features = ["typed-header"] }
tower-http = { version = "0.6.1", features = ["fs", "cors"] }
axum-server = { version = "0.7.1", features = ["tls-openssl"] }
axum = { version = "0.7.7", features = ["ws", "json"] }
tokio-tungstenite = "0.24.0"
tokio-tungstenite = { version = "0.27.0", features = ["rustls-tls-native-roots"] }
axum-extra = { version = "0.10.1", features = ["typed-header"] }
tower-http = { version = "0.6.4", features = ["fs", "cors"] }
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
axum = { version = "0.8.4", features = ["ws", "json", "macros"] }
# SER/DE
ordered_hash_map = { version = "0.4.0", features = ["serde"] }
serde = { version = "1.0.210", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
serde_json = "1.0.132"
indexmap = { version = "2.9.0", features = ["serde"] }
serde = { version = "1.0.219", features = ["derive"] }
strum = { version = "0.27.1", features = ["derive"] }
serde_json = "1.0.140"
serde_yaml = "0.9.34"
toml = "0.8.19"
serde_qs = "0.15.0"
toml = "0.8.22"
# ERROR
anyhow = "1.0.91"
thiserror = "1.0.65"
anyhow = "1.0.98"
thiserror = "2.0.12"
# LOGGING
opentelemetry_sdk = { version = "0.25.0", features = ["rt-tokio"] }
tracing-subscriber = { version = "0.3.18", features = ["json"] }
opentelemetry-semantic-conventions = "0.25.0"
tracing-opentelemetry = "0.26.0"
opentelemetry-otlp = "0.25.0"
opentelemetry = "0.25.0"
tracing = "0.1.40"
opentelemetry-otlp = { version = "0.29.0", features = ["tls-roots", "reqwest-rustls"] }
opentelemetry_sdk = { version = "0.29.0", features = ["rt-tokio"] }
tracing-subscriber = { version = "0.3.19", features = ["json"] }
opentelemetry-semantic-conventions = "0.29.0"
tracing-opentelemetry = "0.30.0"
opentelemetry = "0.29.1"
tracing = "0.1.41"
# CONFIG
clap = { version = "4.5.20", features = ["derive"] }
clap = { version = "4.5.38", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
# CRYPTO / AUTH
uuid = { version = "1.10.0", features = ["v4", "fast-rng", "serde"] }
openidconnect = "3.5.0"
uuid = { version = "1.17.0", features = ["v4", "fast-rng", "serde"] }
jsonwebtoken = { version = "9.3.1", default-features = false }
openidconnect = "4.0.0"
urlencoding = "2.1.3"
nom_pem = "4.0.0"
bcrypt = "0.15.1"
bcrypt = "0.17.0"
base64 = "0.22.1"
rustls = "0.23.27"
hmac = "0.12.1"
sha2 = "0.10.8"
rand = "0.8.5"
jwt = "0.16.0"
sha2 = "0.10.9"
rand = "0.9.1"
hex = "0.4.3"
# SYSTEM
bollard = "0.17.1"
sysinfo = "0.32.0"
portable-pty = "0.9.0"
bollard = "0.19.0"
sysinfo = "0.35.1"
# CLOUD
aws-config = "1.5.9"
aws-sdk-ec2 = "1.83.0"
aws-config = "1.6.3"
aws-sdk-ec2 = "1.134.0"
aws-credential-types = "1.2.3"
## CRON
english-to-cron = "0.1.6"
chrono-tz = "0.10.3"
chrono = "0.4.41"
croner = "2.1.0"
# MISC
derive_builder = "0.20.2"
typeshare = "1.0.4"
octorust = "0.7.0"
octorust = "0.10.0"
dashmap = "6.1.0"
wildcard = "0.2.0"
colored = "2.1.0"
wildcard = "0.3.0"
colored = "3.0.0"
regex = "1.11.1"
bson = "2.13.0"
bytes = "1.10.1"
bson = "2.15.0"

30
bin/binaries.Dockerfile Normal file
View File

@@ -0,0 +1,30 @@
## Builds the Komodo Core, Periphery, and Util binaries
## for a specific architecture.
FROM rust:1.87.0-bullseye AS builder
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
COPY ./lib ./lib
COPY ./client/core/rs ./client/core/rs
COPY ./client/periphery ./client/periphery
COPY ./bin/core ./bin/core
COPY ./bin/periphery ./bin/periphery
COPY ./bin/util ./bin/util
# Compile bin
RUN \
cargo build -p komodo_core --release && \
cargo build -p komodo_periphery --release && \
cargo build -p komodo_util --release
# Copy just the binaries to scratch image
FROM scratch
COPY --from=builder /builder/target/release/core /core
COPY --from=builder /builder/target/release/periphery /periphery
COPY --from=builder /builder/target/release/util /util
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.description="Komodo Binaries"
LABEL org.opencontainers.image.licenses=GPL-3.0

View File

@@ -16,6 +16,7 @@ path = "src/main.rs"
[dependencies]
# local
# komodo_client = "1.16.12"
komodo_client.workspace = true
# external
tracing-subscriber.workspace = true

View File

@@ -12,7 +12,7 @@ use crate::{
};
pub enum ExecutionResult {
Single(Update),
Single(Box<Update>),
Batch(BatchExecutionResponse),
}
@@ -56,6 +56,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::BatchDeploy(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PullDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -179,6 +182,12 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
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())
}
@@ -200,6 +209,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::BatchDestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::TestAlerter(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::Sleep(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -215,231 +227,247 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::RunAction(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchRunAction(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::RunProcedure(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchRunProcedure(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::RunBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchRunBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::CancelBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::Deploy(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchDeploy(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::PullDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::StartDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::RestartDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::PauseDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::UnpauseDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::StopDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::DestroyDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchDestroyDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::CloneRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchCloneRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::PullRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchPullRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::BuildRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchBuildRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::CancelRepoBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::StartContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::RestartContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::PauseContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::UnpauseContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::StopContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::DestroyContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::StartAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::RestartAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::PauseAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::UnpauseAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::StopAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::DeleteNetwork(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneNetworks(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::DeleteImage(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneImages(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::DeleteVolume(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneVolumes(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneDockerBuilders(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneBuildx(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneSystem(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::RunSync(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::CommitSync(request) => komodo_client()
.write(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::DeployStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchDeployStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::DeployStackIfChanged(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchDeployStackIfChanged(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::PullStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchPullStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::StartStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::RestartStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::PauseStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::UnpauseStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::StopStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::DestroyStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchDestroyStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
.map(ExecutionResult::Batch),
Execution::TestAlerter(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::Sleep(request) => {
let duration =
Duration::from_millis(request.duration_ms as u64);

View File

@@ -19,8 +19,10 @@ komodo_client = { workspace = true, features = ["mongo"] }
periphery_client.workspace = true
environment_file.workspace = true
formatting.workspace = true
response.workspace = true
command.workspace = true
logger.workspace = true
cache.workspace = true
git.workspace = true
# mogh
serror = { workspace = true, features = ["axum"] }
@@ -35,9 +37,12 @@ mungos.workspace = true
slack.workspace = true
svi.workspace = true
# external
axum-server.workspace = true
ordered_hash_map.workspace = true
aws-credential-types.workspace = true
tokio-tungstenite.workspace = true
english-to-cron.workspace = true
openidconnect.workspace = true
jsonwebtoken.workspace = true
axum-server.workspace = true
urlencoding.workspace = true
aws-sdk-ec2.workspace = true
aws-config.workspace = true
@@ -47,8 +52,11 @@ tower-http.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
typeshare.workspace = true
chrono-tz.workspace = true
indexmap.workspace = true
octorust.workspace = true
wildcard.workspace = true
arc-swap.workspace = true
dashmap.workspace = true
tracing.workspace = true
reqwest.workspace = true
@@ -56,8 +64,11 @@ futures.workspace = true
nom_pem.workspace = true
dotenvy.workspace = true
anyhow.workspace = true
croner.workspace = true
chrono.workspace = true
bcrypt.workspace = true
base64.workspace = true
rustls.workspace = true
tokio.workspace = true
serde.workspace = true
regex.workspace = true
@@ -68,5 +79,4 @@ envy.workspace = true
rand.workspace = true
hmac.workspace = true
sha2.workspace = true
jwt.workspace = true
hex.workspace = true

View File

@@ -1,13 +1,16 @@
## This one produces smaller images,
## but alpine uses `musl` instead of `glibc`.
## This makes it take longer / more resources to build,
## and may negatively affect runtime performance.
## All in one, multi stage compile + runtime Docker build for your architecture.
# Build Core
FROM rust:1.82.0-alpine AS core-builder
FROM rust:1.87.0-bullseye AS core-builder
WORKDIR /builder
RUN apk update && apk --no-cache add musl-dev openssl-dev openssl-libs-static
COPY . .
COPY Cargo.toml Cargo.lock ./
COPY ./lib ./lib
COPY ./client/core/rs ./client/core/rs
COPY ./client/periphery ./client/periphery
COPY ./bin/core ./bin/core
# Compile app
RUN cargo build -p komodo_core --release
# Build Frontend
@@ -19,34 +22,33 @@ RUN cd client && yarn && yarn build && yarn link
RUN cd frontend && yarn link komodo_client && yarn && yarn build
# Final Image
FROM alpine:3.20
FROM debian:bullseye-slim
# Install Deps
RUN apk update && apk add --no-cache --virtual .build-deps \
openssl ca-certificates git git-lfs curl
COPY ./bin/core/starship.toml /config/starship.toml
COPY ./bin/core/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
# Setup an application directory
WORKDIR /app
# Copy
COPY ./config/core.config.toml /config/config.toml
COPY --from=core-builder /builder/target/release/core /app
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
COPY --from=core-builder /builder/target/release/core /usr/local/bin/core
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
# Set $DENO_DIR and preload external Deno deps
ENV DENO_DIR=/action-cache/deno
RUN mkdir /action-cache && \
cd /action-cache && \
deno install jsr:@std/yaml jsr:@std/toml
cd /action-cache && \
deno install jsr:@std/yaml jsr:@std/toml
# Hint at the port
EXPOSE 9120
EXPOSE 9120
# Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.description="Komodo Core"
LABEL org.opencontainers.image.licenses=GPL-3.0
# Using ENTRYPOINT allows cli args to be passed, eg using "command" in docker compose.
ENTRYPOINT [ "/app/core" ]
ENTRYPOINT [ "core" ]

14
bin/core/debian-deps.sh Normal file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
## Core deps installer
apt-get update
apt-get install -y git curl ca-certificates
rm -rf /var/lib/apt/lists/*
# Starship prompt
curl -sS https://starship.rs/install.sh | sh -s -- --yes --bin-dir /usr/local/bin
echo 'export STARSHIP_CONFIG=/config/starship.toml' >> /root/.bashrc
echo 'eval "$(starship init bash)"' >> /root/.bashrc

View File

@@ -0,0 +1,49 @@
## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile).
## Sets up the necessary runtime container dependencies for Komodo Core.
## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
ARG FRONTEND_IMAGE=ghcr.io/moghtech/komodo-frontend:latest
ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64
ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64
# This is required to work with COPY --from
FROM ${X86_64_BINARIES} AS x86_64
FROM ${AARCH64_BINARIES} AS aarch64
FROM ${FRONTEND_IMAGE} AS frontend
# Final Image
FROM debian:bullseye-slim
COPY ./bin/core/starship.toml /config/starship.toml
COPY ./bin/core/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
WORKDIR /app
# Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM.
COPY --from=x86_64 /core /app/arch/linux/amd64
COPY --from=aarch64 /core /app/arch/linux/arm64
ARG TARGETPLATFORM
RUN mv /app/arch/${TARGETPLATFORM} /usr/local/bin/core && rm -r /app/arch
# Copy default config / static frontend / deno binary
COPY ./config/core.config.toml /config/config.toml
COPY --from=frontend /frontend /app/frontend
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
# Set $DENO_DIR and preload external Deno deps
ENV DENO_DIR=/action-cache/deno
RUN mkdir /action-cache && \
cd /action-cache && \
deno install jsr:@std/yaml jsr:@std/toml
# Hint at the port
EXPOSE 9120
# Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.description="Komodo Core"
LABEL org.opencontainers.image.licenses=GPL-3.0
CMD [ "core" ]

View File

@@ -1,8 +1,10 @@
# Build Core
FROM rust:1.82.0-bullseye AS core-builder
WORKDIR /builder
COPY . .
RUN cargo build -p komodo_core --release
## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).
## Sets up the necessary runtime container dependencies for Komodo Core.
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
# This is required to work with COPY --from
FROM ${BINARIES_IMAGE} AS binaries
# Build Frontend
FROM node:20.12-alpine AS frontend-builder
@@ -12,21 +14,16 @@ COPY ./client/core/ts ./client
RUN cd client && yarn && yarn build && yarn link
RUN cd frontend && yarn link komodo_client && yarn && yarn build
# Final Image
FROM debian:bullseye-slim
# Install Deps
RUN apt update && \
apt install -y git ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Setup an application directory
WORKDIR /app
COPY ./bin/core/starship.toml /config/starship.toml
COPY ./bin/core/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
# Copy
COPY ./config/core.config.toml /config/config.toml
COPY --from=core-builder /builder/target/release/core /app
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
COPY --from=binaries /core /usr/local/bin/core
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
# Set $DENO_DIR and preload external Deno deps
@@ -39,8 +36,8 @@ RUN mkdir /action-cache && \
EXPOSE 9120
# Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.description="Komodo Core"
LABEL org.opencontainers.image.licenses=GPL-3.0
ENTRYPOINT [ "/app/core" ]
CMD [ "core" ]

View File

@@ -11,6 +11,12 @@ pub async fn send_alert(
) -> anyhow::Result<()> {
let level = fmt_level(alert.level);
let content = match &alert.data {
AlertData::Test { id, name } => {
let link = resource_link(ResourceTargetVariant::Alerter, id);
format!(
"{level} | If you see this message, then Alerter **{name}** is **working**\n{link}"
)
}
AlertData::ServerUnreachable {
id,
name,
@@ -22,7 +28,7 @@ pub async fn send_alert(
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | *{name}*{region} is now *reachable*\n{link}"
"{level} | **{name}**{region} is now **reachable**\n{link}"
)
}
SeverityLevel::Critical => {
@@ -31,7 +37,7 @@ pub async fn send_alert(
.map(|e| format!("\n**error**: {e:#?}"))
.unwrap_or_default();
format!(
"{level} | *{name}*{region} is *unreachable* ❌\n{link}{err}"
"{level} | **{name}**{region} is **unreachable**\n{link}{err}"
)
}
_ => unreachable!(),
@@ -46,7 +52,7 @@ pub async fn send_alert(
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
format!(
"{level} | *{name}*{region} cpu usage at *{percentage:.1}%*\n{link}"
"{level} | **{name}**{region} cpu usage at **{percentage:.1}%**\n{link}"
)
}
AlertData::ServerMem {
@@ -60,7 +66,7 @@ pub async fn send_alert(
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾\n\nUsing *{used_gb:.1} GiB* / *{total_gb:.1} GiB*\n{link}"
"{level} | **{name}**{region} memory usage at **{percentage:.1}%** 💾\n\nUsing **{used_gb:.1} GiB** / **{total_gb:.1} GiB**\n{link}"
)
}
AlertData::ServerDisk {
@@ -75,7 +81,7 @@ pub async fn send_alert(
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | *{name}*{region} disk usage at *{percentage:.1}%* 💿\nmount point: `{path:?}`\nusing *{used_gb:.1} GiB* / *{total_gb:.1} GiB*\n{link}"
"{level} | **{name}**{region} disk usage at **{percentage:.1}%** 💿\nmount point: `{path:?}`\nusing **{used_gb:.1} GiB** / **{total_gb:.1} GiB**\n{link}"
)
}
AlertData::ContainerStateChange {
@@ -88,7 +94,33 @@ pub async fn send_alert(
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let to = fmt_docker_container_state(to);
format!("📦 Deployment *{name}* is now {to}\nserver: {server_name}\nprevious: {from}\n{link}")
format!(
"📦 Deployment **{name}** is now **{to}**\nserver: **{server_name}**\nprevious: **{from}**\n{link}"
)
}
AlertData::DeploymentImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!(
"⬆ Deployment **{name}** has an update available\nserver: **{server_name}**\nimage: **{image}**\n{link}"
)
}
AlertData::DeploymentAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!(
"⬆ Deployment **{name}** was updated automatically ⏫\nserver: **{server_name}**\nimage: **{image}**\n{link}"
)
}
AlertData::StackStateChange {
id,
@@ -100,33 +132,109 @@ pub async fn send_alert(
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let to = fmt_stack_state(to);
format!("🥞 Stack *{name}* is now {to}\nserver: {server_name}\nprevious: {from}\n{link}")
format!(
"🥞 Stack **{name}** is now {to}\nserver: **{server_name}**\nprevious: **{from}**\n{link}"
)
}
AlertData::StackImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
service,
image,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
format!(
"⬆ Stack **{name}** has an update available\nserver: **{server_name}**\nservice: **{service}**\nimage: **{image}**\n{link}"
)
}
AlertData::StackAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
images,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images = images.join(", ");
format!(
"⬆ Stack **{name}** was updated automatically ⏫\nserver: **{server_name}**\n{images_label}: **{images}**\n{link}"
)
}
AlertData::AwsBuilderTerminationFailed {
instance_id,
message,
} => {
format!("{level} | Failed to terminated AWS builder instance\ninstance id: *{instance_id}*\n{message}")
format!(
"{level} | Failed to terminated AWS builder instance\ninstance id: **{instance_id}**\n{message}"
)
}
AlertData::ResourceSyncPendingUpdates { id, name } => {
let link =
resource_link(ResourceTargetVariant::ResourceSync, id);
format!(
"{level} | Pending resource sync updates on *{name}*\n{link}"
"{level} | Pending resource sync updates on **{name}**\n{link}"
)
}
AlertData::BuildFailed { id, name, version } => {
let link = resource_link(ResourceTargetVariant::Build, id);
format!("{level} | Build *{name}* failed\nversion: v{version}\n{link}")
format!(
"{level} | Build **{name}** failed\nversion: **v{version}**\n{link}"
)
}
AlertData::RepoBuildFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Repo, id);
format!("{level} | Repo build for *{name}* failed\n{link}")
format!("{level} | Repo build for **{name}** failed\n{link}")
}
AlertData::ProcedureFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Procedure, id);
format!("{level} | Procedure **{name}** failed\n{link}")
}
AlertData::ActionFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Action, id);
format!("{level} | Action **{name}** failed\n{link}")
}
AlertData::ScheduleRun {
resource_type,
id,
name,
} => {
let link = resource_link(*resource_type, id);
format!(
"{level} | **{name}** ({resource_type}) | Scheduled run started 🕝\n{link}"
)
}
AlertData::None {} => Default::default(),
};
if !content.is_empty() {
send_message(url, &content).await?;
let vars_and_secrets = get_variables_and_secrets().await?;
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
let mut url_interpolated = url.to_string();
// interpolate variables and secrets into the url
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut url_interpolated,
&mut global_replacers,
&mut secret_replacers,
)?;
send_message(&url_interpolated, &content)
.await
.map_err(|e| {
let replacers =
secret_replacers.into_iter().collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with slack request: {}",
sanitized_error
))
})?;
}
Ok(())
}

View File

@@ -1,22 +1,32 @@
use ::slack::types::Block;
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use derive_variants::ExtractVariant;
use futures::future::join_all;
use komodo_client::entities::{
alert::{Alert, AlertData, SeverityLevel},
ResourceTargetVariant,
alert::{Alert, AlertData, AlertDataVariant, SeverityLevel},
alerter::*,
deployment::DeploymentState,
komodo_timestamp,
stack::StackState,
ResourceTargetVariant,
};
use mungos::{find::find_collect, mongodb::bson::doc};
use std::collections::HashSet;
use tracing::Instrument;
use crate::helpers::query::get_variables_and_secrets;
use crate::helpers::{
interpolate::interpolate_variables_secrets_into_string,
maintenance::is_in_maintenance,
};
use crate::{config::core_config, state::db_client};
mod discord;
mod ntfy;
mod pushover;
mod slack;
#[instrument(level = "debug")]
pub async fn send_alerts(alerts: &[Alert]) {
if alerts.is_empty() {
return;
@@ -54,14 +64,38 @@ async fn send_alert(alerters: &[Alerter], alert: &Alert) {
return;
}
let handles = alerters
.iter()
.map(|alerter| send_alert_to_alerter(alerter, alert));
join_all(handles)
.await
.into_iter()
.filter_map(|res| res.err())
.for_each(|e| error!("{e:#}"));
}
pub async fn send_alert_to_alerter(
alerter: &Alerter,
alert: &Alert,
) -> anyhow::Result<()> {
// Don't send if not enabled
if !alerter.config.enabled {
return Ok(());
}
if is_in_maintenance(
&alerter.config.maintenance_windows,
komodo_timestamp(),
) {
return Ok(());
}
let alert_type = alert.data.extract_variant();
let handles = alerters.iter().map(|alerter| async {
// Don't send if not enabled
if !alerter.config.enabled {
return Ok(());
}
// In the test case, we don't want the filters inside this
// block to stop the test from being sent to the alerting endpoint.
if alert_type != AlertDataVariant::Test {
// Don't send if alert type not configured on the alerter
if !alerter.config.alert_types.is_empty()
&& !alerter.config.alert_types.contains(&alert_type)
@@ -80,40 +114,52 @@ async fn send_alert(alerters: &[Alerter], alert: &Alert) {
{
return Ok(());
}
}
match &alerter.config.endpoint {
AlerterEndpoint::Custom(CustomAlerterEndpoint { url }) => {
send_custom_alert(url, alert).await.with_context(|| {
format!(
"failed to send alert to custom alerter {}",
alerter.name
)
})
}
AlerterEndpoint::Slack(SlackAlerterEndpoint { url }) => {
slack::send_alert(url, alert).await.with_context(|| {
format!(
"failed to send alert to slack alerter {}",
alerter.name
)
})
}
AlerterEndpoint::Discord(DiscordAlerterEndpoint { url }) => {
discord::send_alert(url, alert).await.with_context(|| {
format!(
"failed to send alert to Discord alerter {}",
alerter.name
)
})
}
match &alerter.config.endpoint {
AlerterEndpoint::Custom(CustomAlerterEndpoint { url }) => {
send_custom_alert(url, alert).await.with_context(|| {
format!(
"Failed to send alert to Custom Alerter {}",
alerter.name
)
})
}
});
join_all(handles)
.await
.into_iter()
.filter_map(|res| res.err())
.for_each(|e| error!("{e:#}"));
AlerterEndpoint::Slack(SlackAlerterEndpoint { url }) => {
slack::send_alert(url, alert).await.with_context(|| {
format!(
"Failed to send alert to Slack Alerter {}",
alerter.name
)
})
}
AlerterEndpoint::Discord(DiscordAlerterEndpoint { url }) => {
discord::send_alert(url, alert).await.with_context(|| {
format!(
"Failed to send alert to Discord Alerter {}",
alerter.name
)
})
}
AlerterEndpoint::Ntfy(NtfyAlerterEndpoint { url, email }) => {
ntfy::send_alert(url, email.as_deref(), alert)
.await
.with_context(|| {
format!(
"Failed to send alert to ntfy Alerter {}",
alerter.name
)
})
}
AlerterEndpoint::Pushover(PushoverAlerterEndpoint { url }) => {
pushover::send_alert(url, alert).await.with_context(|| {
format!(
"Failed to send alert to Pushover Alerter {}",
alerter.name
)
})
}
}
}
#[instrument(level = "debug")]
@@ -121,11 +167,34 @@ async fn send_custom_alert(
url: &str,
alert: &Alert,
) -> anyhow::Result<()> {
let vars_and_secrets = get_variables_and_secrets().await?;
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
let mut url_interpolated = url.to_string();
// interpolate variables and secrets into the url
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut url_interpolated,
&mut global_replacers,
&mut secret_replacers,
)?;
let res = reqwest::Client::new()
.post(url)
.post(url_interpolated)
.json(alert)
.send()
.await
.map_err(|e| {
let replacers =
secret_replacers.into_iter().collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with request: {}",
sanitized_error
))
})
.context("failed at post request to alerter")?;
let status = res.status();
if !status.is_success() {
@@ -204,9 +273,6 @@ fn resource_link(
ResourceTargetVariant::Action => {
format!("/actions/{id}")
}
ResourceTargetVariant::ServerTemplate => {
format!("/server-templates/{id}")
}
ResourceTargetVariant::ResourceSync => {
format!("/resource-syncs/{id}")
}

272
bin/core/src/alert/ntfy.rs Normal file
View File

@@ -0,0 +1,272 @@
use std::sync::OnceLock;
use super::*;
#[instrument(level = "debug")]
pub async fn send_alert(
url: &str,
email: Option<&str>,
alert: &Alert,
) -> anyhow::Result<()> {
let level = fmt_level(alert.level);
let content = match &alert.data {
AlertData::Test { id, name } => {
let link = resource_link(ResourceTargetVariant::Alerter, id);
format!(
"{level} | If you see this message, then Alerter {} is working\n{link}",
name,
)
}
AlertData::ServerUnreachable {
id,
name,
region,
err,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | {}{} is now reachable\n{link}",
name, region
)
}
SeverityLevel::Critical => {
let err = err
.as_ref()
.map(|e| format!("\nerror: {:#?}", e))
.unwrap_or_default();
format!(
"{level} | {}{} is unreachable ❌\n{link}{err}",
name, region
)
}
_ => unreachable!(),
}
}
AlertData::ServerCpu {
id,
name,
region,
percentage,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
format!(
"{level} | {}{} cpu usage at {percentage:.1}%\n{link}",
name, region,
)
}
AlertData::ServerMem {
id,
name,
region,
used_gb,
total_gb,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | {}{} memory usage at {percentage:.1}%💾\n\nUsing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
name, region,
)
}
AlertData::ServerDisk {
id,
name,
region,
path,
used_gb,
total_gb,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | {}{} disk usage at {percentage:.1}%💿\nmount point: {:?}\nusing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
name, region, path,
)
}
AlertData::ContainerStateChange {
id,
name,
server_id: _server_id,
server_name,
from,
to,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let to_state = fmt_docker_container_state(to);
format!(
"📦Deployment {} is now {}\nserver: {}\nprevious: {}\n{link}",
name, to_state, server_name, from,
)
}
AlertData::DeploymentImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!(
"⬆ Deployment {} has an update available\nserver: {}\nimage: {}\n{link}",
name, server_name, image,
)
}
AlertData::DeploymentAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!(
"⬆ Deployment {} was updated automatically\nserver: {}\nimage: {}\n{link}",
name, server_name, image,
)
}
AlertData::StackStateChange {
id,
name,
server_id: _server_id,
server_name,
from,
to,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let to_state = fmt_stack_state(to);
format!(
"🥞 Stack {} is now {}\nserver: {}\nprevious: {}\n{link}",
name, to_state, server_name, from,
)
}
AlertData::StackImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
service,
image,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
format!(
"⬆ Stack {} has an update available\nserver: {}\nservice: {}\nimage: {}\n{link}",
name, server_name, service, image,
)
}
AlertData::StackAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
images,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images_str = images.join(", ");
format!(
"⬆ Stack {} was updated automatically ⏫\nserver: {}\n{}: {}\n{link}",
name, server_name, images_label, images_str,
)
}
AlertData::AwsBuilderTerminationFailed {
instance_id,
message,
} => {
format!(
"{level} | Failed to terminate AWS builder instance\ninstance id: {}\n{}",
instance_id, message,
)
}
AlertData::ResourceSyncPendingUpdates { id, name } => {
let link =
resource_link(ResourceTargetVariant::ResourceSync, id);
format!(
"{level} | Pending resource sync updates on {}\n{link}",
name,
)
}
AlertData::BuildFailed { id, name, version } => {
let link = resource_link(ResourceTargetVariant::Build, id);
format!(
"{level} | Build {} failed\nversion: v{}\n{link}",
name, version,
)
}
AlertData::RepoBuildFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Repo, id);
format!("{level} | Repo build for {} failed\n{link}", name,)
}
AlertData::ProcedureFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Procedure, id);
format!("{level} | Procedure {name} failed\n{link}")
}
AlertData::ActionFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Action, id);
format!("{level} | Action {name} failed\n{link}")
}
AlertData::ScheduleRun {
resource_type,
id,
name,
} => {
let link = resource_link(*resource_type, id);
format!(
"{level} | {name} ({resource_type}) | Scheduled run started 🕝\n{link}"
)
}
AlertData::None {} => Default::default(),
};
if !content.is_empty() {
send_message(url, email, content).await?;
}
Ok(())
}
async fn send_message(
url: &str,
email: Option<&str>,
content: String,
) -> anyhow::Result<()> {
let mut request = http_client()
.post(url)
.header("Title", "ntfy Alert")
.body(content);
if let Some(email) = email {
request = request.header("X-Email", email);
}
let response =
request.send().await.context("Failed to send message")?;
let status = response.status();
if status.is_success() {
debug!("ntfy alert sent successfully: {}", status);
Ok(())
} else {
let text = response.text().await.with_context(|| {
format!(
"Failed to send message to ntfy | {} | failed to get response text",
status
)
})?;
Err(anyhow!(
"Failed to send message to ntfy | {} | {}",
status,
text
))
}
}
fn http_client() -> &'static reqwest::Client {
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
CLIENT.get_or_init(reqwest::Client::new)
}

View File

@@ -0,0 +1,270 @@
use std::sync::OnceLock;
use super::*;
#[instrument(level = "debug")]
pub async fn send_alert(
url: &str,
alert: &Alert,
) -> anyhow::Result<()> {
let level = fmt_level(alert.level);
let content = match &alert.data {
AlertData::Test { id, name } => {
let link = resource_link(ResourceTargetVariant::Alerter, id);
format!(
"{level} | If you see this message, then Alerter {} is working\n{link}",
name,
)
}
AlertData::ServerUnreachable {
id,
name,
region,
err,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | {}{} is now reachable\n{link}",
name, region
)
}
SeverityLevel::Critical => {
let err = err
.as_ref()
.map(|e| format!("\nerror: {:#?}", e))
.unwrap_or_default();
format!(
"{level} | {}{} is unreachable ❌\n{link}{err}",
name, region
)
}
_ => unreachable!(),
}
}
AlertData::ServerCpu {
id,
name,
region,
percentage,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
format!(
"{level} | {}{} cpu usage at {percentage:.1}%\n{link}",
name, region,
)
}
AlertData::ServerMem {
id,
name,
region,
used_gb,
total_gb,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | {}{} memory usage at {percentage:.1}%💾\n\nUsing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
name, region,
)
}
AlertData::ServerDisk {
id,
name,
region,
path,
used_gb,
total_gb,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | {}{} disk usage at {percentage:.1}%💿\nmount point: {:?}\nusing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
name, region, path,
)
}
AlertData::ContainerStateChange {
id,
name,
server_id: _server_id,
server_name,
from,
to,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let to_state = fmt_docker_container_state(to);
format!(
"📦Deployment {} is now {}\nserver: {}\nprevious: {}\n{link}",
name, to_state, server_name, from,
)
}
AlertData::DeploymentImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!(
"⬆ Deployment {} has an update available\nserver: {}\nimage: {}\n{link}",
name, server_name, image,
)
}
AlertData::DeploymentAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!(
"⬆ Deployment {} was updated automatically\nserver: {}\nimage: {}\n{link}",
name, server_name, image,
)
}
AlertData::StackStateChange {
id,
name,
server_id: _server_id,
server_name,
from,
to,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let to_state = fmt_stack_state(to);
format!(
"🥞 Stack {} is now {}\nserver: {}\nprevious: {}\n{link}",
name, to_state, server_name, from,
)
}
AlertData::StackImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
service,
image,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
format!(
"⬆ Stack {} has an update available\nserver: {}\nservice: {}\nimage: {}\n{link}",
name, server_name, service, image,
)
}
AlertData::StackAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
images,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images_str = images.join(", ");
format!(
"⬆ Stack {} was updated automatically ⏫\nserver: {}\n{}: {}\n{link}",
name, server_name, images_label, images_str,
)
}
AlertData::AwsBuilderTerminationFailed {
instance_id,
message,
} => {
format!(
"{level} | Failed to terminate AWS builder instance\ninstance id: {}\n{}",
instance_id, message,
)
}
AlertData::ResourceSyncPendingUpdates { id, name } => {
let link =
resource_link(ResourceTargetVariant::ResourceSync, id);
format!(
"{level} | Pending resource sync updates on {}\n{link}",
name,
)
}
AlertData::BuildFailed { id, name, version } => {
let link = resource_link(ResourceTargetVariant::Build, id);
format!(
"{level} | Build {name} failed\nversion: v{version}\n{link}",
)
}
AlertData::RepoBuildFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Repo, id);
format!("{level} | Repo build for {} failed\n{link}", name,)
}
AlertData::ProcedureFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Procedure, id);
format!("{level} | Procedure {name} failed\n{link}")
}
AlertData::ActionFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Action, id);
format!("{level} | Action {name} failed\n{link}")
}
AlertData::ScheduleRun {
resource_type,
id,
name,
} => {
let link = resource_link(*resource_type, id);
format!(
"{level} | {name} ({resource_type}) | Scheduled run started 🕝\n{link}"
)
}
AlertData::None {} => Default::default(),
};
if !content.is_empty() {
send_message(url, content).await?;
}
Ok(())
}
async fn send_message(
url: &str,
content: String,
) -> anyhow::Result<()> {
// pushover needs all information to be encoded in the URL. At minimum they need
// the user key, the application token, and the message (url encoded).
// other optional params here: https://pushover.net/api (just add them to the
// webhook url along with the application token and the user key).
let content = [("message", content)];
let response = http_client()
.post(url)
.form(&content)
.send()
.await
.context("Failed to send message")?;
let status = response.status();
if status.is_success() {
debug!("pushover alert sent successfully: {}", status);
Ok(())
} else {
let text = response.text().await.with_context(|| {
format!(
"Failed to send message to pushover | {} | failed to get response text",
status
)
})?;
Err(anyhow!(
"Failed to send message to pushover | {} | {}",
status,
text
))
}
}
fn http_client() -> &'static reqwest::Client {
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
CLIENT.get_or_init(reqwest::Client::new)
}

View File

@@ -7,6 +7,22 @@ pub async fn send_alert(
) -> anyhow::Result<()> {
let level = fmt_level(alert.level);
let (text, blocks): (_, Option<_>) = match &alert.data {
AlertData::Test { id, name } => {
let text = format!(
"{level} | If you see this message, then Alerter *{name}* is *working*"
);
let blocks = vec![
Block::header(level),
Block::section(format!(
"If you see this message, then Alerter *{name}* is *working*"
)),
Block::section(resource_link(
ResourceTargetVariant::Alerter,
id,
)),
];
(text, blocks.into())
}
AlertData::ServerUnreachable {
id,
name,
@@ -57,7 +73,9 @@ pub async fn send_alert(
let region = fmt_region(region);
match alert.level {
SeverityLevel::Ok => {
let text = format!("{level} | *{name}*{region} cpu usage at *{percentage:.1}%*");
let text = format!(
"{level} | *{name}*{region} cpu usage at *{percentage:.1}%*"
);
let blocks = vec![
Block::header(level),
Block::section(format!(
@@ -71,7 +89,9 @@ pub async fn send_alert(
(text, blocks.into())
}
_ => {
let text = format!("{level} | *{name}*{region} cpu usage at *{percentage:.1}%* 📈");
let text = format!(
"{level} | *{name}*{region} cpu usage at *{percentage:.1}%* 📈"
);
let blocks = vec![
Block::header(level),
Block::section(format!(
@@ -97,7 +117,9 @@ pub async fn send_alert(
let percentage = 100.0 * used_gb / total_gb;
match alert.level {
SeverityLevel::Ok => {
let text = format!("{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾");
let text = format!(
"{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾"
);
let blocks = vec![
Block::header(level),
Block::section(format!(
@@ -114,7 +136,9 @@ pub async fn send_alert(
(text, blocks.into())
}
_ => {
let text = format!("{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾");
let text = format!(
"{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾"
);
let blocks = vec![
Block::header(level),
Block::section(format!(
@@ -144,7 +168,9 @@ pub async fn send_alert(
let percentage = 100.0 * used_gb / total_gb;
match alert.level {
SeverityLevel::Ok => {
let text = format!("{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿");
let text = format!(
"{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿"
);
let blocks = vec![
Block::header(level),
Block::section(format!(
@@ -153,12 +179,17 @@ pub async fn send_alert(
Block::section(format!(
"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
)),
Block::section(resource_link(ResourceTargetVariant::Server, id)),
Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
];
(text, blocks.into())
}
_ => {
let text = format!("{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿");
let text = format!(
"{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿"
);
let blocks = vec![
Block::header(level),
Block::section(format!(
@@ -167,7 +198,10 @@ pub async fn send_alert(
Block::section(format!(
"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
)),
Block::section(resource_link(ResourceTargetVariant::Server, id)),
Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
];
(text, blocks.into())
}
@@ -182,7 +216,7 @@ pub async fn send_alert(
..
} => {
let to = fmt_docker_container_state(to);
let text = format!("📦 Container *{name}* is now {to}");
let text = format!("📦 Container *{name}* is now *{to}*");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
@@ -195,6 +229,48 @@ pub async fn send_alert(
];
(text, blocks.into())
}
AlertData::DeploymentImageUpdateAvailable {
id,
name,
server_name,
server_id: _server_id,
image,
} => {
let text =
format!("⬆ Deployment *{name}* has an update available");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\nimage: *{image}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Deployment,
id,
)),
];
(text, blocks.into())
}
AlertData::DeploymentAutoUpdated {
id,
name,
server_name,
server_id: _server_id,
image,
} => {
let text =
format!("⬆ Deployment *{name}* was updated automatically ⏫");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\nimage: *{image}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Deployment,
id,
)),
];
(text, blocks.into())
}
AlertData::StackStateChange {
name,
server_name,
@@ -204,11 +280,56 @@ pub async fn send_alert(
..
} => {
let to = fmt_stack_state(to);
let text = format!("🥞 Stack *{name}* is now {to}");
let text = format!("🥞 Stack *{name}* is now *{to}*");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: {server_name}\nprevious: {from}",
"server: *{server_name}*\nprevious: *{from}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Stack,
id,
)),
];
(text, blocks.into())
}
AlertData::StackImageUpdateAvailable {
id,
name,
server_name,
server_id: _server_id,
service,
image,
} => {
let text = format!("⬆ Stack *{name}* has an update available");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\nservice: *{service}*\nimage: *{image}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Stack,
id,
)),
];
(text, blocks.into())
}
AlertData::StackAutoUpdated {
id,
name,
server_name,
server_id: _server_id,
images,
} => {
let text =
format!("⬆ Stack *{name}* was updated automatically ⏫");
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images = images.join(", ");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\n{images_label}: *{images}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Stack,
@@ -233,8 +354,9 @@ pub async fn send_alert(
(text, blocks.into())
}
AlertData::ResourceSyncPendingUpdates { id, name } => {
let text =
format!("{level} | Pending resource sync updates on {name}");
let text = format!(
"{level} | Pending resource sync updates on *{name}*"
);
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
@@ -251,21 +373,19 @@ pub async fn send_alert(
let text = format!("{level} | Build {name} has failed");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"build id: *{id}*\nbuild name: *{name}*\nversion: v{version}",
Block::section(format!("version: *v{version}*",)),
Block::section(resource_link(
ResourceTargetVariant::Build,
id,
)),
Block::section(resource_link(ResourceTargetVariant::Build, id))
];
(text, blocks.into())
}
AlertData::RepoBuildFailed { id, name } => {
let text =
format!("{level} | Repo build for {name} has failed");
format!("{level} | Repo build for *{name}* has *failed*");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"repo id: *{id}*\nrepo name: *{name}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Repo,
id,
@@ -273,11 +393,69 @@ pub async fn send_alert(
];
(text, blocks.into())
}
AlertData::ProcedureFailed { id, name } => {
let text = format!("{level} | Procedure *{name}* has *failed*");
let blocks = vec![
Block::header(text.clone()),
Block::section(resource_link(
ResourceTargetVariant::Procedure,
id,
)),
];
(text, blocks.into())
}
AlertData::ActionFailed { id, name } => {
let text = format!("{level} | Action *{name}* has *failed*");
let blocks = vec![
Block::header(text.clone()),
Block::section(resource_link(
ResourceTargetVariant::Action,
id,
)),
];
(text, blocks.into())
}
AlertData::ScheduleRun {
resource_type,
id,
name,
} => {
let text = format!(
"{level} | *{name}* ({resource_type}) | Scheduled run started 🕝"
);
let blocks = vec![
Block::header(text.clone()),
Block::section(resource_link(*resource_type, id)),
];
(text, blocks.into())
}
AlertData::None {} => Default::default(),
};
if !text.is_empty() {
let slack = ::slack::Client::new(url);
slack.send_message(text, blocks).await?;
let vars_and_secrets = get_variables_and_secrets().await?;
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
let mut url_interpolated = url.to_string();
// interpolate variables and secrets into the url
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut url_interpolated,
&mut global_replacers,
&mut secret_replacers,
)?;
let slack = ::slack::Client::new(url_interpolated);
slack.send_message(text, blocks).await.map_err(|e| {
let replacers =
secret_replacers.into_iter().collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with slack request: {}",
sanitized_error
))
})?;
}
Ok(())
}

View File

@@ -1,13 +1,13 @@
use std::{sync::OnceLock, time::Instant};
use anyhow::anyhow;
use axum::{http::HeaderMap, routing::post, Router};
use axum_extra::{headers::ContentType, TypedHeader};
use axum::{Router, extract::Path, http::HeaderMap, routing::post};
use derive_variants::{EnumVariants, ExtractVariant};
use komodo_client::{api::auth::*, entities::user::User};
use reqwest::StatusCode;
use resolver_api::{derive::Resolver, Resolve, Resolver};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serror::{AddStatusCode, Json};
use serde_json::json;
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
@@ -16,17 +16,27 @@ use crate::{
get_user_id_from_headers,
github::{self, client::github_oauth_client},
google::{self, client::google_oauth_client},
oidc,
oidc::{self, client::oidc_client},
},
config::core_config,
helpers::query::get_user,
state::{jwt_client, State},
state::jwt_client,
};
use super::Variant;
pub struct AuthArgs {
pub headers: HeaderMap,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
#[resolver_target(State)]
#[resolver_args(HeaderMap)]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
)]
#[args(AuthArgs)]
#[response(Response)]
#[error(serror::Error)]
#[variant_derive(Debug)]
#[serde(tag = "type", content = "params")]
#[allow(clippy::enum_variant_names, clippy::large_enum_variant)]
pub enum AuthRequest {
@@ -38,7 +48,9 @@ pub enum AuthRequest {
}
pub fn router() -> Router {
let mut router = Router::new().route("/", post(handler));
let mut router = Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler));
if core_config().local_auth {
info!("🔑 Local Login Enabled");
@@ -62,31 +74,36 @@ pub fn router() -> Router {
router
}
async fn variant_handler(
headers: HeaderMap,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<axum::response::Response> {
let req: AuthRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
}))?;
handler(headers, Json(req)).await
}
#[instrument(name = "AuthHandler", level = "debug", skip(headers))]
async fn handler(
headers: HeaderMap,
Json(request): Json<AuthRequest>,
) -> serror::Result<(TypedHeader<ContentType>, String)> {
) -> serror::Result<axum::response::Response> {
let timer = Instant::now();
let req_id = Uuid::new_v4();
debug!("/auth request {req_id} | METHOD: {}", request.req_type());
let res = State.resolve_request(request, headers).await.map_err(
|e| match e {
resolver_api::Error::Serialization(e) => {
anyhow!("{e:?}").context("response serialization error")
}
resolver_api::Error::Inner(e) => e,
},
debug!(
"/auth request {req_id} | METHOD: {:?}",
request.extract_variant()
);
let res = request.resolve(&AuthArgs { headers }).await;
if let Err(e) = &res {
debug!("/auth request {req_id} | error: {e:#}");
debug!("/auth request {req_id} | error: {:#}", e.error);
}
let elapsed = timer.elapsed();
debug!("/auth request {req_id} | resolve time: {elapsed:?}");
Ok((
TypedHeader(ContentType::json()),
res.status_code(StatusCode::UNAUTHORIZED)?,
))
res.map(|res| res.0)
}
fn login_options_reponse() -> &'static GetLoginOptionsResponse {
@@ -97,53 +114,42 @@ fn login_options_reponse() -> &'static GetLoginOptionsResponse {
let config = core_config();
GetLoginOptionsResponse {
local: config.local_auth,
github: config.github_oauth.enabled
&& !config.github_oauth.id.is_empty()
&& !config.github_oauth.secret.is_empty(),
google: config.google_oauth.enabled
&& !config.google_oauth.id.is_empty()
&& !config.google_oauth.secret.is_empty(),
oidc: config.oidc_enabled
&& !config.oidc_provider.is_empty()
&& !config.oidc_client_id.is_empty()
&& !config.oidc_client_secret.is_empty(),
github: github_oauth_client().is_some(),
google: google_oauth_client().is_some(),
oidc: oidc_client().load().is_some(),
registration_disabled: config.disable_user_registration,
}
})
}
impl Resolve<GetLoginOptions, HeaderMap> for State {
impl Resolve<AuthArgs> for GetLoginOptions {
#[instrument(name = "GetLoginOptions", level = "debug", skip(self))]
async fn resolve(
&self,
_: GetLoginOptions,
_: HeaderMap,
) -> anyhow::Result<GetLoginOptionsResponse> {
self,
_: &AuthArgs,
) -> serror::Result<GetLoginOptionsResponse> {
Ok(*login_options_reponse())
}
}
impl Resolve<ExchangeForJwt, HeaderMap> for State {
impl Resolve<AuthArgs> for ExchangeForJwt {
#[instrument(name = "ExchangeForJwt", level = "debug", skip(self))]
async fn resolve(
&self,
ExchangeForJwt { token }: ExchangeForJwt,
_: HeaderMap,
) -> anyhow::Result<ExchangeForJwtResponse> {
let jwt = jwt_client().redeem_exchange_token(&token).await?;
let res = ExchangeForJwtResponse { jwt };
Ok(res)
self,
_: &AuthArgs,
) -> serror::Result<ExchangeForJwtResponse> {
let jwt = jwt_client().redeem_exchange_token(&self.token).await?;
Ok(ExchangeForJwtResponse { jwt })
}
}
impl Resolve<GetUser, HeaderMap> for State {
impl Resolve<AuthArgs> for GetUser {
#[instrument(name = "GetUser", level = "debug", skip(self))]
async fn resolve(
&self,
GetUser {}: GetUser,
headers: HeaderMap,
) -> anyhow::Result<User> {
let user_id = get_user_id_from_headers(&headers).await?;
get_user(&user_id).await
self,
AuthArgs { headers }: &AuthArgs,
) -> serror::Result<User> {
let user_id = get_user_id_from_headers(headers).await?;
Ok(get_user(&user_id).await?)
}
}

View File

@@ -14,10 +14,12 @@ use komodo_client::{
},
entities::{
action::Action,
alert::{Alert, AlertData, SeverityLevel},
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
update::Update,
user::{action_user, User},
user::action_user,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
@@ -25,7 +27,8 @@ use resolver_api::Resolve;
use tokio::fs;
use crate::{
api::execute::ExecuteRequest,
alert::send_alerts,
api::{execute::ExecuteRequest, user::UserArgs},
config::core_config,
helpers::{
interpolate::{
@@ -36,10 +39,13 @@ use crate::{
random_string,
update::update_update,
},
resource::{self, refresh_action_state_cache},
state::{action_states, db_client, State},
permission::get_check_permissions,
resource::refresh_action_state_cache,
state::{action_states, db_client},
};
use super::ExecuteArgs;
impl super::BatchExecute for BatchRunAction {
type Resource = Action;
fn single_request(action: String) -> ExecuteRequest {
@@ -47,28 +53,29 @@ impl super::BatchExecute for BatchRunAction {
}
}
impl Resolve<BatchRunAction, (User, Update)> for State {
impl Resolve<ExecuteArgs> for BatchRunAction {
#[instrument(name = "BatchRunAction", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchRunAction { pattern }: BatchRunAction,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchRunAction>(&pattern, &user).await
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunAction>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<RunAction, (User, Update)> for State {
#[instrument(name = "RunAction", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for RunAction {
#[instrument(name = "RunAction", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
RunAction { action }: RunAction,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let mut action = resource::get_check_permissions::<Action>(
&action,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut action = get_check_permissions::<Action>(
&self.action,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -83,17 +90,18 @@ impl Resolve<RunAction, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.running = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let CreateApiKeyResponse { key, secret } = State
.resolve(
CreateApiKey {
name: update.id.clone(),
expires: 0,
},
action_user().to_owned(),
)
.await?;
let CreateApiKeyResponse { key, secret } = CreateApiKey {
name: update.id.clone(),
expires: 0,
}
.resolve(&UserArgs {
user: action_user().to_owned(),
})
.await?;
let contents = &mut action.config.file_contents;
@@ -110,19 +118,37 @@ impl Resolve<RunAction, (User, Update)> for State {
let path = core_config().action_directory.join(&file);
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent).await;
fs::create_dir_all(parent)
.await
.with_context(|| format!("Failed to initialize Action file parent directory {parent:?}"))?;
}
fs::write(&path, contents).await.with_context(|| {
format!("Failed to write action file to {path:?}")
})?;
let CoreConfig { ssl_enabled, .. } = core_config();
let https_cert_flag = if *ssl_enabled {
" --unsafely-ignore-certificate-errors=localhost"
} else {
""
};
let reload = if action.config.reload_deno_deps {
" --reload"
} else {
""
};
let mut res = run_komodo_command(
// Keep this stage name as is, the UI will find the latest update log by matching the stage name
"Execute Action",
None,
format!("deno run --allow-all {}", path.display()),
false,
format!(
"deno run --allow-all{https_cert_flag}{reload} {}",
path.display()
),
)
.await;
@@ -133,12 +159,15 @@ impl Resolve<RunAction, (User, Update)> for State {
cleanup_run(file + ".js", &path).await;
if let Err(e) = State
.resolve(DeleteApiKey { key }, action_user().to_owned())
if let Err(e) = (DeleteApiKey { key })
.resolve(&UserArgs {
user: action_user().to_owned(),
})
.await
{
warn!(
"Failed to delete API key after action execution | {e:#}"
"Failed to delete API key after action execution | {:#}",
e.error
);
};
@@ -162,6 +191,26 @@ impl Resolve<RunAction, (User, Update)> for State {
update_update(update.clone()).await?;
if !update.success && action.config.failure_alert {
warn!("action unsuccessful, alerting...");
let target = update.target.clone();
tokio::spawn(async move {
let alert = Alert {
id: Default::default(),
target,
ts: komodo_timestamp(),
resolved_ts: Some(komodo_timestamp()),
resolved: true,
level: SeverityLevel::Warning,
data: AlertData::ActionFailed {
id: action.id,
name: action.name,
},
};
send_alerts(&[alert]).await
});
}
Ok(update)
}
}
@@ -171,7 +220,7 @@ async fn interpolate(
update: &mut Update,
key: String,
secret: String,
) -> anyhow::Result<HashSet<(String, String)>> {
) -> serror::Result<HashSet<(String, String)>> {
let mut vars_and_secrets = get_variables_and_secrets().await?;
vars_and_secrets
@@ -203,7 +252,7 @@ fn full_contents(contents: &str, key: &str, secret: &str) -> String {
let protocol = if *ssl_enabled { "https" } else { "http" };
let base_url = format!("{protocol}://localhost:{port}");
format!(
"import {{ KomodoClient }} from '{base_url}/client/lib.js';
"import {{ KomodoClient, Types }} from '{base_url}/client/lib.js';
import * as __YAML__ from 'jsr:@std/yaml';
import * as __TOML__ from 'jsr:@std/toml';
@@ -226,18 +275,23 @@ const komodo = KomodoClient('{base_url}', {{
params: {{ key: '{key}', secret: '{secret}' }}
}});
async function main() {{{contents}}}
async function main() {{
{contents}
main().catch(error => {{
console.log('🦎 Action completed successfully 🦎');
}}
main()
.catch(error => {{
console.error('🚨 Action exited early with errors 🚨')
if (error.status !== undefined && error.result !== undefined) {{
console.error('Status:', error.status);
console.error(JSON.stringify(error.result, null, 2));
}} else {{
console.error(JSON.stringify(error, null, 2));
console.error(error);
}}
Deno.exit(1)
}}).then(() => console.log('🦎 Action completed successfully 🦎'));"
}});"
)
}
@@ -296,8 +350,8 @@ fn delete_file(
if name == file {
if let Err(e) = fs::remove_file(entry.path()).await {
warn!(
"Failed to clean up generated file after action execution | {e:#}"
);
"Failed to clean up generated file after action execution | {e:#}"
);
};
return true;
}

View File

@@ -0,0 +1,73 @@
use formatting::format_serror;
use komodo_client::{
api::execute::TestAlerter,
entities::{
alert::{Alert, AlertData, SeverityLevel},
alerter::Alerter,
komodo_timestamp,
permission::PermissionLevel,
},
};
use resolver_api::Resolve;
use crate::{
alert::send_alert_to_alerter, helpers::update::update_update,
permission::get_check_permissions,
};
use super::ExecuteArgs;
impl Resolve<ExecuteArgs> for TestAlerter {
#[instrument(name = "TestAlerter", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let alerter = get_check_permissions::<Alerter>(
&self.alerter,
user,
PermissionLevel::Execute.into(),
)
.await?;
let mut update = update.clone();
if !alerter.config.enabled {
update.push_error_log(
"Test Alerter",
String::from(
"Alerter is disabled. Enable the Alerter to send alerts.",
),
);
update.finalize();
update_update(update.clone()).await?;
return Ok(update);
}
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
ts,
resolved: true,
level: SeverityLevel::Ok,
target: update.target.clone(),
data: AlertData::Test {
id: alerter.id.clone(),
name: alerter.name.clone(),
},
resolved_ts: Some(ts),
};
if let Err(e) = send_alert_to_alerter(&alerter, &alert).await {
update.push_error_log("Test Alerter", format_serror(&e.into()));
} else {
update.push_simple_log("Test Alerter", String::from("Alert sent successfully. It should be visible at your alerting destination."));
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}

View File

@@ -1,6 +1,6 @@
use std::{collections::HashSet, future::IntoFuture, time::Duration};
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use formatting::format_serror;
use futures::future::join_all;
use komodo_client::{
@@ -16,8 +16,9 @@ use komodo_client::{
deployment::DeploymentState,
komodo_timestamp,
permission::PermissionLevel,
repo::Repo,
update::{Log, Update},
user::{auto_redeploy_user, User},
user::auto_redeploy_user,
},
};
use mungos::{
@@ -35,9 +36,9 @@ use tokio_util::sync::CancellationToken;
use crate::{
alert::send_alerts,
helpers::{
build_git_token,
builder::{cleanup_builder_instance, get_builder_periphery},
channel::build_cancel_channel,
git_token,
interpolate::{
add_interp_update_log,
interpolate_variables_secrets_into_extra_args,
@@ -48,11 +49,12 @@ use crate::{
registry_token,
update::{init_execution_update, update_update},
},
permission::get_check_permissions,
resource::{self, refresh_build_state_cache},
state::{action_states, db_client, State},
state::{action_states, db_client},
};
use super::ExecuteRequest;
use super::{ExecuteArgs, ExecuteRequest};
impl super::BatchExecute for BatchRunBuild {
type Resource = Build;
@@ -61,34 +63,51 @@ impl super::BatchExecute for BatchRunBuild {
}
}
impl Resolve<BatchRunBuild, (User, Update)> for State {
#[instrument(name = "BatchRunBuild", skip(self, user), fields(user_id = user.id))]
impl Resolve<ExecuteArgs> for BatchRunBuild {
#[instrument(name = "BatchRunBuild", skip(user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchRunBuild { pattern }: BatchRunBuild,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchRunBuild>(&pattern, &user).await
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunBuild>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<RunBuild, (User, Update)> for State {
#[instrument(name = "RunBuild", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for RunBuild {
#[instrument(name = "RunBuild", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
RunBuild { build }: RunBuild,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let mut build = resource::get_check_permissions::<Build>(
&build,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut build = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Execute.into(),
)
.await?;
let mut repo = if !build.config.files_on_host
&& !build.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&build.config.linked_repo)
.await?
.into()
} else {
None
};
let mut vars_and_secrets = get_variables_and_secrets().await?;
// Add the $VERSION to variables. Use with [[$VERSION]]
vars_and_secrets.variables.insert(
String::from("$VERSION"),
build.config.version.to_string(),
);
if build.config.builder_id.is_empty() {
return Err(anyhow!("Must attach builder to RunBuild"));
return Err(anyhow!("Must attach builder to RunBuild").into());
}
// get the action state for the build (or insert default).
@@ -103,26 +122,14 @@ impl Resolve<RunBuild, (User, Update)> for State {
if build.config.auto_increment_version {
build.config.version.increment();
}
let mut update = update.clone();
update.version = build.config.version;
update_update(update.clone()).await?;
// Add the $VERSION to variables. Use with [[$VERSION]]
if !vars_and_secrets.variables.contains_key("$VERSION") {
vars_and_secrets.variables.insert(
String::from("$VERSION"),
build.config.version.to_string(),
);
}
let git_token = git_token(
&build.config.git_provider,
&build.config.git_account,
|https| build.config.git_https = https,
)
.await
.with_context(
|| format!("Failed to get git token in call to db. This is a database error, not a token exisitence error. Stopping run. | {} | {}", build.config.git_provider, build.config.git_account),
)?;
let git_token =
build_git_token(&mut build, repo.as_mut()).await?;
let registry_token =
validate_account_extract_registry_token(&build).await?;
@@ -173,7 +180,6 @@ impl Resolve<RunBuild, (User, Update)> for State {
});
// GET BUILDER PERIPHERY
let (periphery, cleanup_data) = match get_builder_periphery(
build.name.clone(),
Some(build.config.version),
@@ -199,9 +205,8 @@ impl Resolve<RunBuild, (User, Update)> for State {
}
};
// CLONE REPO
// INTERPOLATE VARIABLES
let secret_replacers = if !build.config.skip_secret_interp {
// Interpolate variables / secrets into pre build command
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
@@ -212,6 +217,34 @@ impl Resolve<RunBuild, (User, Update)> for State {
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.build_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.secret_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.dockerfile,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_extra_args(
&vars_and_secrets,
&mut build.config.extra_args,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(
&mut update,
&global_replacers,
@@ -223,88 +256,63 @@ impl Resolve<RunBuild, (User, Update)> for State {
Default::default()
};
let res = tokio::select! {
res = periphery
.request(api::git::CloneRepo {
args: (&build).into(),
git_token,
environment: Default::default(),
env_file_path: Default::default(),
skip_secret_interp: Default::default(),
replacers: secret_replacers.into_iter().collect(),
}) => res,
_ = cancel.cancelled() => {
debug!("build cancelled during clone, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
info!("builder cleaned up");
return handle_early_return(update, build.id, build.name, true).await
},
};
let commit_message = match res {
Ok(res) => {
debug!("finished repo clone");
update.logs.extend(res.logs);
update.commit_hash =
res.commit_hash.unwrap_or_default().to_string();
res.commit_message.unwrap_or_default()
}
Err(e) => {
warn!("failed build at clone repo | {e:#}");
update.push_error_log(
"clone repo",
format_serror(&e.context("failed to clone repo").into()),
);
Default::default()
}
};
update_update(update.clone()).await?;
if all_logs_success(&update.logs) {
let secret_replacers = if !build.config.skip_secret_interp {
// Interpolate variables / secrets into build args
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.build_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.secret_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_extra_args(
&vars_and_secrets,
&mut build.config.extra_args,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(
&mut update,
&global_replacers,
&secret_replacers,
);
secret_replacers
} else {
Default::default()
let commit_message = if !build.config.files_on_host
&& (!build.config.repo.is_empty()
|| !build.config.linked_repo.is_empty())
{
// PULL OR CLONE REPO
let res = tokio::select! {
res = periphery
.request(api::git::PullOrCloneRepo {
args: repo.as_ref().map(Into::into).unwrap_or((&build).into()),
git_token,
environment: Default::default(),
env_file_path: Default::default(),
skip_secret_interp: Default::default(),
replacers: Default::default(),
}) => res,
_ = cancel.cancelled() => {
debug!("build cancelled during clone, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(cleanup_data, &mut update)
.await;
info!("builder cleaned up");
return handle_early_return(update, build.id, build.name, true).await
},
};
let commit_message = match res {
Ok(res) => {
debug!("finished repo clone");
update.logs.extend(res.logs);
update.commit_hash =
res.commit_hash.unwrap_or_default().to_string();
res.commit_message.unwrap_or_default()
}
Err(e) => {
warn!("Failed build at clone repo | {e:#}");
update.push_error_log(
"Clone Repo",
format_serror(&e.context("Failed to clone repo").into()),
);
Default::default()
}
};
update_update(update.clone()).await?;
Some(commit_message)
} else {
None
};
if all_logs_success(&update.logs) {
// RUN BUILD
let res = tokio::select! {
res = periphery
.request(api::build::Build {
build: build.clone(),
repo,
registry_token,
replacers: secret_replacers.into_iter().collect(),
// Push a commit hash tagged image
@@ -317,7 +325,7 @@ impl Resolve<RunBuild, (User, Update)> for State {
_ = cancel.cancelled() => {
info!("build cancelled during build, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during docker build"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
cleanup_builder_instance(cleanup_data, &mut update)
.await;
return handle_early_return(update, build.id, build.name, true).await
},
@@ -361,8 +369,9 @@ impl Resolve<RunBuild, (User, Update)> for State {
// stop the cancel listening task from going forever
cancel.cancel();
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// If building on temporary cloud server (AWS),
// this will terminate the server.
cleanup_builder_instance(cleanup_data, &mut update).await;
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
@@ -408,7 +417,7 @@ impl Resolve<RunBuild, (User, Update)> for State {
});
}
Ok(update)
Ok(update.clone())
}
}
@@ -418,7 +427,7 @@ async fn handle_early_return(
build_id: String,
build_name: String,
is_cancel: bool,
) -> anyhow::Result<Update> {
) -> serror::Result<Update> {
update.finalize();
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
@@ -456,7 +465,7 @@ async fn handle_early_return(
send_alerts(&[alert]).await
});
}
Ok(update)
Ok(update.clone())
}
pub async fn validate_cancel_build(
@@ -505,17 +514,16 @@ pub async fn validate_cancel_build(
Ok(())
}
impl Resolve<CancelBuild, (User, Update)> for State {
#[instrument(name = "CancelBuild", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for CancelBuild {
#[instrument(name = "CancelBuild", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
CancelBuild { build }: CancelBuild,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let build = resource::get_check_permissions::<Build>(
&build,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let build = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -527,9 +535,11 @@ impl Resolve<CancelBuild, (User, Update)> for State {
.and_then(|s| s.get().ok().map(|s| s.building))
.unwrap_or_default()
{
return Err(anyhow!("Build is not building."));
return Err(anyhow!("Build is not building.").into());
}
let mut update = update.clone();
update.push_simple_log(
"cancel triggered",
"the build cancel has been triggered",
@@ -555,7 +565,9 @@ impl Resolve<CancelBuild, (User, Update)> for State {
)
.await
{
warn!("failed to set CancelBuild Update status Complete after timeout | {e:#}")
warn!(
"failed to set CancelBuild Update status Complete after timeout | {e:#}"
)
}
});
@@ -582,8 +594,9 @@ async fn handle_post_build_redeploy(build_id: &str) {
redeploy_deployments
.into_iter()
.map(|deployment| async move {
let state =
get_deployment_state(&deployment).await.unwrap_or_default();
let state = get_deployment_state(&deployment.id)
.await
.unwrap_or_default();
if state == DeploymentState::Running {
let req = super::ExecuteRequest::Deploy(Deploy {
deployment: deployment.id.clone(),
@@ -593,16 +606,13 @@ async fn handle_post_build_redeploy(build_id: &str) {
let user = auto_redeploy_user().to_owned();
let res = async {
let update = init_execution_update(&req, &user).await?;
State
.resolve(
Deploy {
deployment: deployment.id.clone(),
stop_signal: None,
stop_time: None,
},
(user, update),
)
.await
Deploy {
deployment: deployment.id.clone(),
stop_signal: None,
stop_time: None,
}
.resolve(&ExecuteArgs { user, update })
.await
}
.await;
Some((deployment.id.clone(), res))
@@ -616,7 +626,10 @@ async fn handle_post_build_redeploy(build_id: &str) {
continue;
};
if let Err(e) = res {
warn!("failed post build redeploy for deployment {id}: {e:#}");
warn!(
"failed post build redeploy for deployment {id}: {:#}",
e.error
);
}
}
}
@@ -636,14 +649,17 @@ async fn validate_account_extract_registry_token(
},
..
}: &Build,
) -> anyhow::Result<Option<String>> {
) -> serror::Result<Option<String>> {
if domain.is_empty() {
return Ok(None);
}
if account.is_empty() {
return Err(anyhow!(
"Must attach account to use registry provider {domain}"
));
return Err(
anyhow!(
"Must attach account to use registry provider {domain}"
)
.into(),
);
}
let registry_token = registry_token(domain, account).await.with_context(

View File

@@ -1,20 +1,21 @@
use std::collections::HashSet;
use std::{collections::HashSet, sync::OnceLock};
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use cache::TimeoutCache;
use formatting::format_serror;
use komodo_client::{
api::execute::*,
entities::{
Version,
build::{Build, ImageRegistryConfig},
deployment::{
extract_registry_domain, Deployment, DeploymentImage,
Deployment, DeploymentImage, extract_registry_domain,
},
get_image_name,
get_image_name, komodo_timestamp, optional_string,
permission::PermissionLevel,
server::Server,
update::{Log, Update},
user::User,
Version,
},
};
use periphery_client::api;
@@ -33,11 +34,12 @@ use crate::{
update::update_update,
},
monitor::update_cache_for_server,
permission::get_check_permissions,
resource,
state::{action_states, State},
state::action_states,
};
use super::ExecuteRequest;
use super::{ExecuteArgs, ExecuteRequest};
impl super::BatchExecute for BatchDeploy {
type Resource = Deployment;
@@ -50,14 +52,16 @@ impl super::BatchExecute for BatchDeploy {
}
}
impl Resolve<BatchDeploy, (User, Update)> for State {
#[instrument(name = "BatchDeploy", skip(self, user), fields(user_id = user.id))]
impl Resolve<ExecuteArgs> for BatchDeploy {
#[instrument(name = "BatchDeploy", skip(user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchDeploy { pattern }: BatchDeploy,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDeploy>(&pattern, &user).await
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeploy>(&self.pattern, user)
.await?,
)
}
}
@@ -65,36 +69,35 @@ async fn setup_deployment_execution(
deployment: &str,
user: &User,
) -> anyhow::Result<(Deployment, Server)> {
let deployment = resource::get_check_permissions::<Deployment>(
let deployment = get_check_permissions::<Deployment>(
deployment,
user,
PermissionLevel::Execute,
PermissionLevel::Execute.into(),
)
.await?;
if deployment.config.server_id.is_empty() {
return Err(anyhow!("deployment has no server configured"));
return Err(anyhow!("Deployment has no Server configured"));
}
let server =
resource::get::<Server>(&deployment.config.server_id).await?;
if !server.config.enabled {
return Err(anyhow!("Attached Server is not enabled"));
}
Ok((deployment, server))
}
impl Resolve<Deploy, (User, Update)> for State {
#[instrument(name = "Deploy", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for Deploy {
#[instrument(name = "Deploy", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
Deploy {
deployment,
stop_signal,
stop_time,
}: Deploy,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (mut deployment, server) =
setup_deployment_execution(&deployment, &user).await?;
setup_deployment_execution(&self.deployment, user).await?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -107,16 +110,11 @@ impl Resolve<Deploy, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.deploying = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
periphery
.health_check()
.await
.context("Failed server health check, stopping run.")?;
// This block resolves the attached Build to an actual versioned image
let (version, registry_token) = match &deployment.config.image {
DeploymentImage::Build { build_id, version } => {
@@ -128,12 +126,7 @@ impl Resolve<Deploy, (User, Update)> for State {
} else {
*version
};
// Remove ending patch if it is 0, this means use latest patch.
let version_str = if version.patch == 0 {
format!("{}.{}", version.major, version.minor)
} else {
version.to_string()
};
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
@@ -241,11 +234,11 @@ impl Resolve<Deploy, (User, Update)> for State {
update.version = version;
update_update(update.clone()).await?;
match periphery
match periphery_client(&server)?
.request(api::container::Deploy {
deployment,
stop_signal,
stop_time,
stop_signal: self.stop_signal,
stop_time: self.stop_time,
registry_token,
replacers: secret_replacers.into_iter().collect(),
})
@@ -254,10 +247,8 @@ impl Resolve<Deploy, (User, Update)> for State {
Ok(log) => update.logs.push(log),
Err(e) => {
update.push_error_log(
"deploy container",
format_serror(
&e.context("failed to deploy container").into(),
),
"Deploy Container",
format_serror(&e.into()),
);
}
};
@@ -271,15 +262,164 @@ impl Resolve<Deploy, (User, Update)> for State {
}
}
impl Resolve<StartDeployment, (User, Update)> for State {
#[instrument(name = "StartDeployment", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
/// Wait this long after a pull to allow another pull through
const PULL_TIMEOUT: i64 = 5_000;
type ServerId = String;
type Image = String;
type PullCache = TimeoutCache<(ServerId, Image), Log>;
fn pull_cache() -> &'static PullCache {
static PULL_CACHE: OnceLock<PullCache> = OnceLock::new();
PULL_CACHE.get_or_init(Default::default)
}
pub async fn pull_deployment_inner(
deployment: Deployment,
server: &Server,
) -> anyhow::Result<Log> {
let (image, account, token) = match deployment.config.image {
DeploymentImage::Build { build_id, version } => {
let build = resource::get::<Build>(&build_id).await?;
let image_name = get_image_name(&build)
.context("failed to create image name")?;
let version = if version.is_none() {
build.config.version.to_string()
} else {
version.to_string()
};
// Potentially add the build image_tag postfix
let version = if build.config.image_tag.is_empty() {
version
} else {
format!("{version}-{}", build.config.image_tag)
};
// replace image with corresponding build image.
let image = format!("{image_name}:{version}");
if build.config.image_registry.domain.is_empty() {
(image, None, None)
} else {
let ImageRegistryConfig {
domain, account, ..
} = build.config.image_registry;
let account =
if deployment.config.image_registry_account.is_empty() {
account
} else {
deployment.config.image_registry_account
};
let token = if !account.is_empty() {
registry_token(&domain, &account).await.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {domain} | {account}"),
)?
} else {
None
};
(image, optional_string(&account), token)
}
}
DeploymentImage::Image { image } => {
let domain = extract_registry_domain(&image)?;
let token = if !deployment
.config
.image_registry_account
.is_empty()
{
registry_token(&domain, &deployment.config.image_registry_account).await.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {domain} | {}", deployment.config.image_registry_account),
)?
} else {
None
};
(
image,
optional_string(&deployment.config.image_registry_account),
token,
)
}
};
// Acquire the pull lock for this image on the server
let lock = pull_cache()
.get_lock((server.id.clone(), image.clone()))
.await;
// Lock the path lock, prevents simultaneous pulls by
// ensuring simultaneous pulls will wait for first to finish
// and checking cached results.
let mut locked = lock.lock().await;
// Early return from cache if lasted pulled with PULL_TIMEOUT
if locked.last_ts + PULL_TIMEOUT > komodo_timestamp() {
return locked.clone_res();
}
let res = async {
let log = match periphery_client(server)?
.request(api::image::PullImage {
name: image,
account,
token,
})
.await
{
Ok(log) => log,
Err(e) => Log::error("Pull image", format_serror(&e.into())),
};
update_cache_for_server(server).await;
anyhow::Ok(log)
}
.await;
// Set the cache with results. Any other calls waiting on the lock will
// then immediately also use this same result.
locked.set(&res, komodo_timestamp());
res
}
impl Resolve<ExecuteArgs> for PullDeployment {
#[instrument(name = "PullDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
StartDeployment { deployment }: StartDeployment,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&deployment, &user).await?;
setup_deployment_execution(&self.deployment, user).await?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
.deployment
.get_or_insert_default(&deployment.id)
.await;
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let _action_guard =
action_state.update(|state| state.pulling = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = pull_deployment_inner(deployment, &server).await?;
update.logs.push(log);
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for StartDeployment {
#[instrument(name = "StartDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&self.deployment, user).await?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -292,12 +432,12 @@ impl Resolve<StartDeployment, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.starting = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
let log = match periphery_client(&server)?
.request(api::container::StartContainer {
name: deployment.name,
})
@@ -319,15 +459,14 @@ impl Resolve<StartDeployment, (User, Update)> for State {
}
}
impl Resolve<RestartDeployment, (User, Update)> for State {
#[instrument(name = "RestartDeployment", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for RestartDeployment {
#[instrument(name = "RestartDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
RestartDeployment { deployment }: RestartDeployment,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&deployment, &user).await?;
setup_deployment_execution(&self.deployment, user).await?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -340,12 +479,12 @@ impl Resolve<RestartDeployment, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.restarting = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
let log = match periphery_client(&server)?
.request(api::container::RestartContainer {
name: deployment.name,
})
@@ -369,15 +508,14 @@ impl Resolve<RestartDeployment, (User, Update)> for State {
}
}
impl Resolve<PauseDeployment, (User, Update)> for State {
#[instrument(name = "PauseDeployment", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for PauseDeployment {
#[instrument(name = "PauseDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PauseDeployment { deployment }: PauseDeployment,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&deployment, &user).await?;
setup_deployment_execution(&self.deployment, user).await?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -390,12 +528,12 @@ impl Resolve<PauseDeployment, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.pausing = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
let log = match periphery_client(&server)?
.request(api::container::PauseContainer {
name: deployment.name,
})
@@ -417,15 +555,14 @@ impl Resolve<PauseDeployment, (User, Update)> for State {
}
}
impl Resolve<UnpauseDeployment, (User, Update)> for State {
#[instrument(name = "UnpauseDeployment", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for UnpauseDeployment {
#[instrument(name = "UnpauseDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
UnpauseDeployment { deployment }: UnpauseDeployment,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&deployment, &user).await?;
setup_deployment_execution(&self.deployment, user).await?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -438,12 +575,12 @@ impl Resolve<UnpauseDeployment, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.unpausing = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
let log = match periphery_client(&server)?
.request(api::container::UnpauseContainer {
name: deployment.name,
})
@@ -467,19 +604,14 @@ impl Resolve<UnpauseDeployment, (User, Update)> for State {
}
}
impl Resolve<StopDeployment, (User, Update)> for State {
#[instrument(name = "StopDeployment", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for StopDeployment {
#[instrument(name = "StopDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
StopDeployment {
deployment,
signal,
time,
}: StopDeployment,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&deployment, &user).await?;
setup_deployment_execution(&self.deployment, user).await?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -492,18 +624,20 @@ impl Resolve<StopDeployment, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.stopping = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
let log = match periphery_client(&server)?
.request(api::container::StopContainer {
name: deployment.name,
signal: signal
signal: self
.signal
.unwrap_or(deployment.config.termination_signal)
.into(),
time: time
time: self
.time
.unwrap_or(deployment.config.termination_timeout)
.into(),
})
@@ -525,19 +659,41 @@ impl Resolve<StopDeployment, (User, Update)> for State {
}
}
impl Resolve<DestroyDeployment, (User, Update)> for State {
#[instrument(name = "DestroyDeployment", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
DestroyDeployment {
impl super::BatchExecute for BatchDestroyDeployment {
type Resource = Deployment;
fn single_request(deployment: String) -> ExecuteRequest {
ExecuteRequest::DestroyDeployment(DestroyDeployment {
deployment,
signal,
time,
}: DestroyDeployment,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
signal: None,
time: None,
})
}
}
impl Resolve<ExecuteArgs> for BatchDestroyDeployment {
#[instrument(name = "BatchDestroyDeployment", skip(user), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDestroyDeployment>(
&self.pattern,
user,
)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for DestroyDeployment {
#[instrument(name = "DestroyDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&deployment, &user).await?;
setup_deployment_execution(&self.deployment, user).await?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -550,18 +706,20 @@ impl Resolve<DestroyDeployment, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.destroying = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
let log = match periphery_client(&server)?
.request(api::container::RemoveContainer {
name: deployment.name,
signal: signal
signal: self
.signal
.unwrap_or(deployment.config.termination_signal)
.into(),
time: time
time: self
.time
.unwrap_or(deployment.config.termination_timeout)
.into(),
})

View File

@@ -1,22 +1,27 @@
use std::time::Instant;
use std::{pin::Pin, time::Instant};
use anyhow::{anyhow, Context};
use axum::{middleware, routing::post, Extension, Router};
use axum_extra::{headers::ContentType, TypedHeader};
use anyhow::Context;
use axum::{
Extension, Router, extract::Path, middleware, routing::post,
};
use axum_extra::{TypedHeader, headers::ContentType};
use derive_variants::{EnumVariants, ExtractVariant};
use formatting::format_serror;
use futures::future::join_all;
use komodo_client::{
api::execute::*,
entities::{
Operation,
permission::PermissionLevel,
update::{Log, Update},
user::User,
Operation,
},
};
use mungos::by_id::find_one_by_id;
use resolver_api::{derive::Resolver, Resolver};
use resolver_api::Resolve;
use response::JsonString;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
@@ -24,27 +29,39 @@ use uuid::Uuid;
use crate::{
auth::auth_request,
helpers::update::{init_execution_update, update_update},
resource::{list_full_for_user_using_pattern, KomodoResource},
state::{db_client, State},
resource::{KomodoResource, list_full_for_user_using_pattern},
state::db_client,
};
mod action;
mod alerter;
mod build;
mod deployment;
mod procedure;
mod repo;
mod server;
mod server_template;
mod stack;
mod sync;
use super::Variant;
pub use {
deployment::pull_deployment_inner, stack::pull_stack_inner,
};
pub struct ExecuteArgs {
pub user: User,
pub update: Update,
}
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolver, EnumVariants,
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
)]
#[variant_derive(Debug)]
#[resolver_target(State)]
#[resolver_args((User, Update))]
#[args(ExecuteArgs)]
#[response(JsonString)]
#[error(serror::Error)]
#[serde(tag = "type", content = "params")]
pub enum ExecuteRequest {
// ==== SERVER ====
@@ -70,21 +87,13 @@ pub enum ExecuteRequest {
PruneBuildx(PruneBuildx),
PruneSystem(PruneSystem),
// ==== DEPLOYMENT ====
Deploy(Deploy),
BatchDeploy(BatchDeploy),
StartDeployment(StartDeployment),
RestartDeployment(RestartDeployment),
PauseDeployment(PauseDeployment),
UnpauseDeployment(UnpauseDeployment),
StopDeployment(StopDeployment),
DestroyDeployment(DestroyDeployment),
// ==== STACK ====
DeployStack(DeployStack),
BatchDeployStack(BatchDeployStack),
DeployStackIfChanged(DeployStackIfChanged),
BatchDeployStackIfChanged(BatchDeployStackIfChanged),
PullStack(PullStack),
BatchPullStack(BatchPullStack),
StartStack(StartStack),
RestartStack(RestartStack),
StopStack(StopStack),
@@ -93,6 +102,18 @@ pub enum ExecuteRequest {
DestroyStack(DestroyStack),
BatchDestroyStack(BatchDestroyStack),
// ==== DEPLOYMENT ====
Deploy(Deploy),
BatchDeploy(BatchDeploy),
PullDeployment(PullDeployment),
StartDeployment(StartDeployment),
RestartDeployment(RestartDeployment),
PauseDeployment(PauseDeployment),
UnpauseDeployment(UnpauseDeployment),
StopDeployment(StopDeployment),
DestroyDeployment(DestroyDeployment),
BatchDestroyDeployment(BatchDestroyDeployment),
// ==== BUILD ====
RunBuild(RunBuild),
BatchRunBuild(BatchRunBuild),
@@ -115,8 +136,8 @@ pub enum ExecuteRequest {
RunAction(RunAction),
BatchRunAction(BatchRunAction),
// ==== SERVER TEMPLATE ====
LaunchServer(LaunchServer),
// ==== ALERTER ====
TestAlerter(TestAlerter),
// ==== SYNC ====
RunSync(RunSync),
@@ -125,9 +146,22 @@ pub enum ExecuteRequest {
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler))
.layer(middleware::from_fn(auth_request))
}
async fn variant_handler(
user: Extension<User>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<(TypedHeader<ContentType>, String)> {
let req: ExecuteRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
}))?;
handler(user, Json(req)).await
}
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<ExecuteRequest>,
@@ -140,70 +174,82 @@ async fn handler(
Ok((TypedHeader(ContentType::json()), res))
}
enum ExecutionResult {
Single(Update),
#[typeshare(serialized_as = "Update")]
type BoxUpdate = Box<Update>;
pub enum ExecutionResult {
Single(BoxUpdate),
/// The batch contents will be pre serialized here
Batch(String),
}
async fn inner_handler(
pub fn inner_handler(
request: ExecuteRequest,
user: User,
) -> anyhow::Result<ExecutionResult> {
let req_id = Uuid::new_v4();
) -> Pin<
Box<
dyn std::future::Future<Output = anyhow::Result<ExecutionResult>>
+ Send,
>,
> {
Box::pin(async move {
let req_id = Uuid::new_v4();
// need to validate no cancel is active before any update is created.
build::validate_cancel_build(&request).await?;
// need to validate no cancel is active before any update is created.
build::validate_cancel_build(&request).await?;
let update = init_execution_update(&request, &user).await?;
let update = init_execution_update(&request, &user).await?;
// This will be the case for the Batch exections,
// they don't have their own updates.
// The batch calls also call "inner_handler" themselves,
// and in their case will spawn tasks, so that isn't necessary
// here either.
if update.operation == Operation::None {
return Ok(ExecutionResult::Batch(
task(req_id, request, user, update).await?,
));
}
let handle =
tokio::spawn(task(req_id, request, user, update.clone()));
tokio::spawn({
let update_id = update.id.clone();
async move {
let log = match handle.await {
Ok(Err(e)) => {
warn!("/execute request {req_id} task error: {e:#}",);
Log::error("task error", format_serror(&e.into()))
}
Err(e) => {
warn!("/execute request {req_id} spawn error: {e:?}",);
Log::error("spawn error", format!("{e:#?}"))
}
_ => return,
};
let res = async {
let mut update =
find_one_by_id(&db_client().updates, &update_id)
.await
.context("failed to query to db")?
.context("no update exists with given id")?;
update.logs.push(log);
update.finalize();
update_update(update).await
}
.await;
if let Err(e) = res {
warn!("failed to update update with task error log | {e:#}");
}
// This will be the case for the Batch exections,
// they don't have their own updates.
// The batch calls also call "inner_handler" themselves,
// and in their case will spawn tasks, so that isn't necessary
// here either.
if update.operation == Operation::None {
return Ok(ExecutionResult::Batch(
task(req_id, request, user, update).await?,
));
}
});
Ok(ExecutionResult::Single(update))
let handle =
tokio::spawn(task(req_id, request, user, update.clone()));
tokio::spawn({
let update_id = update.id.clone();
async move {
let log = match handle.await {
Ok(Err(e)) => {
warn!("/execute request {req_id} task error: {e:#}",);
Log::error("task error", format_serror(&e.into()))
}
Err(e) => {
warn!("/execute request {req_id} spawn error: {e:?}",);
Log::error("spawn error", format!("{e:#?}"))
}
_ => return,
};
let res = async {
let mut update =
find_one_by_id(&db_client().updates, &update_id)
.await
.context("failed to query to db")?
.context("no update exists with given id")?;
update.logs.push(log);
update.finalize();
update_update(update).await
}
.await;
if let Err(e) = res {
warn!(
"failed to update update with task error log | {e:#}"
);
}
}
});
Ok(ExecutionResult::Single(update.into()))
})
}
#[instrument(
@@ -224,15 +270,14 @@ async fn task(
info!("/execute request {req_id} | user: {}", user.username);
let timer = Instant::now();
let res = State
.resolve_request(request, (user, update))
.await
.map_err(|e| match e {
resolver_api::Error::Serialization(e) => {
anyhow!("{e:?}").context("response serialization error")
}
resolver_api::Error::Inner(e) => e,
});
let res = match request.resolve(&ExecuteArgs { user, update }).await
{
Err(e) => Err(e.error),
Ok(JsonString::Err(e)) => Err(
anyhow::Error::from(e).context("failed to serialize response"),
),
Ok(JsonString::Ok(res)) => Ok(res),
};
if let Err(e) = &res {
warn!("/execute request {req_id} error: {e:#}");
@@ -254,9 +299,10 @@ async fn batch_execute<E: BatchExecute>(
user: &User,
) -> anyhow::Result<BatchExecutionResponse> {
let resources = list_full_for_user_using_pattern::<E::Resource>(
&pattern,
pattern,
Default::default(),
&user,
user,
PermissionLevel::Execute.into(),
&[],
)
.await?;

View File

@@ -1,13 +1,17 @@
use std::pin::Pin;
use formatting::{bold, colored, format_serror, muted, Color};
use formatting::{Color, bold, colored, format_serror, muted};
use komodo_client::{
api::execute::{
BatchExecutionResponse, BatchRunProcedure, RunProcedure,
},
entities::{
permission::PermissionLevel, procedure::Procedure,
update::Update, user::User,
alert::{Alert, AlertData, SeverityLevel},
komodo_timestamp,
permission::PermissionLevel,
procedure::Procedure,
update::Update,
user::User,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
@@ -15,12 +19,14 @@ use resolver_api::Resolve;
use tokio::sync::Mutex;
use crate::{
alert::send_alerts,
helpers::{procedure::execute_procedure, update::update_update},
resource::{self, refresh_procedure_state_cache},
state::{action_states, db_client, State},
permission::get_check_permissions,
resource::refresh_procedure_state_cache,
state::{action_states, db_client},
};
use super::ExecuteRequest;
use super::{ExecuteArgs, ExecuteRequest};
impl super::BatchExecute for BatchRunProcedure {
type Resource = Procedure;
@@ -29,25 +35,29 @@ impl super::BatchExecute for BatchRunProcedure {
}
}
impl Resolve<BatchRunProcedure, (User, Update)> for State {
#[instrument(name = "BatchRunProcedure", skip(self, user), fields(user_id = user.id))]
impl Resolve<ExecuteArgs> for BatchRunProcedure {
#[instrument(name = "BatchRunProcedure", skip(user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchRunProcedure { pattern }: BatchRunProcedure,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchRunProcedure>(&pattern, &user).await
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunProcedure>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<RunProcedure, (User, Update)> for State {
#[instrument(name = "RunProcedure", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for RunProcedure {
#[instrument(name = "RunProcedure", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
RunProcedure { procedure }: RunProcedure,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
resolve_inner(procedure, user, update).await
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
Ok(
resolve_inner(self.procedure, user.clone(), update.clone())
.await?,
)
}
}
@@ -61,10 +71,10 @@ fn resolve_inner(
>,
> {
Box::pin(async move {
let procedure = resource::get_check_permissions::<Procedure>(
let procedure = get_check_permissions::<Procedure>(
&procedure,
&user,
PermissionLevel::Execute,
PermissionLevel::Execute.into(),
)
.await?;
@@ -133,6 +143,26 @@ fn resolve_inner(
update_update(update.clone()).await?;
if !update.success && procedure.config.failure_alert {
warn!("procedure unsuccessful, alerting...");
let target = update.target.clone();
tokio::spawn(async move {
let alert = Alert {
id: Default::default(),
target,
ts: komodo_timestamp(),
resolved_ts: Some(komodo_timestamp()),
resolved: true,
level: SeverityLevel::Warning,
data: AlertData::ProcedureFailed {
id: procedure.id,
name: procedure.name,
},
};
send_alerts(&[alert]).await
});
}
Ok(update)
})
}

View File

@@ -1,6 +1,6 @@
use std::{collections::HashSet, future::IntoFuture, time::Duration};
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use formatting::format_serror;
use komodo_client::{
api::{execute::*, write::RefreshRepoCache},
@@ -12,7 +12,6 @@ use komodo_client::{
repo::Repo,
server::Server,
update::{Log, Update},
user::User,
},
};
use mungos::{
@@ -28,6 +27,7 @@ use tokio_util::sync::CancellationToken;
use crate::{
alert::send_alerts,
api::write::WriteArgs,
helpers::{
builder::{cleanup_builder_instance, get_builder_periphery},
channel::repo_cancel_channel,
@@ -41,11 +41,12 @@ use crate::{
query::get_variables_and_secrets,
update::update_update,
},
permission::get_check_permissions,
resource::{self, refresh_repo_state_cache},
state::{action_states, db_client, State},
state::{action_states, db_client},
};
use super::ExecuteRequest;
use super::{ExecuteArgs, ExecuteRequest};
impl super::BatchExecute for BatchCloneRepo {
type Resource = Repo;
@@ -54,28 +55,29 @@ impl super::BatchExecute for BatchCloneRepo {
}
}
impl Resolve<BatchCloneRepo, (User, Update)> for State {
#[instrument(name = "BatchCloneRepo", skip(self, user), fields(user_id = user.id))]
impl Resolve<ExecuteArgs> for BatchCloneRepo {
#[instrument(name = "BatchCloneRepo", skip( user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchCloneRepo { pattern }: BatchCloneRepo,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchCloneRepo>(&pattern, &user).await
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchCloneRepo>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<CloneRepo, (User, Update)> for State {
#[instrument(name = "CloneRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for CloneRepo {
#[instrument(name = "CloneRepo", skip( user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
CloneRepo { repo }: CloneRepo,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let mut repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut repo = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -88,10 +90,11 @@ impl Resolve<CloneRepo, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.cloning = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
if repo.config.server_id.is_empty() {
return Err(anyhow!("repo has no server attached"));
return Err(anyhow!("repo has no server attached").into());
}
let git_token = git_token(
@@ -128,8 +131,8 @@ impl Resolve<CloneRepo, (User, Update)> for State {
Ok(res) => res.logs,
Err(e) => {
vec![Log::error(
"clone repo",
format_serror(&e.context("failed to clone repo").into()),
"Clone Repo",
format_serror(&e.context("Failed to clone repo").into()),
)]
}
};
@@ -141,9 +144,10 @@ impl Resolve<CloneRepo, (User, Update)> for State {
update_last_pulled_time(&repo.name).await;
}
if let Err(e) = State
.resolve(RefreshRepoCache { repo: repo.id }, user)
if let Err(e) = (RefreshRepoCache { repo: repo.id })
.resolve(&WriteArgs { user: user.clone() })
.await
.map_err(|e| e.error)
.context("Failed to refresh repo cache")
{
update.push_error_log(
@@ -159,32 +163,33 @@ impl Resolve<CloneRepo, (User, Update)> for State {
impl super::BatchExecute for BatchPullRepo {
type Resource = Repo;
fn single_request(repo: String) -> ExecuteRequest {
ExecuteRequest::CloneRepo(CloneRepo { repo })
ExecuteRequest::PullRepo(PullRepo { repo })
}
}
impl Resolve<BatchPullRepo, (User, Update)> for State {
#[instrument(name = "BatchPullRepo", skip(self, user), fields(user_id = user.id))]
impl Resolve<ExecuteArgs> for BatchPullRepo {
#[instrument(name = "BatchPullRepo", skip(user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchPullRepo { pattern }: BatchPullRepo,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchPullRepo>(&pattern, &user).await
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchPullRepo>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<PullRepo, (User, Update)> for State {
#[instrument(name = "PullRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for PullRepo {
#[instrument(name = "PullRepo", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PullRepo { repo }: PullRepo,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let mut repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut repo = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -197,10 +202,12 @@ impl Resolve<PullRepo, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.pulling = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
if repo.config.server_id.is_empty() {
return Err(anyhow!("repo has no server attached"));
return Err(anyhow!("repo has no server attached").into());
}
let git_token = git_token(
@@ -254,9 +261,10 @@ impl Resolve<PullRepo, (User, Update)> for State {
update_last_pulled_time(&repo.name).await;
}
if let Err(e) = State
.resolve(RefreshRepoCache { repo: repo.id }, user)
if let Err(e) = (RefreshRepoCache { repo: repo.id })
.resolve(&WriteArgs { user: user.clone() })
.await
.map_err(|e| e.error)
.context("Failed to refresh repo cache")
{
update.push_error_log(
@@ -272,7 +280,7 @@ impl Resolve<PullRepo, (User, Update)> for State {
#[instrument(skip_all, fields(update_id = update.id))]
async fn handle_server_update_return(
update: Update,
) -> anyhow::Result<Update> {
) -> serror::Result<Update> {
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
// The Err case of to_document should be unreachable,
@@ -314,33 +322,34 @@ impl super::BatchExecute for BatchBuildRepo {
}
}
impl Resolve<BatchBuildRepo, (User, Update)> for State {
#[instrument(name = "BatchBuildRepo", skip(self, user), fields(user_id = user.id))]
impl Resolve<ExecuteArgs> for BatchBuildRepo {
#[instrument(name = "BatchBuildRepo", skip(user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchBuildRepo { pattern }: BatchBuildRepo,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchBuildRepo>(&pattern, &user).await
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchBuildRepo>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<BuildRepo, (User, Update)> for State {
#[instrument(name = "BuildRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for BuildRepo {
#[instrument(name = "BuildRepo", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
BuildRepo { repo }: BuildRepo,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let mut repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut repo = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute.into(),
)
.await?;
if repo.config.builder_id.is_empty() {
return Err(anyhow!("Must attach builder to BuildRepo"));
return Err(anyhow!("Must attach builder to BuildRepo").into());
}
// get the action state for the repo (or insert default).
@@ -352,6 +361,7 @@ impl Resolve<BuildRepo, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.building = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let git_token = git_token(
@@ -453,7 +463,7 @@ impl Resolve<BuildRepo, (User, Update)> for State {
_ = cancel.cancelled() => {
debug!("build cancelled during clone, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
cleanup_builder_instance(cleanup_data, &mut update)
.await;
info!("builder cleaned up");
return handle_builder_early_return(update, repo.id, repo.name, true).await
@@ -469,8 +479,8 @@ impl Resolve<BuildRepo, (User, Update)> for State {
}
Err(e) => {
update.push_error_log(
"clone repo",
format_serror(&e.context("failed to clone repo").into()),
"Clone Repo",
format_serror(&e.context("Failed to clone repo").into()),
);
Default::default()
}
@@ -497,8 +507,9 @@ impl Resolve<BuildRepo, (User, Update)> for State {
// stop the cancel listening task from going forever
cancel.cancel();
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// If building on temporary cloud server (AWS),
// this will terminate the server.
cleanup_builder_instance(cleanup_data, &mut update).await;
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
@@ -547,7 +558,7 @@ async fn handle_builder_early_return(
repo_id: String,
repo_name: String,
is_cancel: bool,
) -> anyhow::Result<Update> {
) -> serror::Result<Update> {
update.finalize();
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
@@ -635,17 +646,16 @@ pub async fn validate_cancel_repo_build(
Ok(())
}
impl Resolve<CancelRepoBuild, (User, Update)> for State {
#[instrument(name = "CancelRepoBuild", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for CancelRepoBuild {
#[instrument(name = "CancelRepoBuild", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
CancelRepoBuild { repo }: CancelRepoBuild,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let repo = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -657,9 +667,11 @@ impl Resolve<CancelRepoBuild, (User, Update)> for State {
.and_then(|s| s.get().ok().map(|s| s.building))
.unwrap_or_default()
{
return Err(anyhow!("Repo is not building."));
return Err(anyhow!("Repo is not building.").into());
}
let mut update = update.clone();
update.push_simple_log(
"cancel triggered",
"the repo build cancel has been triggered",
@@ -685,7 +697,9 @@ impl Resolve<CancelRepoBuild, (User, Update)> for State {
)
.await
{
warn!("failed to set CancelRepoBuild Update status Complete after timeout | {e:#}")
warn!(
"failed to set CancelRepoBuild Update status Complete after timeout | {e:#}"
)
}
});

View File

@@ -7,7 +7,6 @@ use komodo_client::{
permission::PermissionLevel,
server::Server,
update::{Log, Update},
user::User,
},
};
use periphery_client::api;
@@ -16,21 +15,22 @@ use resolver_api::Resolve;
use crate::{
helpers::{periphery_client, update::update_update},
monitor::update_cache_for_server,
resource,
state::{action_states, State},
permission::get_check_permissions,
state::action_states,
};
impl Resolve<StartContainer, (User, Update)> for State {
use super::ExecuteArgs;
impl Resolve<ExecuteArgs> for StartContainer {
#[instrument(name = "StartContainer", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
StartContainer { server, container }: StartContainer,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -45,13 +45,17 @@ impl Resolve<StartContainer, (User, Update)> for State {
let _action_guard = action_state
.update(|state| state.starting_containers = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
.request(api::container::StartContainer { name: container })
.request(api::container::StartContainer {
name: self.container,
})
.await
{
Ok(log) => log,
@@ -71,17 +75,16 @@ impl Resolve<StartContainer, (User, Update)> for State {
}
}
impl Resolve<RestartContainer, (User, Update)> for State {
impl Resolve<ExecuteArgs> for RestartContainer {
#[instrument(name = "RestartContainer", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
RestartContainer { server, container }: RestartContainer,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -96,13 +99,17 @@ impl Resolve<RestartContainer, (User, Update)> for State {
let _action_guard = action_state
.update(|state| state.restarting_containers = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
.request(api::container::RestartContainer { name: container })
.request(api::container::RestartContainer {
name: self.container,
})
.await
{
Ok(log) => log,
@@ -124,17 +131,16 @@ impl Resolve<RestartContainer, (User, Update)> for State {
}
}
impl Resolve<PauseContainer, (User, Update)> for State {
#[instrument(name = "PauseContainer", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for PauseContainer {
#[instrument(name = "PauseContainer", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PauseContainer { server, container }: PauseContainer,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -149,13 +155,17 @@ impl Resolve<PauseContainer, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.pausing_containers = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
.request(api::container::PauseContainer { name: container })
.request(api::container::PauseContainer {
name: self.container,
})
.await
{
Ok(log) => log,
@@ -175,17 +185,16 @@ impl Resolve<PauseContainer, (User, Update)> for State {
}
}
impl Resolve<UnpauseContainer, (User, Update)> for State {
#[instrument(name = "UnpauseContainer", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for UnpauseContainer {
#[instrument(name = "UnpauseContainer", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
UnpauseContainer { server, container }: UnpauseContainer,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -200,13 +209,17 @@ impl Resolve<UnpauseContainer, (User, Update)> for State {
let _action_guard = action_state
.update(|state| state.unpausing_containers = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
.request(api::container::UnpauseContainer { name: container })
.request(api::container::UnpauseContainer {
name: self.container,
})
.await
{
Ok(log) => log,
@@ -228,22 +241,16 @@ impl Resolve<UnpauseContainer, (User, Update)> for State {
}
}
impl Resolve<StopContainer, (User, Update)> for State {
#[instrument(name = "StopContainer", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for StopContainer {
#[instrument(name = "StopContainer", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
StopContainer {
server,
container,
signal,
time,
}: StopContainer,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -258,6 +265,8 @@ impl Resolve<StopContainer, (User, Update)> for State {
let _action_guard = action_state
.update(|state| state.stopping_containers = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
@@ -265,9 +274,9 @@ impl Resolve<StopContainer, (User, Update)> for State {
let log = match periphery
.request(api::container::StopContainer {
name: container,
signal,
time,
name: self.container,
signal: self.signal,
time: self.time,
})
.await
{
@@ -288,22 +297,22 @@ impl Resolve<StopContainer, (User, Update)> for State {
}
}
impl Resolve<DestroyContainer, (User, Update)> for State {
#[instrument(name = "DestroyContainer", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for DestroyContainer {
#[instrument(name = "DestroyContainer", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
DestroyContainer {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let DestroyContainer {
server,
container,
signal,
time,
}: DestroyContainer,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
} = self;
let server = get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -318,6 +327,8 @@ impl Resolve<DestroyContainer, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.pruning_containers = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
@@ -348,17 +359,16 @@ impl Resolve<DestroyContainer, (User, Update)> for State {
}
}
impl Resolve<StartAllContainers, (User, Update)> for State {
#[instrument(name = "StartAllContainers", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for StartAllContainers {
#[instrument(name = "StartAllContainers", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
StartAllContainers { server }: StartAllContainers,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -373,6 +383,8 @@ impl Resolve<StartAllContainers, (User, Update)> for State {
let _action_guard = action_state
.update(|state| state.starting_containers = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let logs = periphery_client(&server)?
@@ -397,17 +409,16 @@ impl Resolve<StartAllContainers, (User, Update)> for State {
}
}
impl Resolve<RestartAllContainers, (User, Update)> for State {
#[instrument(name = "RestartAllContainers", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for RestartAllContainers {
#[instrument(name = "RestartAllContainers", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
RestartAllContainers { server }: RestartAllContainers,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -422,6 +433,8 @@ impl Resolve<RestartAllContainers, (User, Update)> for State {
let _action_guard = action_state
.update(|state| state.restarting_containers = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let logs = periphery_client(&server)?
@@ -448,17 +461,16 @@ impl Resolve<RestartAllContainers, (User, Update)> for State {
}
}
impl Resolve<PauseAllContainers, (User, Update)> for State {
#[instrument(name = "PauseAllContainers", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for PauseAllContainers {
#[instrument(name = "PauseAllContainers", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PauseAllContainers { server }: PauseAllContainers,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -473,6 +485,8 @@ impl Resolve<PauseAllContainers, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.pausing_containers = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let logs = periphery_client(&server)?
@@ -497,17 +511,16 @@ impl Resolve<PauseAllContainers, (User, Update)> for State {
}
}
impl Resolve<UnpauseAllContainers, (User, Update)> for State {
#[instrument(name = "UnpauseAllContainers", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for UnpauseAllContainers {
#[instrument(name = "UnpauseAllContainers", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
UnpauseAllContainers { server }: UnpauseAllContainers,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -522,6 +535,8 @@ impl Resolve<UnpauseAllContainers, (User, Update)> for State {
let _action_guard = action_state
.update(|state| state.unpausing_containers = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let logs = periphery_client(&server)?
@@ -548,17 +563,16 @@ impl Resolve<UnpauseAllContainers, (User, Update)> for State {
}
}
impl Resolve<StopAllContainers, (User, Update)> for State {
#[instrument(name = "StopAllContainers", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for StopAllContainers {
#[instrument(name = "StopAllContainers", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
StopAllContainers { server }: StopAllContainers,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -573,6 +587,8 @@ impl Resolve<StopAllContainers, (User, Update)> for State {
let _action_guard = action_state
.update(|state| state.stopping_containers = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let logs = periphery_client(&server)?
@@ -597,17 +613,16 @@ impl Resolve<StopAllContainers, (User, Update)> for State {
}
}
impl Resolve<PruneContainers, (User, Update)> for State {
#[instrument(name = "PruneContainers", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for PruneContainers {
#[instrument(name = "PruneContainers", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PruneContainers { server }: PruneContainers,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -622,6 +637,8 @@ impl Resolve<PruneContainers, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.pruning_containers = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
@@ -652,37 +669,43 @@ impl Resolve<PruneContainers, (User, Update)> for State {
}
}
impl Resolve<DeleteNetwork, (User, Update)> for State {
#[instrument(name = "DeleteNetwork", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for DeleteNetwork {
#[instrument(name = "DeleteNetwork", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
DeleteNetwork { server, name }: DeleteNetwork,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
let mut update = update.clone();
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
.request(api::network::DeleteNetwork { name: name.clone() })
.request(api::network::DeleteNetwork {
name: self.name.clone(),
})
.await
.context(format!(
"failed to delete network {name} on server {}",
server.name
"failed to delete network {} on server {}",
self.name, server.name
)) {
Ok(log) => log,
Err(e) => Log::error(
"delete network",
format_serror(
&e.context(format!("failed to delete network {name}"))
.into(),
&e.context(format!(
"failed to delete network {}",
self.name
))
.into(),
),
),
};
@@ -697,17 +720,16 @@ impl Resolve<DeleteNetwork, (User, Update)> for State {
}
}
impl Resolve<PruneNetworks, (User, Update)> for State {
#[instrument(name = "PruneNetworks", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for PruneNetworks {
#[instrument(name = "PruneNetworks", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PruneNetworks { server }: PruneNetworks,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -722,6 +744,8 @@ impl Resolve<PruneNetworks, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.pruning_networks = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
@@ -750,36 +774,40 @@ impl Resolve<PruneNetworks, (User, Update)> for State {
}
}
impl Resolve<DeleteImage, (User, Update)> for State {
#[instrument(name = "DeleteImage", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for DeleteImage {
#[instrument(name = "DeleteImage", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
DeleteImage { server, name }: DeleteImage,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
let mut update = update.clone();
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
.request(api::image::DeleteImage { name: name.clone() })
.request(api::image::DeleteImage {
name: self.name.clone(),
})
.await
.context(format!(
"failed to delete image {name} on server {}",
server.name
"failed to delete image {} on server {}",
self.name, server.name
)) {
Ok(log) => log,
Err(e) => Log::error(
"delete image",
format_serror(
&e.context(format!("failed to delete image {name}")).into(),
&e.context(format!("failed to delete image {}", self.name))
.into(),
),
),
};
@@ -794,17 +822,16 @@ impl Resolve<DeleteImage, (User, Update)> for State {
}
}
impl Resolve<PruneImages, (User, Update)> for State {
#[instrument(name = "PruneImages", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for PruneImages {
#[instrument(name = "PruneImages", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PruneImages { server }: PruneImages,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -819,6 +846,8 @@ impl Resolve<PruneImages, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.pruning_images = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
@@ -845,37 +874,43 @@ impl Resolve<PruneImages, (User, Update)> for State {
}
}
impl Resolve<DeleteVolume, (User, Update)> for State {
#[instrument(name = "DeleteVolume", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for DeleteVolume {
#[instrument(name = "DeleteVolume", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
DeleteVolume { server, name }: DeleteVolume,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
let mut update = update.clone();
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
let log = match periphery
.request(api::volume::DeleteVolume { name: name.clone() })
.request(api::volume::DeleteVolume {
name: self.name.clone(),
})
.await
.context(format!(
"failed to delete volume {name} on server {}",
server.name
"failed to delete volume {} on server {}",
self.name, server.name
)) {
Ok(log) => log,
Err(e) => Log::error(
"delete volume",
format_serror(
&e.context(format!("failed to delete volume {name}"))
.into(),
&e.context(format!(
"failed to delete volume {}",
self.name
))
.into(),
),
),
};
@@ -890,17 +925,16 @@ impl Resolve<DeleteVolume, (User, Update)> for State {
}
}
impl Resolve<PruneVolumes, (User, Update)> for State {
#[instrument(name = "PruneVolumes", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for PruneVolumes {
#[instrument(name = "PruneVolumes", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PruneVolumes { server }: PruneVolumes,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -915,6 +949,8 @@ impl Resolve<PruneVolumes, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.pruning_volumes = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
@@ -941,17 +977,16 @@ impl Resolve<PruneVolumes, (User, Update)> for State {
}
}
impl Resolve<PruneDockerBuilders, (User, Update)> for State {
#[instrument(name = "PruneDockerBuilders", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for PruneDockerBuilders {
#[instrument(name = "PruneDockerBuilders", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PruneDockerBuilders { server }: PruneDockerBuilders,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -966,6 +1001,8 @@ impl Resolve<PruneDockerBuilders, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.pruning_builders = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
@@ -992,17 +1029,16 @@ impl Resolve<PruneDockerBuilders, (User, Update)> for State {
}
}
impl Resolve<PruneBuildx, (User, Update)> for State {
#[instrument(name = "PruneBuildx", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for PruneBuildx {
#[instrument(name = "PruneBuildx", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PruneBuildx { server }: PruneBuildx,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -1017,6 +1053,8 @@ impl Resolve<PruneBuildx, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.pruning_buildx = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;
@@ -1043,17 +1081,16 @@ impl Resolve<PruneBuildx, (User, Update)> for State {
}
}
impl Resolve<PruneSystem, (User, Update)> for State {
#[instrument(name = "PruneSystem", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for PruneSystem {
#[instrument(name = "PruneSystem", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PruneSystem { server }: PruneSystem,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -1068,6 +1105,8 @@ impl Resolve<PruneSystem, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.pruning_system = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let periphery = periphery_client(&server)?;

View File

@@ -1,148 +0,0 @@
use anyhow::{anyhow, Context};
use formatting::format_serror;
use komodo_client::{
api::{execute::LaunchServer, write::CreateServer},
entities::{
permission::PermissionLevel,
server::PartialServerConfig,
server_template::{ServerTemplate, ServerTemplateConfig},
update::Update,
user::User,
},
};
use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
cloud::{
aws::ec2::launch_ec2_instance, hetzner::launch_hetzner_server,
},
helpers::update::update_update,
resource,
state::{db_client, State},
};
impl Resolve<LaunchServer, (User, Update)> for State {
#[instrument(name = "LaunchServer", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
LaunchServer {
name,
server_template,
}: LaunchServer,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
// validate name isn't already taken by another server
if db_client()
.servers
.find_one(doc! {
"name": &name
})
.await
.context("failed to query db for servers")?
.is_some()
{
return Err(anyhow!("name is already taken"));
}
let template = resource::get_check_permissions::<ServerTemplate>(
&server_template,
&user,
PermissionLevel::Execute,
)
.await?;
update.push_simple_log(
"launching server",
format!("{:#?}", template.config),
);
update_update(update.clone()).await?;
let config = match template.config {
ServerTemplateConfig::Aws(config) => {
let region = config.region.clone();
let use_https = config.use_https;
let port = config.port;
let instance = match launch_ec2_instance(&name, config).await
{
Ok(instance) => instance,
Err(e) => {
update.push_error_log(
"launch server",
format!("failed to launch aws instance\n\n{e:#?}"),
);
update.finalize();
update_update(update.clone()).await?;
return Ok(update);
}
};
update.push_simple_log(
"launch server",
format!(
"successfully launched server {name} on ip {}",
instance.ip
),
);
let protocol = if use_https { "https" } else { "http" };
PartialServerConfig {
address: format!("{protocol}://{}:{port}", instance.ip)
.into(),
region: region.into(),
..Default::default()
}
}
ServerTemplateConfig::Hetzner(config) => {
let datacenter = config.datacenter;
let use_https = config.use_https;
let port = config.port;
let server = match launch_hetzner_server(&name, config).await
{
Ok(server) => server,
Err(e) => {
update.push_error_log(
"launch server",
format!("failed to launch hetzner server\n\n{e:#?}"),
);
update.finalize();
update_update(update.clone()).await?;
return Ok(update);
}
};
update.push_simple_log(
"launch server",
format!(
"successfully launched server {name} on ip {}",
server.ip
),
);
let protocol = if use_https { "https" } else { "http" };
PartialServerConfig {
address: format!("{protocol}://{}:{port}", server.ip)
.into(),
region: datacenter.as_ref().to_string().into(),
..Default::default()
}
}
};
match self.resolve(CreateServer { name, config }, user).await {
Ok(server) => {
update.push_simple_log(
"create server",
format!("created server {} ({})", server.name, server.id),
);
update.other_data = server.id;
}
Err(e) => {
update.push_error_log(
"create server",
format_serror(&e.context("failed to create server").into()),
);
}
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}

View File

@@ -6,9 +6,10 @@ use komodo_client::{
api::{execute::*, write::RefreshStackCache},
entities::{
permission::PermissionLevel,
repo::Repo,
server::Server,
stack::{Stack, StackInfo},
update::Update,
user::User,
update::{Log, Update},
},
};
use mungos::mongodb::bson::{doc, to_document};
@@ -16,6 +17,7 @@ use periphery_client::api::compose::*;
use resolver_api::Resolve;
use crate::{
api::write::WriteArgs,
helpers::{
interpolate::{
add_interp_update_log,
@@ -25,55 +27,66 @@ use crate::{
},
periphery_client,
query::get_variables_and_secrets,
stack_git_token,
update::{add_update_without_send, update_update},
},
monitor::update_cache_for_server,
permission::get_check_permissions,
resource,
stack::{
execute::execute_compose, get_stack_and_server,
services::extract_services_into_res,
},
state::{action_states, db_client, State},
stack::{execute::execute_compose, get_stack_and_server},
state::{action_states, db_client},
};
use super::ExecuteRequest;
use super::{ExecuteArgs, ExecuteRequest};
impl super::BatchExecute for BatchDeployStack {
type Resource = Stack;
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DeployStack(DeployStack {
stack,
services: Vec::new(),
stop_time: None,
})
}
}
impl Resolve<BatchDeployStack, (User, Update)> for State {
#[instrument(name = "BatchDeployStack", skip(self, user), fields(user_id = user.id))]
impl Resolve<ExecuteArgs> for BatchDeployStack {
#[instrument(name = "BatchDeployStack", skip(user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchDeployStack { pattern }: BatchDeployStack,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDeployStack>(&pattern, &user).await
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeployStack>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<DeployStack, (User, Update)> for State {
#[instrument(name = "DeployStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for DeployStack {
#[instrument(name = "DeployStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
DeployStack { stack, stop_time }: DeployStack,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (mut stack, server) = get_stack_and_server(
&stack,
&user,
PermissionLevel::Execute,
&self.stack,
user,
PermissionLevel::Execute.into(),
true,
)
.await?;
let mut repo = if !stack.config.files_on_host
&& !stack.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&stack.config.linked_repo)
.await?
.into()
} else {
None
};
// get the action state for the stack (or insert default).
let action_state =
action_states().stack.get_or_insert_default(&stack.id).await;
@@ -83,15 +96,22 @@ impl Resolve<DeployStack, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.deploying = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let git_token = crate::helpers::git_token(
&stack.config.git_provider,
&stack.config.git_account,
|https| stack.config.git_https = https,
).await.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {}", stack.config.git_provider, stack.config.git_account),
)?;
if !self.services.is_empty() {
update.logs.push(Log::simple(
"Service/s",
format!(
"Execution requested for Stack service/s {}",
self.services.join(", ")
),
))
}
let git_token =
stack_git_token(&mut stack, repo.as_mut()).await?;
let registry_token = crate::helpers::registry_token(
&stack.config.registry_provider,
@@ -108,6 +128,13 @@ impl Resolve<DeployStack, (User, Update)> for State {
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut stack.config.file_contents,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut stack.config.environment,
@@ -136,6 +163,13 @@ impl Resolve<DeployStack, (User, Update)> for State {
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_system_command(
&vars_and_secrets,
&mut stack.config.post_deploy,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(
&mut update,
&global_replacers,
@@ -150,15 +184,18 @@ impl Resolve<DeployStack, (User, Update)> for State {
let ComposeUpResponse {
logs,
deployed,
services,
file_contents,
missing_files,
remote_errors,
compose_config,
commit_hash,
commit_message,
} = periphery_client(&server)?
.request(ComposeUp {
stack: stack.clone(),
service: None,
services: self.services,
repo,
git_token,
registry_token,
replacers: secret_replacers.into_iter().collect(),
@@ -168,24 +205,11 @@ impl Resolve<DeployStack, (User, Update)> for State {
update.logs.extend(logs);
let update_info = async {
let latest_services = if !file_contents.is_empty() {
let mut services = Vec::new();
for contents in &file_contents {
if let Err(e) = extract_services_into_res(
&stack.project_name(true),
&contents.contents,
&mut services,
) {
update.push_error_log(
"extract services",
format_serror(&e.context(format!("Failed to extract stack services for compose file path {}. Things probably won't work correctly", contents.path)).into())
);
}
}
services
} else {
let latest_services = if services.is_empty() {
// maybe better to do something else here for services.
stack.info.latest_services.clone()
} else {
services
};
// This ensures to get the latest project name,
@@ -195,12 +219,14 @@ impl Resolve<DeployStack, (User, Update)> for State {
let (
deployed_services,
deployed_contents,
deployed_config,
deployed_hash,
deployed_message,
) = if deployed {
(
Some(latest_services.clone()),
Some(file_contents.clone()),
compose_config,
commit_hash.clone(),
commit_message.clone(),
)
@@ -208,6 +234,7 @@ impl Resolve<DeployStack, (User, Update)> for State {
(
stack.info.deployed_services,
stack.info.deployed_contents,
stack.info.deployed_config,
stack.info.deployed_hash,
stack.info.deployed_message,
)
@@ -218,6 +245,7 @@ impl Resolve<DeployStack, (User, Update)> for State {
deployed_project_name: project_name.into(),
deployed_services,
deployed_contents,
deployed_config,
deployed_hash,
deployed_message,
latest_services,
@@ -279,38 +307,39 @@ impl super::BatchExecute for BatchDeployStackIfChanged {
}
}
impl Resolve<BatchDeployStackIfChanged, (User, Update)> for State {
#[instrument(name = "BatchDeployStackIfChanged", skip(self, user), fields(user_id = user.id))]
impl Resolve<ExecuteArgs> for BatchDeployStackIfChanged {
#[instrument(name = "BatchDeployStackIfChanged", skip(user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchDeployStackIfChanged { pattern }: BatchDeployStackIfChanged,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDeployStackIfChanged>(&pattern, &user)
.await
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeployStackIfChanged>(
&self.pattern,
user,
)
.await?,
)
}
}
impl Resolve<DeployStackIfChanged, (User, Update)> for State {
impl Resolve<ExecuteArgs> for DeployStackIfChanged {
#[instrument(name = "DeployStackIfChanged", skip(user, update), fields(user_id = user.id))]
async fn resolve(
&self,
DeployStackIfChanged { stack, stop_time }: DeployStackIfChanged,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let stack = resource::get_check_permissions::<Stack>(
&stack,
&user,
PermissionLevel::Execute,
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let stack = get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Execute.into(),
)
.await?;
State
.resolve(
RefreshStackCache {
stack: stack.id.clone(),
},
user.clone(),
)
.await?;
RefreshStackCache {
stack: stack.id.clone(),
}
.resolve(&WriteArgs { user: user.clone() })
.await?;
let stack = resource::get::<Stack>(&stack.id).await?;
let changed = match (
&stack.info.deployed_contents,
@@ -337,6 +366,8 @@ impl Resolve<DeployStackIfChanged, (User, Update)> for State {
_ => false,
};
let mut update = update.clone();
if !changed {
update.push_simple_log(
"Diff compose files",
@@ -350,116 +381,263 @@ impl Resolve<DeployStackIfChanged, (User, Update)> for State {
// This is usually done in crate::helpers::update::init_execution_update.
update.id = add_update_without_send(&update).await?;
State
.resolve(
DeployStack {
stack: stack.name,
stop_time,
},
(user, update),
)
.await
}
}
impl Resolve<StartStack, (User, Update)> for State {
#[instrument(name = "StartStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
StartStack { stack, service }: StartStack,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
execute_compose::<StartStack>(
&stack,
service,
&user,
|state| state.starting = true,
DeployStack {
stack: stack.name,
services: Vec::new(),
stop_time: self.stop_time,
}
.resolve(&ExecuteArgs {
user: user.clone(),
update,
(),
)
})
.await
}
}
impl Resolve<RestartStack, (User, Update)> for State {
#[instrument(name = "RestartStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl super::BatchExecute for BatchPullStack {
type Resource = Stack;
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::PullStack(PullStack {
stack,
services: Vec::new(),
})
}
}
impl Resolve<ExecuteArgs> for BatchPullStack {
#[instrument(name = "BatchPullStack", skip(user), fields(user_id = user.id))]
async fn resolve(
&self,
RestartStack { stack, service }: RestartStack,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchPullStack>(&self.pattern, user)
.await?,
)
}
}
pub async fn pull_stack_inner(
mut stack: Stack,
services: Vec<String>,
server: &Server,
mut repo: Option<Repo>,
mut update: Option<&mut Update>,
) -> anyhow::Result<ComposePullResponse> {
if let Some(update) = update.as_mut() {
if !services.is_empty() {
update.logs.push(Log::simple(
"Service/s",
format!(
"Execution requested for Stack service/s {}",
services.join(", ")
),
))
}
}
let git_token = stack_git_token(&mut stack, repo.as_mut()).await?;
let registry_token = crate::helpers::registry_token(
&stack.config.registry_provider,
&stack.config.registry_account,
).await.with_context(
|| format!("Failed to get registry token in call to db. Stopping run. | {} | {}", stack.config.registry_provider, stack.config.registry_account),
)?;
// interpolate variables / secrets
if !stack.config.skip_secret_interp {
let vars_and_secrets = get_variables_and_secrets().await?;
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut stack.config.file_contents,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut stack.config.environment,
&mut global_replacers,
&mut secret_replacers,
)?;
if let Some(update) = update {
add_interp_update_log(
update,
&global_replacers,
&secret_replacers,
);
}
};
let res = periphery_client(server)?
.request(ComposePull {
stack,
services,
repo,
git_token,
registry_token,
})
.await?;
// Ensure cached stack state up to date by updating server cache
update_cache_for_server(server).await;
Ok(res)
}
impl Resolve<ExecuteArgs> for PullStack {
#[instrument(name = "PullStack", skip(user, update), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (stack, server) = get_stack_and_server(
&self.stack,
user,
PermissionLevel::Execute.into(),
true,
)
.await?;
let repo = if !stack.config.files_on_host
&& !stack.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&stack.config.linked_repo)
.await?
.into()
} else {
None
};
// get the action state for the stack (or insert default).
let action_state =
action_states().stack.get_or_insert_default(&stack.id).await;
// Will check to ensure stack not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let _action_guard =
action_state.update(|state| state.pulling = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let res = pull_stack_inner(
stack,
self.services,
&server,
repo,
Some(&mut update),
)
.await?;
update.logs.extend(res.logs);
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for StartStack {
#[instrument(name = "StartStack", skip(user, update), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<StartStack>(
&self.stack,
self.services,
user,
|state| state.starting = true,
update.clone(),
(),
)
.await
.map_err(Into::into)
}
}
impl Resolve<ExecuteArgs> for RestartStack {
#[instrument(name = "RestartStack", skip(user, update), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<RestartStack>(
&stack,
service,
&user,
&self.stack,
self.services,
user,
|state| {
state.restarting = true;
},
update,
update.clone(),
(),
)
.await
.map_err(Into::into)
}
}
impl Resolve<PauseStack, (User, Update)> for State {
#[instrument(name = "PauseStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for PauseStack {
#[instrument(name = "PauseStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PauseStack { stack, service }: PauseStack,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<PauseStack>(
&stack,
service,
&user,
&self.stack,
self.services,
user,
|state| state.pausing = true,
update,
update.clone(),
(),
)
.await
.map_err(Into::into)
}
}
impl Resolve<UnpauseStack, (User, Update)> for State {
#[instrument(name = "UnpauseStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for UnpauseStack {
#[instrument(name = "UnpauseStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
UnpauseStack { stack, service }: UnpauseStack,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<UnpauseStack>(
&stack,
service,
&user,
&self.stack,
self.services,
user,
|state| state.unpausing = true,
update,
update.clone(),
(),
)
.await
.map_err(Into::into)
}
}
impl Resolve<StopStack, (User, Update)> for State {
#[instrument(name = "StopStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for StopStack {
#[instrument(name = "StopStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
StopStack {
stack,
stop_time,
service,
}: StopStack,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<StopStack>(
&stack,
service,
&user,
&self.stack,
self.services,
user,
|state| state.stopping = true,
update,
stop_time,
update.clone(),
self.stop_time,
)
.await
.map_err(Into::into)
}
}
@@ -468,42 +646,40 @@ impl super::BatchExecute for BatchDestroyStack {
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DestroyStack(DestroyStack {
stack,
services: Vec::new(),
remove_orphans: false,
stop_time: None,
})
}
}
impl Resolve<BatchDestroyStack, (User, Update)> for State {
#[instrument(name = "BatchDestroyStack", skip(self, user), fields(user_id = user.id))]
impl Resolve<ExecuteArgs> for BatchDestroyStack {
#[instrument(name = "BatchDestroyStack", skip(user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchDestroyStack { pattern }: BatchDestroyStack,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDestroyStack>(&pattern, &user).await
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDestroyStack>(&self.pattern, user)
.await
.map_err(Into::into)
}
}
impl Resolve<DestroyStack, (User, Update)> for State {
#[instrument(name = "DestroyStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for DestroyStack {
#[instrument(name = "DestroyStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
DestroyStack {
stack,
remove_orphans,
stop_time,
}: DestroyStack,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<DestroyStack>(
&stack,
None,
&user,
&self.stack,
self.services,
user,
|state| state.destroying = true,
update,
(stop_time, remove_orphans),
update.clone(),
(self.stop_time, self.remove_orphans),
)
.await
.map_err(Into::into)
}
}

View File

@@ -1,11 +1,11 @@
use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, Context};
use formatting::{colored, format_serror, Color};
use anyhow::{Context, anyhow};
use formatting::{Color, colored, format_serror};
use komodo_client::{
api::{execute::RunSync, write::RefreshResourceSyncPending},
entities::{
self,
self, ResourceTargetVariant,
action::Action,
alerter::Alerter,
build::Build,
@@ -16,51 +16,64 @@ use komodo_client::{
procedure::Procedure,
repo::Repo,
server::Server,
server_template::ServerTemplate,
stack::Stack,
sync::ResourceSync,
update::{Log, Update},
user::{sync_user, User},
ResourceTargetVariant,
user::sync_user,
},
};
use mongo_indexed::doc;
use mungos::{
by_id::update_one_by_id,
mongodb::bson::{oid::ObjectId, to_document},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::oid::ObjectId};
use resolver_api::Resolve;
use crate::{
helpers::{query::get_id_to_tags, update::update_update},
resource::{self, refresh_resource_sync_state_cache},
state::{action_states, db_client, State},
api::write::WriteArgs,
helpers::{
all_resources::AllResourcesById, query::get_id_to_tags,
update::update_update,
},
permission::get_check_permissions,
state::{action_states, db_client},
sync::{
ResourceSyncTrait,
deploy::{
build_deploy_cache, deploy_from_cache, SyncDeployParams,
SyncDeployParams, build_deploy_cache, deploy_from_cache,
},
execute::{get_updates_for_execution, ExecuteResourceSync},
execute::{ExecuteResourceSync, get_updates_for_execution},
remote::RemoteResources,
AllResourcesById, ResourceSyncTrait,
},
};
impl Resolve<RunSync, (User, Update)> for State {
#[instrument(name = "RunSync", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
use super::ExecuteArgs;
impl Resolve<ExecuteArgs> for RunSync {
#[instrument(name = "RunSync", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
RunSync {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let RunSync {
sync,
resource_type: match_resource_type,
resources: match_resources,
}: RunSync,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let sync = resource::get_check_permissions::<
entities::sync::ResourceSync,
>(&sync, &user, PermissionLevel::Execute)
} = self;
let sync = get_check_permissions::<entities::sync::ResourceSync>(
&sync,
user,
PermissionLevel::Execute.into(),
)
.await?;
let repo = if !sync.config.files_on_host
&& !sync.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&sync.config.linked_repo)
.await?
.into()
} else {
None
};
// get the action state for the sync (or insert default).
let action_state = action_states()
.resource_sync
@@ -72,6 +85,8 @@ impl Resolve<RunSync, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.syncing = true)?;
let mut update = update.clone();
// Send update here for FE to recheck action state
update_update(update.clone()).await?;
@@ -82,15 +97,18 @@ impl Resolve<RunSync, (User, Update)> for State {
message,
file_errors,
..
} = crate::sync::remote::get_remote_resources(&sync)
.await
.context("failed to get remote resources")?;
} =
crate::sync::remote::get_remote_resources(&sync, repo.as_ref())
.await
.context("failed to get remote resources")?;
update.logs.extend(logs);
update_update(update.clone()).await?;
if !file_errors.is_empty() {
return Err(anyhow!("Found file errors. Cannot execute sync."));
return Err(
anyhow!("Found file errors. Cannot execute sync.").into(),
);
}
let resources = resources?;
@@ -139,10 +157,6 @@ impl Resolve<RunSync, (User, Update)> for State {
.servers
.get(&name_or_id)
.map(|s| s.name.clone()),
ResourceTargetVariant::ServerTemplate => all_resources
.templates
.get(&name_or_id)
.map(|t| t.name.clone()),
ResourceTargetVariant::Stack => all_resources
.stacks
.get(&name_or_id)
@@ -197,145 +211,141 @@ impl Resolve<RunSync, (User, Update)> for State {
deployment_map: &deployments_by_name,
stacks: &resources.stacks,
stack_map: &stacks_by_name,
all_resources: &all_resources,
})
.await?;
let delete = sync.config.managed || sync.config.delete;
let (servers_to_create, servers_to_update, servers_to_delete) =
let server_deltas = if sync.config.include_resources {
get_updates_for_execution::<Server>(
resources.servers,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (
deployments_to_create,
deployments_to_update,
deployments_to_delete,
) = get_updates_for_execution::<Deployment>(
resources.deployments,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (stacks_to_create, stacks_to_update, stacks_to_delete) =
.await?
} else {
Default::default()
};
let stack_deltas = if sync.config.include_resources {
get_updates_for_execution::<Stack>(
resources.stacks,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (builds_to_create, builds_to_update, builds_to_delete) =
.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,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (repos_to_create, repos_to_update, repos_to_delete) =
.await?
} else {
Default::default()
};
let repo_deltas = if sync.config.include_resources {
get_updates_for_execution::<Repo>(
resources.repos,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (
procedures_to_create,
procedures_to_update,
procedures_to_delete,
) = get_updates_for_execution::<Procedure>(
resources.procedures,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (actions_to_create, actions_to_update, actions_to_delete) =
.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,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (builders_to_create, builders_to_update, builders_to_delete) =
.await?
} else {
Default::default()
};
let builder_deltas = if sync.config.include_resources {
get_updates_for_execution::<Builder>(
resources.builders,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (alerters_to_create, alerters_to_update, alerters_to_delete) =
.await?
} else {
Default::default()
};
let alerter_deltas = if sync.config.include_resources {
get_updates_for_execution::<Alerter>(
resources.alerters,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (
server_templates_to_create,
server_templates_to_update,
server_templates_to_delete,
) = get_updates_for_execution::<ServerTemplate>(
resources.server_templates,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (
resource_syncs_to_create,
resource_syncs_to_update,
resource_syncs_to_delete,
) = get_updates_for_execution::<entities::sync::ResourceSync>(
resources.resource_syncs,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
.await?
} else {
Default::default()
};
let resource_sync_deltas = if sync.config.include_resources {
get_updates_for_execution::<entities::sync::ResourceSync>(
resources.resource_syncs,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let (
variables_to_create,
@@ -343,12 +353,11 @@ impl Resolve<RunSync, (User, Update)> for State {
variables_to_delete,
) = if match_resource_type.is_none()
&& match_resources.is_none()
&& sync.config.match_tags.is_empty()
&& sync.config.include_variables
{
crate::sync::variables::get_updates_for_execution(
resources.variables,
// Delete doesn't work with variables when match tags are set
sync.config.match_tags.is_empty() && delete,
delete,
)
.await?
} else {
@@ -360,13 +369,11 @@ impl Resolve<RunSync, (User, Update)> for State {
user_groups_to_delete,
) = if match_resource_type.is_none()
&& match_resources.is_none()
&& sync.config.match_tags.is_empty()
&& sync.config.include_user_groups
{
crate::sync::user_groups::get_updates_for_execution(
resources.user_groups,
// Delete doesn't work with user groups when match tags are set
sync.config.match_tags.is_empty() && delete,
&all_resources,
delete,
)
.await?
} else {
@@ -374,39 +381,16 @@ impl Resolve<RunSync, (User, Update)> for State {
};
if deploy_cache.is_empty()
&& resource_syncs_to_create.is_empty()
&& resource_syncs_to_update.is_empty()
&& resource_syncs_to_delete.is_empty()
&& server_templates_to_create.is_empty()
&& server_templates_to_update.is_empty()
&& server_templates_to_delete.is_empty()
&& servers_to_create.is_empty()
&& servers_to_update.is_empty()
&& servers_to_delete.is_empty()
&& deployments_to_create.is_empty()
&& deployments_to_update.is_empty()
&& deployments_to_delete.is_empty()
&& stacks_to_create.is_empty()
&& stacks_to_update.is_empty()
&& stacks_to_delete.is_empty()
&& builds_to_create.is_empty()
&& builds_to_update.is_empty()
&& builds_to_delete.is_empty()
&& builders_to_create.is_empty()
&& builders_to_update.is_empty()
&& builders_to_delete.is_empty()
&& alerters_to_create.is_empty()
&& alerters_to_update.is_empty()
&& alerters_to_delete.is_empty()
&& repos_to_create.is_empty()
&& repos_to_update.is_empty()
&& repos_to_delete.is_empty()
&& procedures_to_create.is_empty()
&& procedures_to_update.is_empty()
&& procedures_to_delete.is_empty()
&& actions_to_create.is_empty()
&& actions_to_update.is_empty()
&& actions_to_delete.is_empty()
&& resource_sync_deltas.no_changes()
&& server_deltas.no_changes()
&& deployment_deltas.no_changes()
&& stack_deltas.no_changes()
&& build_deltas.no_changes()
&& builder_deltas.no_changes()
&& alerter_deltas.no_changes()
&& repo_deltas.no_changes()
&& procedure_deltas.no_changes()
&& action_deltas.no_changes()
&& user_groups_to_create.is_empty()
&& user_groups_to_update.is_empty()
&& user_groups_to_delete.is_empty()
@@ -449,111 +433,52 @@ impl Resolve<RunSync, (User, Update)> for State {
);
maybe_extend(
&mut update.logs,
ResourceSync::execute_sync_updates(
resource_syncs_to_create,
resource_syncs_to_update,
resource_syncs_to_delete,
)
.await,
ResourceSync::execute_sync_updates(resource_sync_deltas).await,
);
maybe_extend(
&mut update.logs,
ServerTemplate::execute_sync_updates(
server_templates_to_create,
server_templates_to_update,
server_templates_to_delete,
)
.await,
Server::execute_sync_updates(server_deltas).await,
);
maybe_extend(
&mut update.logs,
Server::execute_sync_updates(
servers_to_create,
servers_to_update,
servers_to_delete,
)
.await,
Alerter::execute_sync_updates(alerter_deltas).await,
);
maybe_extend(
&mut update.logs,
Alerter::execute_sync_updates(
alerters_to_create,
alerters_to_update,
alerters_to_delete,
)
.await,
);
maybe_extend(
&mut update.logs,
Action::execute_sync_updates(
actions_to_create,
actions_to_update,
actions_to_delete,
)
.await,
Action::execute_sync_updates(action_deltas).await,
);
// Dependent on server
maybe_extend(
&mut update.logs,
Builder::execute_sync_updates(
builders_to_create,
builders_to_update,
builders_to_delete,
)
.await,
Builder::execute_sync_updates(builder_deltas).await,
);
maybe_extend(
&mut update.logs,
Repo::execute_sync_updates(
repos_to_create,
repos_to_update,
repos_to_delete,
)
.await,
Repo::execute_sync_updates(repo_deltas).await,
);
// Dependant on builder
maybe_extend(
&mut update.logs,
Build::execute_sync_updates(
builds_to_create,
builds_to_update,
builds_to_delete,
)
.await,
Build::execute_sync_updates(build_deltas).await,
);
// Dependant on server / build
maybe_extend(
&mut update.logs,
Deployment::execute_sync_updates(
deployments_to_create,
deployments_to_update,
deployments_to_delete,
)
.await,
Deployment::execute_sync_updates(deployment_deltas).await,
);
// stack only depends on server, but maybe will depend on build later.
maybe_extend(
&mut update.logs,
Stack::execute_sync_updates(
stacks_to_create,
stacks_to_update,
stacks_to_delete,
)
.await,
Stack::execute_sync_updates(stack_deltas).await,
);
// Dependant on everything
maybe_extend(
&mut update.logs,
Procedure::execute_sync_updates(
procedures_to_create,
procedures_to_update,
procedures_to_delete,
)
.await,
Procedure::execute_sync_updates(procedure_deltas).await,
);
// Execute the deploy cache
@@ -581,39 +506,27 @@ impl Resolve<RunSync, (User, Update)> for State {
)
}
if let Err(e) = State
.resolve(
RefreshResourceSyncPending { sync: sync.id },
sync_user().to_owned(),
)
if let Err(e) = (RefreshResourceSyncPending { sync: sync.id })
.resolve(&WriteArgs {
user: sync_user().to_owned(),
})
.await
{
warn!("failed to refresh sync {} after run | {e:#}", sync.name);
warn!(
"failed to refresh sync {} after run | {:#}",
sync.name, e.error
);
update.push_error_log(
"refresh sync",
format_serror(
&e.context("failed to refresh sync pending after run")
&e.error
.context("failed to refresh sync pending after run")
.into(),
),
);
}
update.finalize();
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
// The Err case of to_document should be unreachable,
// but will fail to update cache in that case.
if let Ok(update_doc) = to_document(&update) {
let _ = update_one_by_id(
&db.updates,
&update.id,
mungos::update::Update::Set(update_doc),
None,
)
.await;
refresh_resource_sync_state_cache().await;
}
update_update(update.clone()).await?;
Ok(update)

View File

@@ -1,5 +1,11 @@
pub mod auth;
pub mod execute;
pub mod read;
pub mod terminal;
pub mod user;
pub mod write;
#[derive(serde::Deserialize)]
struct Variant {
variant: String,
}

View File

@@ -6,73 +6,88 @@ use komodo_client::{
Action, ActionActionState, ActionListItem, ActionState,
},
permission::PermissionLevel,
user::User,
},
};
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{action_state_cache, action_states, State},
state::{action_state_cache, action_states},
};
impl Resolve<GetAction, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetAction {
async fn resolve(
&self,
GetAction { action }: GetAction,
user: User,
) -> anyhow::Result<Action> {
resource::get_check_permissions::<Action>(
&action,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Action> {
Ok(
get_check_permissions::<Action>(
&self.action,
user,
PermissionLevel::Read.into(),
)
.await?,
)
.await
}
}
impl Resolve<ListActions, User> for State {
impl Resolve<ReadArgs> for ListActions {
async fn resolve(
&self,
ListActions { query }: ListActions,
user: User,
) -> anyhow::Result<Vec<ActionListItem>> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<ActionListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Action>(query, &user, &all_tags).await
Ok(
resource::list_for_user::<Action>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ListFullActions, User> for State {
impl Resolve<ReadArgs> for ListFullActions {
async fn resolve(
&self,
ListFullActions { query }: ListFullActions,
user: User,
) -> anyhow::Result<ListFullActionsResponse> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullActionsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Action>(query, &user, &all_tags)
.await
Ok(
resource::list_full_for_user::<Action>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<GetActionActionState, User> for State {
impl Resolve<ReadArgs> for GetActionActionState {
async fn resolve(
&self,
GetActionActionState { action }: GetActionActionState,
user: User,
) -> anyhow::Result<ActionActionState> {
let action = resource::get_check_permissions::<Action>(
&action,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ActionActionState> {
let action = get_check_permissions::<Action>(
&self.action,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
@@ -85,15 +100,15 @@ impl Resolve<GetActionActionState, User> for State {
}
}
impl Resolve<GetActionsSummary, User> for State {
impl Resolve<ReadArgs> for GetActionsSummary {
async fn resolve(
&self,
GetActionsSummary {}: GetActionsSummary,
user: User,
) -> anyhow::Result<GetActionsSummaryResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetActionsSummaryResponse> {
let actions = resource::list_full_for_user::<Action>(
Default::default(),
&user,
user,
PermissionLevel::Read.into(),
&[],
)
.await

View File

@@ -5,7 +5,7 @@ use komodo_client::{
},
entities::{
deployment::Deployment, server::Server, stack::Stack,
sync::ResourceSync, user::User,
sync::ResourceSync,
},
};
use mungos::{
@@ -16,29 +16,29 @@ use mungos::{
use resolver_api::Resolve;
use crate::{
config::core_config,
resource::get_resource_ids_for_user,
state::{db_client, State},
config::core_config, permission::get_resource_ids_for_user,
state::db_client,
};
use super::ReadArgs;
const NUM_ALERTS_PER_PAGE: u64 = 100;
impl Resolve<ListAlerts, User> for State {
impl Resolve<ReadArgs> for ListAlerts {
async fn resolve(
&self,
ListAlerts { query, page }: ListAlerts,
user: User,
) -> anyhow::Result<ListAlertsResponse> {
let mut query = query.unwrap_or_default();
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListAlertsResponse> {
let mut query = self.query.unwrap_or_default();
if !user.admin && !core_config().transparent_mode {
let server_ids =
get_resource_ids_for_user::<Server>(&user).await?;
get_resource_ids_for_user::<Server>(user).await?;
let stack_ids =
get_resource_ids_for_user::<Stack>(&user).await?;
get_resource_ids_for_user::<Stack>(user).await?;
let deployment_ids =
get_resource_ids_for_user::<Deployment>(&user).await?;
get_resource_ids_for_user::<Deployment>(user).await?;
let sync_ids =
get_resource_ids_for_user::<ResourceSync>(&user).await?;
get_resource_ids_for_user::<ResourceSync>(user).await?;
query.extend(doc! {
"$or": [
{ "target.type": "Server", "target.id": { "$in": &server_ids } },
@@ -55,7 +55,7 @@ impl Resolve<ListAlerts, User> for State {
FindOptions::builder()
.sort(doc! { "ts": -1 })
.limit(NUM_ALERTS_PER_PAGE as i64)
.skip(page * NUM_ALERTS_PER_PAGE)
.skip(self.page * NUM_ALERTS_PER_PAGE)
.build(),
)
.await
@@ -64,7 +64,7 @@ impl Resolve<ListAlerts, User> for State {
let next_page = if alerts.len() < NUM_ALERTS_PER_PAGE as usize {
None
} else {
Some((page + 1) as i64)
Some((self.page + 1) as i64)
};
let res = ListAlertsResponse { next_page, alerts };
@@ -73,15 +73,16 @@ impl Resolve<ListAlerts, User> for State {
}
}
impl Resolve<GetAlert, User> for State {
impl Resolve<ReadArgs> for GetAlert {
async fn resolve(
&self,
GetAlert { id }: GetAlert,
_: User,
) -> anyhow::Result<GetAlertResponse> {
find_one_by_id(&db_client().alerts, &id)
.await
.context("failed to query db for alert")?
.context("no alert found with given id")
self,
_: &ReadArgs,
) -> serror::Result<GetAlertResponse> {
Ok(
find_one_by_id(&db_client().alerts, &self.id)
.await
.context("failed to query db for alert")?
.context("no alert found with given id")?,
)
}
}

View File

@@ -4,7 +4,6 @@ use komodo_client::{
entities::{
alerter::{Alerter, AlerterListItem},
permission::PermissionLevel,
user::User,
},
};
use mongo_indexed::Document;
@@ -12,66 +11,80 @@ use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags,
resource,
state::{db_client, State},
helpers::query::get_all_tags, permission::get_check_permissions,
resource, state::db_client,
};
impl Resolve<GetAlerter, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetAlerter {
async fn resolve(
&self,
GetAlerter { alerter }: GetAlerter,
user: User,
) -> anyhow::Result<Alerter> {
resource::get_check_permissions::<Alerter>(
&alerter,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Alerter> {
Ok(
get_check_permissions::<Alerter>(
&self.alerter,
user,
PermissionLevel::Read.into(),
)
.await?,
)
.await
}
}
impl Resolve<ListAlerters, User> for State {
impl Resolve<ReadArgs> for ListAlerters {
async fn resolve(
&self,
ListAlerters { query }: ListAlerters,
user: User,
) -> anyhow::Result<Vec<AlerterListItem>> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<AlerterListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Alerter>(query, &user, &all_tags).await
Ok(
resource::list_for_user::<Alerter>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ListFullAlerters, User> for State {
impl Resolve<ReadArgs> for ListFullAlerters {
async fn resolve(
&self,
ListFullAlerters { query }: ListFullAlerters,
user: User,
) -> anyhow::Result<ListFullAlertersResponse> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullAlertersResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Alerter>(query, &user, &all_tags)
.await
Ok(
resource::list_full_for_user::<Alerter>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<GetAlertersSummary, User> for State {
impl Resolve<ReadArgs> for GetAlertersSummary {
async fn resolve(
&self,
GetAlertersSummary {}: GetAlertersSummary,
user: User,
) -> anyhow::Result<GetAlertersSummaryResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetAlertersSummaryResponse> {
let query = match resource::get_resource_object_ids_for_user::<
Alerter,
>(&user)
>(user)
.await?
{
Some(ids) => doc! {

View File

@@ -6,12 +6,11 @@ use futures::TryStreamExt;
use komodo_client::{
api::read::*,
entities::{
Operation,
build::{Build, BuildActionState, BuildListItem, BuildState},
config::core::CoreConfig,
permission::PermissionLevel,
update::UpdateStatus,
user::User,
Operation,
},
};
use mungos::{
@@ -23,68 +22,84 @@ use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{
action_states, build_state_cache, db_client, github_client, State,
action_states, build_state_cache, db_client, github_client,
},
};
impl Resolve<GetBuild, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetBuild {
async fn resolve(
&self,
GetBuild { build }: GetBuild,
user: User,
) -> anyhow::Result<Build> {
resource::get_check_permissions::<Build>(
&build,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Build> {
Ok(
get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Read.into(),
)
.await?,
)
.await
}
}
impl Resolve<ListBuilds, User> for State {
impl Resolve<ReadArgs> for ListBuilds {
async fn resolve(
&self,
ListBuilds { query }: ListBuilds,
user: User,
) -> anyhow::Result<Vec<BuildListItem>> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<BuildListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Build>(query, &user, &all_tags).await
Ok(
resource::list_for_user::<Build>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ListFullBuilds, User> for State {
impl Resolve<ReadArgs> for ListFullBuilds {
async fn resolve(
&self,
ListFullBuilds { query }: ListFullBuilds,
user: User,
) -> anyhow::Result<ListFullBuildsResponse> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullBuildsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Build>(query, &user, &all_tags)
.await
Ok(
resource::list_full_for_user::<Build>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<GetBuildActionState, User> for State {
impl Resolve<ReadArgs> for GetBuildActionState {
async fn resolve(
&self,
GetBuildActionState { build }: GetBuildActionState,
user: User,
) -> anyhow::Result<BuildActionState> {
let build = resource::get_check_permissions::<Build>(
&build,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<BuildActionState> {
let build = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
@@ -97,15 +112,15 @@ impl Resolve<GetBuildActionState, User> for State {
}
}
impl Resolve<GetBuildsSummary, User> for State {
impl Resolve<ReadArgs> for GetBuildsSummary {
async fn resolve(
&self,
GetBuildsSummary {}: GetBuildsSummary,
user: User,
) -> anyhow::Result<GetBuildsSummaryResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetBuildsSummaryResponse> {
let builds = resource::list_full_for_user::<Build>(
Default::default(),
&user,
user,
PermissionLevel::Read.into(),
&[],
)
.await
@@ -145,16 +160,15 @@ impl Resolve<GetBuildsSummary, User> for State {
const ONE_DAY_MS: i64 = 86400000;
impl Resolve<GetBuildMonthlyStats, User> for State {
impl Resolve<ReadArgs> for GetBuildMonthlyStats {
async fn resolve(
&self,
GetBuildMonthlyStats { page }: GetBuildMonthlyStats,
_: User,
) -> anyhow::Result<GetBuildMonthlyStatsResponse> {
self,
_: &ReadArgs,
) -> serror::Result<GetBuildMonthlyStatsResponse> {
let curr_ts = unix_timestamp_ms() as i64;
let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS;
let close_ts = next_day - page as i64 * 30 * ONE_DAY_MS;
let close_ts = next_day - self.page as i64 * 30 * ONE_DAY_MS;
let open_ts = close_ts - 30 * ONE_DAY_MS;
let mut build_updates = db_client()
@@ -202,22 +216,22 @@ fn ms_to_hour(duration: i64) -> f64 {
duration as f64 / MS_TO_HOUR_DIVISOR
}
impl Resolve<ListBuildVersions, User> for State {
impl Resolve<ReadArgs> for ListBuildVersions {
async fn resolve(
&self,
ListBuildVersions {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<BuildVersionResponseItem>> {
let ListBuildVersions {
build,
major,
minor,
patch,
limit,
}: ListBuildVersions,
user: User,
) -> anyhow::Result<Vec<BuildVersionResponseItem>> {
let build = resource::get_check_permissions::<Build>(
} = self;
let build = get_check_permissions::<Build>(
&build,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
@@ -259,21 +273,24 @@ impl Resolve<ListBuildVersions, User> for State {
}
}
impl Resolve<ListCommonBuildExtraArgs, User> for State {
impl Resolve<ReadArgs> for ListCommonBuildExtraArgs {
async fn resolve(
&self,
ListCommonBuildExtraArgs { query }: ListCommonBuildExtraArgs,
user: User,
) -> anyhow::Result<ListCommonBuildExtraArgsResponse> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListCommonBuildExtraArgsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let builds =
resource::list_full_for_user::<Build>(query, &user, &all_tags)
.await
.context("failed to get resources matching query")?;
let builds = resource::list_full_for_user::<Build>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();
@@ -290,12 +307,11 @@ impl Resolve<ListCommonBuildExtraArgs, User> for State {
}
}
impl Resolve<GetBuildWebhookEnabled, User> for State {
impl Resolve<ReadArgs> for GetBuildWebhookEnabled {
async fn resolve(
&self,
GetBuildWebhookEnabled { build }: GetBuildWebhookEnabled,
user: User,
) -> anyhow::Result<GetBuildWebhookEnabledResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetBuildWebhookEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetBuildWebhookEnabledResponse {
managed: false,
@@ -303,10 +319,10 @@ impl Resolve<GetBuildWebhookEnabled, User> for State {
});
};
let build = resource::get_check_permissions::<Build>(
&build,
&user,
PermissionLevel::Read,
let build = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Read.into(),
)
.await?;

View File

@@ -4,7 +4,6 @@ use komodo_client::{
entities::{
builder::{Builder, BuilderListItem},
permission::PermissionLevel,
user::User,
},
};
use mongo_indexed::Document;
@@ -12,66 +11,80 @@ use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags,
resource,
state::{db_client, State},
helpers::query::get_all_tags, permission::get_check_permissions,
resource, state::db_client,
};
impl Resolve<GetBuilder, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetBuilder {
async fn resolve(
&self,
GetBuilder { builder }: GetBuilder,
user: User,
) -> anyhow::Result<Builder> {
resource::get_check_permissions::<Builder>(
&builder,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Builder> {
Ok(
get_check_permissions::<Builder>(
&self.builder,
user,
PermissionLevel::Read.into(),
)
.await?,
)
.await
}
}
impl Resolve<ListBuilders, User> for State {
impl Resolve<ReadArgs> for ListBuilders {
async fn resolve(
&self,
ListBuilders { query }: ListBuilders,
user: User,
) -> anyhow::Result<Vec<BuilderListItem>> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<BuilderListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Builder>(query, &user, &all_tags).await
Ok(
resource::list_for_user::<Builder>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ListFullBuilders, User> for State {
impl Resolve<ReadArgs> for ListFullBuilders {
async fn resolve(
&self,
ListFullBuilders { query }: ListFullBuilders,
user: User,
) -> anyhow::Result<ListFullBuildersResponse> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullBuildersResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Builder>(query, &user, &all_tags)
.await
Ok(
resource::list_full_for_user::<Builder>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<GetBuildersSummary, User> for State {
impl Resolve<ReadArgs> for GetBuildersSummary {
async fn resolve(
&self,
GetBuildersSummary {}: GetBuildersSummary,
user: User,
) -> anyhow::Result<GetBuildersSummaryResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetBuildersSummaryResponse> {
let query = match resource::get_resource_object_ids_for_user::<
Builder,
>(&user)
>(user)
.await?
{
Some(ids) => doc! {

View File

@@ -1,6 +1,6 @@
use std::{cmp, collections::HashSet};
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use komodo_client::{
api::read::*,
entities::{
@@ -8,81 +8,103 @@ use komodo_client::{
Deployment, DeploymentActionState, DeploymentConfig,
DeploymentListItem, DeploymentState,
},
docker::container::ContainerStats,
docker::container::{Container, ContainerStats},
permission::PermissionLevel,
server::Server,
server::{Server, ServerState},
update::Log,
user::User,
},
};
use periphery_client::api;
use periphery_client::api::{self, container::InspectContainer};
use resolver_api::Resolve;
use crate::{
helpers::{periphery_client, query::get_all_tags},
permission::get_check_permissions,
resource,
state::{action_states, deployment_status_cache, State},
state::{
action_states, deployment_status_cache, server_status_cache,
},
};
impl Resolve<GetDeployment, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetDeployment {
async fn resolve(
&self,
GetDeployment { deployment }: GetDeployment,
user: User,
) -> anyhow::Result<Deployment> {
resource::get_check_permissions::<Deployment>(
&deployment,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Deployment> {
Ok(
get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read.into(),
)
.await?,
)
.await
}
}
impl Resolve<ListDeployments, User> for State {
impl Resolve<ReadArgs> for ListDeployments {
async fn resolve(
&self,
ListDeployments { query }: ListDeployments,
user: User,
) -> anyhow::Result<Vec<DeploymentListItem>> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<DeploymentListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Deployment>(query, &user, &all_tags)
.await
let only_update_available = self.query.specific.update_available;
let deployments = resource::list_for_user::<Deployment>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?;
let deployments = if only_update_available {
deployments
.into_iter()
.filter(|deployment| deployment.info.update_available)
.collect()
} else {
deployments
};
Ok(deployments)
}
}
impl Resolve<ListFullDeployments, User> for State {
impl Resolve<ReadArgs> for ListFullDeployments {
async fn resolve(
&self,
ListFullDeployments { query }: ListFullDeployments,
user: User,
) -> anyhow::Result<ListFullDeploymentsResponse> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullDeploymentsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Deployment>(
query, &user, &all_tags,
Ok(
resource::list_full_for_user::<Deployment>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
.await
}
}
impl Resolve<GetDeploymentContainer, User> for State {
impl Resolve<ReadArgs> for GetDeploymentContainer {
async fn resolve(
&self,
GetDeploymentContainer { deployment }: GetDeploymentContainer,
user: User,
) -> anyhow::Result<GetDeploymentContainerResponse> {
let deployment = resource::get_check_permissions::<Deployment>(
&deployment,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDeploymentContainerResponse> {
let deployment = get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read.into(),
)
.await?;
let status = deployment_status_cache()
@@ -99,68 +121,69 @@ impl Resolve<GetDeploymentContainer, User> for State {
const MAX_LOG_LENGTH: u64 = 5000;
impl Resolve<GetDeploymentLog, User> for State {
impl Resolve<ReadArgs> for GetDeploymentLog {
async fn resolve(
&self,
GetDeploymentLog {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
let GetDeploymentLog {
deployment,
tail,
timestamps,
}: GetDeploymentLog,
user: User,
) -> anyhow::Result<Log> {
} = self;
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = resource::get_check_permissions::<Deployment>(
} = get_check_permissions::<Deployment>(
&deployment,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.logs(),
)
.await?;
if server_id.is_empty() {
return Ok(Log::default());
}
let server = resource::get::<Server>(&server_id).await?;
periphery_client(&server)?
let res = periphery_client(&server)?
.request(api::container::GetContainerLog {
name,
tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps,
})
.await
.context("failed at call to periphery")
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<SearchDeploymentLog, User> for State {
impl Resolve<ReadArgs> for SearchDeploymentLog {
async fn resolve(
&self,
SearchDeploymentLog {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
let SearchDeploymentLog {
deployment,
terms,
combinator,
invert,
timestamps,
}: SearchDeploymentLog,
user: User,
) -> anyhow::Result<Log> {
} = self;
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = resource::get_check_permissions::<Deployment>(
} = get_check_permissions::<Deployment>(
&deployment,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.logs(),
)
.await?;
if server_id.is_empty() {
return Ok(Log::default());
}
let server = resource::get::<Server>(&server_id).await?;
periphery_client(&server)?
let res = periphery_client(&server)?
.request(api::container::GetContainerLogSearch {
name,
terms,
@@ -169,47 +192,93 @@ impl Resolve<SearchDeploymentLog, User> for State {
timestamps,
})
.await
.context("failed at call to periphery")
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<GetDeploymentStats, User> for State {
impl Resolve<ReadArgs> for InspectDeploymentContainer {
async fn resolve(
&self,
GetDeploymentStats { deployment }: GetDeploymentStats,
user: User,
) -> anyhow::Result<ContainerStats> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Container> {
let InspectDeploymentContainer { deployment } = self;
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = resource::get_check_permissions::<Deployment>(
} = get_check_permissions::<Deployment>(
&deployment,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.inspect(),
)
.await?;
if server_id.is_empty() {
return Err(anyhow!("deployment has no server attached"));
return Err(
anyhow!(
"Cannot inspect deployment, not attached to any server"
)
.into(),
);
}
let server = resource::get::<Server>(&server_id).await?;
periphery_client(&server)?
.request(api::container::GetContainerStats { name })
.await
.context("failed to get stats from periphery")
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)?
.request(InspectContainer { name })
.await?;
Ok(res)
}
}
impl Resolve<GetDeploymentActionState, User> for State {
impl Resolve<ReadArgs> for GetDeploymentStats {
async fn resolve(
&self,
GetDeploymentActionState { deployment }: GetDeploymentActionState,
user: User,
) -> anyhow::Result<DeploymentActionState> {
let deployment = resource::get_check_permissions::<Deployment>(
&deployment,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ContainerStats> {
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read.into(),
)
.await?;
if server_id.is_empty() {
return Err(
anyhow!("deployment has no server attached").into(),
);
}
let server = resource::get::<Server>(&server_id).await?;
let res = periphery_client(&server)?
.request(api::container::GetContainerStats { name })
.await
.context("failed to get stats from periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for GetDeploymentActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<DeploymentActionState> {
let deployment = get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
@@ -222,15 +291,15 @@ impl Resolve<GetDeploymentActionState, User> for State {
}
}
impl Resolve<GetDeploymentsSummary, User> for State {
impl Resolve<ReadArgs> for GetDeploymentsSummary {
async fn resolve(
&self,
GetDeploymentsSummary {}: GetDeploymentsSummary,
user: User,
) -> anyhow::Result<GetDeploymentsSummaryResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDeploymentsSummaryResponse> {
let deployments = resource::list_full_for_user::<Deployment>(
Default::default(),
&user,
user,
PermissionLevel::Read.into(),
&[],
)
.await
@@ -263,19 +332,21 @@ impl Resolve<GetDeploymentsSummary, User> for State {
}
}
impl Resolve<ListCommonDeploymentExtraArgs, User> for State {
impl Resolve<ReadArgs> for ListCommonDeploymentExtraArgs {
async fn resolve(
&self,
ListCommonDeploymentExtraArgs { query }: ListCommonDeploymentExtraArgs,
user: User,
) -> anyhow::Result<ListCommonDeploymentExtraArgsResponse> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListCommonDeploymentExtraArgsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let deployments = resource::list_full_for_user::<Deployment>(
query, &user, &all_tags,
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await
.context("failed to get resources matching query")?;

View File

@@ -1,34 +1,38 @@
use std::{collections::HashSet, sync::OnceLock, time::Instant};
use anyhow::{anyhow, Context};
use axum::{middleware, routing::post, Extension, Router};
use axum_extra::{headers::ContentType, TypedHeader};
use anyhow::{Context, anyhow};
use axum::{
Extension, Router, extract::Path, middleware, routing::post,
};
use komodo_client::{
api::read::*,
entities::{
ResourceTarget,
build::Build,
builder::{Builder, BuilderConfig},
config::{DockerRegistry, GitProvider},
permission::PermissionLevel,
repo::Repo,
server::Server,
sync::ResourceSync,
user::User,
ResourceTarget,
},
};
use resolver_api::{
derive::Resolver, Resolve, ResolveToString, Resolver,
};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
use crate::{
auth::auth_request, config::core_config, helpers::periphery_client,
resource, state::State,
resource,
};
use super::Variant;
mod action;
mod alert;
mod alerter;
@@ -39,9 +43,8 @@ mod permission;
mod procedure;
mod provider;
mod repo;
mod search;
mod schedule;
mod server;
mod server_template;
mod stack;
mod sync;
mod tag;
@@ -51,15 +54,18 @@ mod user;
mod user_group;
mod variable;
pub struct ReadArgs {
pub user: User,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
#[resolver_target(State)]
#[resolver_args(User)]
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
#[args(ReadArgs)]
#[response(Response)]
#[error(serror::Error)]
#[serde(tag = "type", content = "params")]
enum ReadRequest {
#[to_string_resolver]
GetVersion(GetVersion),
#[to_string_resolver]
GetCoreInfo(GetCoreInfo),
ListSecrets(ListSecrets),
ListGitProvidersFromConfig(ListGitProvidersFromConfig),
@@ -67,7 +73,7 @@ enum ReadRequest {
// ==== USER ====
GetUsername(GetUsername),
GetPermissionLevel(GetPermissionLevel),
GetPermission(GetPermission),
FindUser(FindUser),
ListUsers(ListUsers),
ListApiKeys(ListApiKeys),
@@ -79,9 +85,6 @@ enum ReadRequest {
GetUserGroup(GetUserGroup),
ListUserGroups(ListUserGroups),
// ==== SEARCH ====
FindResources(FindResources),
// ==== PROCEDURE ====
GetProceduresSummary(GetProceduresSummary),
GetProcedure(GetProcedure),
@@ -96,11 +99,8 @@ enum ReadRequest {
ListActions(ListActions),
ListFullActions(ListFullActions),
// ==== SERVER TEMPLATE ====
GetServerTemplate(GetServerTemplate),
GetServerTemplatesSummary(GetServerTemplatesSummary),
ListServerTemplates(ListServerTemplates),
ListFullServerTemplates(ListFullServerTemplates),
// ==== SCHEDULE ====
ListSchedules(ListSchedules),
// ==== SERVER ====
GetServersSummary(GetServersSummary),
@@ -119,17 +119,33 @@ enum ReadRequest {
InspectDockerImage(InspectDockerImage),
ListDockerImageHistory(ListDockerImageHistory),
InspectDockerVolume(InspectDockerVolume),
GetDockerContainersSummary(GetDockerContainersSummary),
ListAllDockerContainers(ListAllDockerContainers),
#[to_string_resolver]
ListDockerContainers(ListDockerContainers),
#[to_string_resolver]
ListDockerNetworks(ListDockerNetworks),
#[to_string_resolver]
ListDockerImages(ListDockerImages),
#[to_string_resolver]
ListDockerVolumes(ListDockerVolumes),
#[to_string_resolver]
ListComposeProjects(ListComposeProjects),
ListTerminals(ListTerminals),
// ==== SERVER STATS ====
GetSystemInformation(GetSystemInformation),
GetSystemStats(GetSystemStats),
ListSystemProcesses(ListSystemProcesses),
// ==== STACK ====
GetStacksSummary(GetStacksSummary),
GetStack(GetStack),
GetStackActionState(GetStackActionState),
GetStackWebhooksEnabled(GetStackWebhooksEnabled),
GetStackLog(GetStackLog),
SearchStackLog(SearchStackLog),
InspectStackContainer(InspectStackContainer),
ListStacks(ListStacks),
ListFullStacks(ListFullStacks),
ListStackServices(ListStackServices),
ListCommonStackExtraArgs(ListCommonStackExtraArgs),
ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs),
// ==== DEPLOYMENT ====
GetDeploymentsSummary(GetDeploymentsSummary),
@@ -139,6 +155,7 @@ enum ReadRequest {
GetDeploymentStats(GetDeploymentStats),
GetDeploymentLog(GetDeploymentLog),
SearchDeploymentLog(SearchDeploymentLog),
InspectDeploymentContainer(InspectDeploymentContainer),
ListDeployments(ListDeployments),
ListFullDeployments(ListFullDeployments),
ListCommonDeploymentExtraArgs(ListCommonDeploymentExtraArgs),
@@ -170,19 +187,6 @@ enum ReadRequest {
ListResourceSyncs(ListResourceSyncs),
ListFullResourceSyncs(ListFullResourceSyncs),
// ==== STACK ====
GetStacksSummary(GetStacksSummary),
GetStack(GetStack),
GetStackActionState(GetStackActionState),
GetStackWebhooksEnabled(GetStackWebhooksEnabled),
GetStackServiceLog(GetStackServiceLog),
SearchStackServiceLog(SearchStackServiceLog),
ListStacks(ListStacks),
ListFullStacks(ListFullStacks),
ListStackServices(ListStackServices),
ListCommonStackExtraArgs(ListCommonStackExtraArgs),
ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs),
// ==== BUILDER ====
GetBuildersSummary(GetBuildersSummary),
GetBuilder(GetBuilder),
@@ -211,14 +215,6 @@ enum ReadRequest {
ListAlerts(ListAlerts),
GetAlert(GetAlert),
// ==== SERVER STATS ====
#[to_string_resolver]
GetSystemInformation(GetSystemInformation),
#[to_string_resolver]
GetSystemStats(GetSystemStats),
#[to_string_resolver]
ListSystemProcesses(ListSystemProcesses),
// ==== VARIABLE ====
GetVariable(GetVariable),
ListVariables(ListVariables),
@@ -233,61 +229,55 @@ enum ReadRequest {
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler))
.layer(middleware::from_fn(auth_request))
}
async fn variant_handler(
user: Extension<User>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<axum::response::Response> {
let req: ReadRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
}))?;
handler(user, Json(req)).await
}
#[instrument(name = "ReadHandler", level = "debug", skip(user), fields(user_id = user.id))]
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<ReadRequest>,
) -> serror::Result<(TypedHeader<ContentType>, String)> {
) -> serror::Result<axum::response::Response> {
let timer = Instant::now();
let req_id = Uuid::new_v4();
debug!("/read request | user: {}", user.username);
let res =
State
.resolve_request(request, user)
.await
.map_err(|e| match e {
resolver_api::Error::Serialization(e) => {
anyhow!("{e:?}").context("response serialization error")
}
resolver_api::Error::Inner(e) => e,
});
let res = request.resolve(&ReadArgs { user }).await;
if let Err(e) = &res {
debug!("/read request {req_id} error: {e:#}");
debug!("/read request {req_id} error: {:#}", e.error);
}
let elapsed = timer.elapsed();
debug!("/read request {req_id} | resolve time: {elapsed:?}");
Ok((TypedHeader(ContentType::json()), res?))
res.map(|res| res.0)
}
fn version() -> &'static String {
static VERSION: OnceLock<String> = OnceLock::new();
VERSION.get_or_init(|| {
serde_json::to_string(&GetVersionResponse {
impl Resolve<ReadArgs> for GetVersion {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<GetVersionResponse> {
Ok(GetVersionResponse {
version: env!("CARGO_PKG_VERSION").to_string(),
})
.context("failed to serialize GetVersionResponse")
.unwrap()
})
}
impl ResolveToString<GetVersion, User> for State {
async fn resolve_to_string(
&self,
GetVersion {}: GetVersion,
_: User,
) -> anyhow::Result<String> {
Ok(version().to_string())
}
}
fn core_info() -> &'static String {
static CORE_INFO: OnceLock<String> = OnceLock::new();
fn core_info() -> &'static GetCoreInfoResponse {
static CORE_INFO: OnceLock<GetCoreInfoResponse> = OnceLock::new();
CORE_INFO.get_or_init(|| {
let config = core_config();
let info = GetCoreInfoResponse {
GetCoreInfoResponse {
title: config.title.clone(),
monitoring_interval: config.monitoring_interval,
webhook_base_url: if config.webhook_base_url.is_empty() {
@@ -299,42 +289,39 @@ fn core_info() -> &'static String {
ui_write_disabled: config.ui_write_disabled,
disable_confirm_dialog: config.disable_confirm_dialog,
disable_non_admin_create: config.disable_non_admin_create,
disable_websocket_reconnect: config.disable_websocket_reconnect,
github_webhook_owners: config
.github_webhook_app
.installations
.iter()
.map(|i| i.namespace.to_string())
.collect(),
};
serde_json::to_string(&info)
.context("failed to serialize GetCoreInfoResponse")
.unwrap()
timezone: config.timezone.clone(),
}
})
}
impl ResolveToString<GetCoreInfo, User> for State {
async fn resolve_to_string(
&self,
GetCoreInfo {}: GetCoreInfo,
_: User,
) -> anyhow::Result<String> {
Ok(core_info().to_string())
impl Resolve<ReadArgs> for GetCoreInfo {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<GetCoreInfoResponse> {
Ok(core_info().clone())
}
}
impl Resolve<ListSecrets, User> for State {
impl Resolve<ReadArgs> for ListSecrets {
async fn resolve(
&self,
ListSecrets { target }: ListSecrets,
_: User,
) -> anyhow::Result<ListSecretsResponse> {
self,
_: &ReadArgs,
) -> serror::Result<ListSecretsResponse> {
let mut secrets = core_config()
.secrets
.keys()
.cloned()
.collect::<HashSet<_>>();
if let Some(target) = target {
if let Some(target) = self.target {
let server_id = match target {
ResourceTarget::Server(id) => Some(id),
ResourceTarget::Builder(id) => {
@@ -348,7 +335,9 @@ impl Resolve<ListSecrets, User> for State {
}
}
_ => {
return Err(anyhow!("target must be `Server` or `Builder`"))
return Err(
anyhow!("target must be `Server` or `Builder`").into(),
);
}
};
if let Some(id) = server_id {
@@ -373,15 +362,14 @@ impl Resolve<ListSecrets, User> for State {
}
}
impl Resolve<ListGitProvidersFromConfig, User> for State {
impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
async fn resolve(
&self,
ListGitProvidersFromConfig { target }: ListGitProvidersFromConfig,
user: User,
) -> anyhow::Result<ListGitProvidersFromConfigResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListGitProvidersFromConfigResponse> {
let mut providers = core_config().git_providers.clone();
if let Some(target) = target {
if let Some(target) = self.target {
match target {
ResourceTarget::Server(id) => {
merge_git_providers_for_server(&mut providers, &id).await?;
@@ -405,7 +393,9 @@ impl Resolve<ListGitProvidersFromConfig, User> for State {
}
}
_ => {
return Err(anyhow!("target must be `Server` or `Builder`"))
return Err(
anyhow!("target must be `Server` or `Builder`").into(),
);
}
}
}
@@ -413,17 +403,20 @@ impl Resolve<ListGitProvidersFromConfig, User> for State {
let (builds, repos, syncs) = tokio::try_join!(
resource::list_full_for_user::<Build>(
Default::default(),
&user,
user,
PermissionLevel::Read.into(),
&[]
),
resource::list_full_for_user::<Repo>(
Default::default(),
&user,
user,
PermissionLevel::Read.into(),
&[]
),
resource::list_full_for_user::<ResourceSync>(
Default::default(),
&user,
user,
PermissionLevel::Read.into(),
&[]
),
)?;
@@ -471,15 +464,14 @@ impl Resolve<ListGitProvidersFromConfig, User> for State {
}
}
impl Resolve<ListDockerRegistriesFromConfig, User> for State {
impl Resolve<ReadArgs> for ListDockerRegistriesFromConfig {
async fn resolve(
&self,
ListDockerRegistriesFromConfig { target }: ListDockerRegistriesFromConfig,
_: User,
) -> anyhow::Result<ListDockerRegistriesFromConfigResponse> {
self,
_: &ReadArgs,
) -> serror::Result<ListDockerRegistriesFromConfigResponse> {
let mut registries = core_config().docker_registries.clone();
if let Some(target) = target {
if let Some(target) = self.target {
match target {
ResourceTarget::Server(id) => {
merge_docker_registries_for_server(&mut registries, &id)
@@ -504,7 +496,9 @@ impl Resolve<ListDockerRegistriesFromConfig, User> for State {
}
}
_ => {
return Err(anyhow!("target must be `Server` or `Builder`"))
return Err(
anyhow!("target must be `Server` or `Builder`").into(),
);
}
}
}
@@ -518,7 +512,7 @@ impl Resolve<ListDockerRegistriesFromConfig, User> for State {
async fn merge_git_providers_for_server(
providers: &mut Vec<GitProvider>,
server_id: &str,
) -> anyhow::Result<()> {
) -> serror::Result<()> {
let server = resource::get::<Server>(server_id).await?;
let more = periphery_client(&server)?
.request(periphery_client::api::ListGitProviders {})
@@ -556,7 +550,7 @@ fn merge_git_providers(
async fn merge_docker_registries_for_server(
registries: &mut Vec<DockerRegistry>,
server_id: &str,
) -> anyhow::Result<()> {
) -> serror::Result<()> {
let server = resource::get::<Server>(server_id).await?;
let more = periphery_client(&server)?
.request(periphery_client::api::ListDockerRegistries {})

View File

@@ -1,27 +1,27 @@
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use komodo_client::{
api::read::{
GetPermissionLevel, GetPermissionLevelResponse, ListPermissions,
GetPermission, GetPermissionResponse, ListPermissions,
ListPermissionsResponse, ListUserTargetPermissions,
ListUserTargetPermissionsResponse,
},
entities::{permission::PermissionLevel, user::User},
entities::permission::PermissionLevel,
};
use mungos::{find::find_collect, mongodb::bson::doc};
use resolver_api::Resolve;
use crate::{
helpers::query::get_user_permission_on_target,
state::{db_client, State},
helpers::query::get_user_permission_on_target, state::db_client,
};
impl Resolve<ListPermissions, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for ListPermissions {
async fn resolve(
&self,
ListPermissions {}: ListPermissions,
user: User,
) -> anyhow::Result<ListPermissionsResponse> {
find_collect(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListPermissionsResponse> {
let res = find_collect(
&db_client().permissions,
doc! {
"user_target.type": "User",
@@ -30,34 +30,33 @@ impl Resolve<ListPermissions, User> for State {
None,
)
.await
.context("failed to query db for permissions")
.context("failed to query db for permissions")?;
Ok(res)
}
}
impl Resolve<GetPermissionLevel, User> for State {
impl Resolve<ReadArgs> for GetPermission {
async fn resolve(
&self,
GetPermissionLevel { target }: GetPermissionLevel,
user: User,
) -> anyhow::Result<GetPermissionLevelResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetPermissionResponse> {
if user.admin {
return Ok(PermissionLevel::Write);
return Ok(PermissionLevel::Write.all());
}
get_user_permission_on_target(&user, &target).await
Ok(get_user_permission_on_target(user, &self.target).await?)
}
}
impl Resolve<ListUserTargetPermissions, User> for State {
impl Resolve<ReadArgs> for ListUserTargetPermissions {
async fn resolve(
&self,
ListUserTargetPermissions { user_target }: ListUserTargetPermissions,
user: User,
) -> anyhow::Result<ListUserTargetPermissionsResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListUserTargetPermissionsResponse> {
if !user.admin {
return Err(anyhow!("this method is admin only"));
return Err(anyhow!("this method is admin only").into());
}
let (variant, id) = user_target.extract_variant_id();
find_collect(
let (variant, id) = self.user_target.extract_variant_id();
let res = find_collect(
&db_client().permissions,
doc! {
"user_target.type": variant.as_ref(),
@@ -66,6 +65,7 @@ impl Resolve<ListUserTargetPermissions, User> for State {
None,
)
.await
.context("failed to query db for permissions")
.context("failed to query db for permissions")?;
Ok(res)
}
}

View File

@@ -4,73 +4,88 @@ use komodo_client::{
entities::{
permission::PermissionLevel,
procedure::{Procedure, ProcedureState},
user::User,
},
};
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{action_states, procedure_state_cache, State},
state::{action_states, procedure_state_cache},
};
impl Resolve<GetProcedure, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetProcedure {
async fn resolve(
&self,
GetProcedure { procedure }: GetProcedure,
user: User,
) -> anyhow::Result<GetProcedureResponse> {
resource::get_check_permissions::<Procedure>(
&procedure,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetProcedureResponse> {
Ok(
get_check_permissions::<Procedure>(
&self.procedure,
user,
PermissionLevel::Read.into(),
)
.await?,
)
.await
}
}
impl Resolve<ListProcedures, User> for State {
impl Resolve<ReadArgs> for ListProcedures {
async fn resolve(
&self,
ListProcedures { query }: ListProcedures,
user: User,
) -> anyhow::Result<ListProceduresResponse> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListProceduresResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Procedure>(query, &user, &all_tags)
.await
Ok(
resource::list_for_user::<Procedure>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ListFullProcedures, User> for State {
impl Resolve<ReadArgs> for ListFullProcedures {
async fn resolve(
&self,
ListFullProcedures { query }: ListFullProcedures,
user: User,
) -> anyhow::Result<ListFullProceduresResponse> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullProceduresResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Procedure>(query, &user, &all_tags)
.await
Ok(
resource::list_full_for_user::<Procedure>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<GetProceduresSummary, User> for State {
impl Resolve<ReadArgs> for GetProceduresSummary {
async fn resolve(
&self,
GetProceduresSummary {}: GetProceduresSummary,
user: User,
) -> anyhow::Result<GetProceduresSummaryResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetProceduresSummaryResponse> {
let procedures = resource::list_full_for_user::<Procedure>(
Default::default(),
&user,
user,
PermissionLevel::Read.into(),
&[],
)
.await
@@ -108,16 +123,15 @@ impl Resolve<GetProceduresSummary, User> for State {
}
}
impl Resolve<GetProcedureActionState, User> for State {
impl Resolve<ReadArgs> for GetProcedureActionState {
async fn resolve(
&self,
GetProcedureActionState { procedure }: GetProcedureActionState,
user: User,
) -> anyhow::Result<GetProcedureActionStateResponse> {
let procedure = resource::get_check_permissions::<Procedure>(
&procedure,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetProcedureActionStateResponse> {
let procedure = get_check_permissions::<Procedure>(
&self.procedure,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()

View File

@@ -1,59 +1,54 @@
use anyhow::{anyhow, Context};
use komodo_client::{
api::read::{
GetDockerRegistryAccount, GetDockerRegistryAccountResponse,
GetGitProviderAccount, GetGitProviderAccountResponse,
ListDockerRegistryAccounts, ListDockerRegistryAccountsResponse,
ListGitProviderAccounts, ListGitProviderAccountsResponse,
},
entities::user::User,
};
use mongo_indexed::{doc, Document};
use anyhow::{Context, anyhow};
use komodo_client::api::read::*;
use mongo_indexed::{Document, doc};
use mungos::{
by_id::find_one_by_id, find::find_collect,
mongodb::options::FindOptions,
};
use resolver_api::Resolve;
use crate::state::{db_client, State};
use crate::state::db_client;
impl Resolve<GetGitProviderAccount, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetGitProviderAccount {
async fn resolve(
&self,
GetGitProviderAccount { id }: GetGitProviderAccount,
user: User,
) -> anyhow::Result<GetGitProviderAccountResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetGitProviderAccountResponse> {
if !user.admin {
return Err(anyhow!(
"Only admins can read git provider accounts"
));
return Err(
anyhow!("Only admins can read git provider accounts").into(),
);
}
find_one_by_id(&db_client().git_accounts, &id)
let res = find_one_by_id(&db_client().git_accounts, &self.id)
.await
.context("failed to query db for git provider accounts")?
.context("did not find git provider account with the given id")
.context(
"did not find git provider account with the given id",
)?;
Ok(res)
}
}
impl Resolve<ListGitProviderAccounts, User> for State {
impl Resolve<ReadArgs> for ListGitProviderAccounts {
async fn resolve(
&self,
ListGitProviderAccounts { domain, username }: ListGitProviderAccounts,
user: User,
) -> anyhow::Result<ListGitProviderAccountsResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListGitProviderAccountsResponse> {
if !user.admin {
return Err(anyhow!(
"Only admins can read git provider accounts"
));
return Err(
anyhow!("Only admins can read git provider accounts").into(),
);
}
let mut filter = Document::new();
if let Some(domain) = domain {
if let Some(domain) = self.domain {
filter.insert("domain", domain);
}
if let Some(username) = username {
if let Some(username) = self.username {
filter.insert("username", username);
}
find_collect(
let res = find_collect(
&db_client().git_accounts,
filter,
FindOptions::builder()
@@ -61,49 +56,52 @@ impl Resolve<ListGitProviderAccounts, User> for State {
.build(),
)
.await
.context("failed to query db for git provider accounts")
.context("failed to query db for git provider accounts")?;
Ok(res)
}
}
impl Resolve<GetDockerRegistryAccount, User> for State {
impl Resolve<ReadArgs> for GetDockerRegistryAccount {
async fn resolve(
&self,
GetDockerRegistryAccount { id }: GetDockerRegistryAccount,
user: User,
) -> anyhow::Result<GetDockerRegistryAccountResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDockerRegistryAccountResponse> {
if !user.admin {
return Err(anyhow!(
"Only admins can read docker registry accounts"
));
return Err(
anyhow!("Only admins can read docker registry accounts")
.into(),
);
}
find_one_by_id(&db_client().registry_accounts, &id)
.await
.context("failed to query db for docker registry accounts")?
.context(
"did not find docker registry account with the given id",
)
let res =
find_one_by_id(&db_client().registry_accounts, &self.id)
.await
.context("failed to query db for docker registry accounts")?
.context(
"did not find docker registry account with the given id",
)?;
Ok(res)
}
}
impl Resolve<ListDockerRegistryAccounts, User> for State {
impl Resolve<ReadArgs> for ListDockerRegistryAccounts {
async fn resolve(
&self,
ListDockerRegistryAccounts { domain, username }: ListDockerRegistryAccounts,
user: User,
) -> anyhow::Result<ListDockerRegistryAccountsResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerRegistryAccountsResponse> {
if !user.admin {
return Err(anyhow!(
"Only admins can read docker registry accounts"
));
return Err(
anyhow!("Only admins can read docker registry accounts")
.into(),
);
}
let mut filter = Document::new();
if let Some(domain) = domain {
if let Some(domain) = self.domain {
filter.insert("domain", domain);
}
if let Some(username) = username {
if let Some(username) = self.username {
filter.insert("username", username);
}
find_collect(
let res = find_collect(
&db_client().registry_accounts,
filter,
FindOptions::builder()
@@ -111,6 +109,7 @@ impl Resolve<ListDockerRegistryAccounts, User> for State {
.build(),
)
.await
.context("failed to query db for docker registry accounts")
.context("failed to query db for docker registry accounts")?;
Ok(res)
}
}

View File

@@ -5,7 +5,6 @@ use komodo_client::{
config::core::CoreConfig,
permission::PermissionLevel,
repo::{Repo, RepoActionState, RepoListItem, RepoState},
user::User,
},
};
use resolver_api::Resolve;
@@ -13,66 +12,82 @@ use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{action_states, github_client, repo_state_cache, State},
state::{action_states, github_client, repo_state_cache},
};
impl Resolve<GetRepo, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetRepo {
async fn resolve(
&self,
GetRepo { repo }: GetRepo,
user: User,
) -> anyhow::Result<Repo> {
resource::get_check_permissions::<Repo>(
&repo,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Repo> {
Ok(
get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Read.into(),
)
.await?,
)
.await
}
}
impl Resolve<ListRepos, User> for State {
impl Resolve<ReadArgs> for ListRepos {
async fn resolve(
&self,
ListRepos { query }: ListRepos,
user: User,
) -> anyhow::Result<Vec<RepoListItem>> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<RepoListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Repo>(query, &user, &all_tags).await
Ok(
resource::list_for_user::<Repo>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ListFullRepos, User> for State {
impl Resolve<ReadArgs> for ListFullRepos {
async fn resolve(
&self,
ListFullRepos { query }: ListFullRepos,
user: User,
) -> anyhow::Result<ListFullReposResponse> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullReposResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Repo>(query, &user, &all_tags)
.await
Ok(
resource::list_full_for_user::<Repo>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<GetRepoActionState, User> for State {
impl Resolve<ReadArgs> for GetRepoActionState {
async fn resolve(
&self,
GetRepoActionState { repo }: GetRepoActionState,
user: User,
) -> anyhow::Result<RepoActionState> {
let repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<RepoActionState> {
let repo = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
@@ -85,15 +100,15 @@ impl Resolve<GetRepoActionState, User> for State {
}
}
impl Resolve<GetReposSummary, User> for State {
impl Resolve<ReadArgs> for GetReposSummary {
async fn resolve(
&self,
GetReposSummary {}: GetReposSummary,
user: User,
) -> anyhow::Result<GetReposSummaryResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetReposSummaryResponse> {
let repos = resource::list_full_for_user::<Repo>(
Default::default(),
&user,
user,
PermissionLevel::Read.into(),
&[],
)
.await
@@ -141,12 +156,11 @@ impl Resolve<GetReposSummary, User> for State {
}
}
impl Resolve<GetRepoWebhooksEnabled, User> for State {
impl Resolve<ReadArgs> for GetRepoWebhooksEnabled {
async fn resolve(
&self,
GetRepoWebhooksEnabled { repo }: GetRepoWebhooksEnabled,
user: User,
) -> anyhow::Result<GetRepoWebhooksEnabledResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetRepoWebhooksEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetRepoWebhooksEnabledResponse {
managed: false,
@@ -156,10 +170,10 @@ impl Resolve<GetRepoWebhooksEnabled, User> for State {
});
};
let repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
PermissionLevel::Read,
let repo = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Read.into(),
)
.await?;

View File

@@ -0,0 +1,102 @@
use futures::future::join_all;
use komodo_client::{
api::read::*,
entities::{
ResourceTarget, action::Action, permission::PermissionLevel,
procedure::Procedure, resource::ResourceQuery,
schedule::Schedule,
},
};
use resolver_api::Resolve;
use crate::{
helpers::query::{get_all_tags, get_last_run_at},
resource::list_full_for_user,
schedule::get_schedule_item_info,
};
use super::ReadArgs;
impl Resolve<ReadArgs> for ListSchedules {
async fn resolve(
self,
args: &ReadArgs,
) -> serror::Result<Vec<Schedule>> {
let all_tags = get_all_tags(None).await?;
let (actions, procedures) = tokio::try_join!(
list_full_for_user::<Action>(
ResourceQuery {
names: Default::default(),
tag_behavior: self.tag_behavior,
tags: self.tags.clone(),
specific: Default::default(),
},
&args.user,
PermissionLevel::Read.into(),
&all_tags,
),
list_full_for_user::<Procedure>(
ResourceQuery {
names: Default::default(),
tag_behavior: self.tag_behavior,
tags: self.tags.clone(),
specific: Default::default(),
},
&args.user,
PermissionLevel::Read.into(),
&all_tags,
)
)?;
let actions = actions.into_iter().map(async |action| {
let (next_scheduled_run, schedule_error) =
get_schedule_item_info(&ResourceTarget::Action(
action.id.clone(),
));
let last_run_at =
get_last_run_at::<Action>(&action.id).await.unwrap_or(None);
Schedule {
target: ResourceTarget::Action(action.id),
name: action.name,
enabled: action.config.schedule_enabled,
schedule_format: action.config.schedule_format,
schedule: action.config.schedule,
schedule_timezone: action.config.schedule_timezone,
tags: action.tags,
last_run_at,
next_scheduled_run,
schedule_error,
}
});
let procedures = procedures.into_iter().map(async |procedure| {
let (next_scheduled_run, schedule_error) =
get_schedule_item_info(&ResourceTarget::Procedure(
procedure.id.clone(),
));
let last_run_at = get_last_run_at::<Procedure>(&procedure.id)
.await
.unwrap_or(None);
Schedule {
target: ResourceTarget::Procedure(procedure.id),
name: procedure.name,
enabled: procedure.config.schedule_enabled,
schedule_format: procedure.config.schedule_format,
schedule: procedure.config.schedule,
schedule_timezone: procedure.config.schedule_timezone,
tags: procedure.tags,
last_run_at,
next_scheduled_run,
schedule_error,
}
});
let (actions, procedures) =
tokio::join!(join_all(actions), join_all(procedures));
Ok(
actions
.into_iter()
.chain(procedures)
.filter(|s| !s.schedule.is_empty())
.collect(),
)
}
}

View File

@@ -1,82 +0,0 @@
use komodo_client::{
api::read::{FindResources, FindResourcesResponse},
entities::{
build::Build, deployment::Deployment, procedure::Procedure,
repo::Repo, server::Server, user::User, ResourceTargetVariant,
},
};
use resolver_api::Resolve;
use crate::{resource, state::State};
const FIND_RESOURCE_TYPES: [ResourceTargetVariant; 5] = [
ResourceTargetVariant::Server,
ResourceTargetVariant::Build,
ResourceTargetVariant::Deployment,
ResourceTargetVariant::Repo,
ResourceTargetVariant::Procedure,
];
impl Resolve<FindResources, User> for State {
async fn resolve(
&self,
FindResources { query, resources }: FindResources,
user: User,
) -> anyhow::Result<FindResourcesResponse> {
let mut res = FindResourcesResponse::default();
let resource_types = if resources.is_empty() {
FIND_RESOURCE_TYPES.to_vec()
} else {
resources
.into_iter()
.filter(|r| {
!matches!(
r,
ResourceTargetVariant::System
| ResourceTargetVariant::Builder
| ResourceTargetVariant::Alerter
)
})
.collect()
};
for resource_type in resource_types {
match resource_type {
ResourceTargetVariant::Server => {
res.servers = resource::list_for_user_using_document::<
Server,
>(query.clone(), &user)
.await?;
}
ResourceTargetVariant::Deployment => {
res.deployments = resource::list_for_user_using_document::<
Deployment,
>(query.clone(), &user)
.await?;
}
ResourceTargetVariant::Build => {
res.builds =
resource::list_for_user_using_document::<Build>(
query.clone(),
&user,
)
.await?;
}
ResourceTargetVariant::Repo => {
res.repos = resource::list_for_user_using_document::<Repo>(
query.clone(),
&user,
)
.await?;
}
ResourceTargetVariant::Procedure => {
res.procedures = resource::list_for_user_using_document::<
Procedure,
>(query.clone(), &user)
.await?;
}
_ => {}
}
}
Ok(res)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,94 +0,0 @@
use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel, server_template::ServerTemplate,
user::User,
},
};
use mongo_indexed::Document;
use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags,
resource,
state::{db_client, State},
};
impl Resolve<GetServerTemplate, User> for State {
async fn resolve(
&self,
GetServerTemplate { server_template }: GetServerTemplate,
user: User,
) -> anyhow::Result<GetServerTemplateResponse> {
resource::get_check_permissions::<ServerTemplate>(
&server_template,
&user,
PermissionLevel::Read,
)
.await
}
}
impl Resolve<ListServerTemplates, User> for State {
async fn resolve(
&self,
ListServerTemplates { query }: ListServerTemplates,
user: User,
) -> anyhow::Result<ListServerTemplatesResponse> {
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<ServerTemplate>(query, &user, &all_tags)
.await
}
}
impl Resolve<ListFullServerTemplates, User> for State {
async fn resolve(
&self,
ListFullServerTemplates { query }: ListFullServerTemplates,
user: User,
) -> anyhow::Result<ListFullServerTemplatesResponse> {
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<ServerTemplate>(
query, &user, &all_tags,
)
.await
}
}
impl Resolve<GetServerTemplatesSummary, User> for State {
async fn resolve(
&self,
GetServerTemplatesSummary {}: GetServerTemplatesSummary,
user: User,
) -> anyhow::Result<GetServerTemplatesSummaryResponse> {
let query = match resource::get_resource_object_ids_for_user::<
ServerTemplate,
>(&user)
.await?
{
Some(ids) => doc! {
"_id": { "$in": ids }
},
None => Document::new(),
};
let total = db_client()
.server_templates
.count_documents(query)
.await
.context("failed to count all server template documents")?;
let res = GetServerTemplatesSummaryResponse {
total: total as u32,
};
Ok(res)
}
}

View File

@@ -1,53 +1,61 @@
use std::collections::HashSet;
use anyhow::Context;
use anyhow::{Context, anyhow};
use komodo_client::{
api::read::*,
entities::{
config::core::CoreConfig,
docker::container::Container,
permission::PermissionLevel,
server::{Server, ServerState},
stack::{Stack, StackActionState, StackListItem, StackState},
user::User,
},
};
use periphery_client::api::compose::{
GetComposeServiceLog, GetComposeServiceLogSearch,
use periphery_client::api::{
compose::{GetComposeLog, GetComposeLogSearch},
container::InspectContainer,
};
use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::{periphery_client, query::get_all_tags},
permission::get_check_permissions,
resource,
stack::get_stack_and_server,
state::{action_states, github_client, stack_status_cache, State},
state::{
action_states, github_client, server_status_cache,
stack_status_cache,
},
};
impl Resolve<GetStack, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetStack {
async fn resolve(
&self,
GetStack { stack }: GetStack,
user: User,
) -> anyhow::Result<Stack> {
resource::get_check_permissions::<Stack>(
&stack,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Stack> {
Ok(
get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read.into(),
)
.await?,
)
.await
}
}
impl Resolve<ListStackServices, User> for State {
impl Resolve<ReadArgs> for ListStackServices {
async fn resolve(
&self,
ListStackServices { stack }: ListStackServices,
user: User,
) -> anyhow::Result<ListStackServicesResponse> {
let stack = resource::get_check_permissions::<Stack>(
&stack,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListStackServicesResponse> {
let stack = get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read.into(),
)
.await?;
@@ -63,85 +71,144 @@ impl Resolve<ListStackServices, User> for State {
}
}
impl Resolve<GetStackServiceLog, User> for State {
impl Resolve<ReadArgs> for GetStackLog {
async fn resolve(
&self,
GetStackServiceLog {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetStackLogResponse> {
let GetStackLog {
stack,
service,
services,
tail,
timestamps,
}: GetStackServiceLog,
user: User,
) -> anyhow::Result<GetStackServiceLogResponse> {
} = self;
let (stack, server) = get_stack_and_server(
&stack,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.logs(),
true,
)
.await?;
periphery_client(&server)?
.request(GetComposeServiceLog {
let res = periphery_client(&server)?
.request(GetComposeLog {
project: stack.project_name(false),
service,
services,
tail,
timestamps,
})
.await
.context("failed to get stack service log from periphery")
.context("Failed to get stack log from periphery")?;
Ok(res)
}
}
impl Resolve<SearchStackServiceLog, User> for State {
impl Resolve<ReadArgs> for SearchStackLog {
async fn resolve(
&self,
SearchStackServiceLog {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<SearchStackLogResponse> {
let SearchStackLog {
stack,
service,
services,
terms,
combinator,
invert,
timestamps,
}: SearchStackServiceLog,
user: User,
) -> anyhow::Result<SearchStackServiceLogResponse> {
} = self;
let (stack, server) = get_stack_and_server(
&stack,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.logs(),
true,
)
.await?;
periphery_client(&server)?
.request(GetComposeServiceLogSearch {
let res = periphery_client(&server)?
.request(GetComposeLogSearch {
project: stack.project_name(false),
service,
services,
terms,
combinator,
invert,
timestamps,
})
.await
.context("failed to get stack service log from periphery")
.context("Failed to search stack log from periphery")?;
Ok(res)
}
}
impl Resolve<ListCommonStackExtraArgs, User> for State {
impl Resolve<ReadArgs> for InspectStackContainer {
async fn resolve(
&self,
ListCommonStackExtraArgs { query }: ListCommonStackExtraArgs,
user: User,
) -> anyhow::Result<ListCommonStackExtraArgsResponse> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Container> {
let InspectStackContainer { stack, service } = self;
let stack = get_check_permissions::<Stack>(
&stack,
user,
PermissionLevel::Read.inspect(),
)
.await?;
if stack.config.server_id.is_empty() {
return Err(
anyhow!("Cannot inspect stack, not attached to any server")
.into(),
);
}
let server =
resource::get::<Server>(&stack.config.server_id).await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
)
.into(),
);
}
let services = &stack_status_cache()
.get(&stack.id)
.await
.unwrap_or_default()
.curr
.services;
let Some(name) = services
.iter()
.find(|s| s.service == service)
.and_then(|s| s.container.as_ref().map(|c| c.name.clone()))
else {
return Err(anyhow!(
"No service found matching '{service}'. Was the stack last deployed manually?"
).into());
};
let res = periphery_client(&server)?
.request(InspectContainer { name })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListCommonStackExtraArgsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let stacks =
resource::list_full_for_user::<Stack>(query, &user, &all_tags)
.await
.context("failed to get resources matching query")?;
let stacks = resource::list_full_for_user::<Stack>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();
@@ -158,21 +225,24 @@ impl Resolve<ListCommonStackExtraArgs, User> for State {
}
}
impl Resolve<ListCommonStackBuildExtraArgs, User> for State {
impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
async fn resolve(
&self,
ListCommonStackBuildExtraArgs { query }: ListCommonStackBuildExtraArgs,
user: User,
) -> anyhow::Result<ListCommonStackBuildExtraArgsResponse> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListCommonStackBuildExtraArgsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let stacks =
resource::list_full_for_user::<Stack>(query, &user, &all_tags)
.await
.context("failed to get resources matching query")?;
let stacks = resource::list_full_for_user::<Stack>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();
@@ -189,47 +259,73 @@ impl Resolve<ListCommonStackBuildExtraArgs, User> for State {
}
}
impl Resolve<ListStacks, User> for State {
impl Resolve<ReadArgs> for ListStacks {
async fn resolve(
&self,
ListStacks { query }: ListStacks,
user: User,
) -> anyhow::Result<Vec<StackListItem>> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<StackListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Stack>(query, &user, &all_tags).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<ListFullStacks, User> for State {
impl Resolve<ReadArgs> for ListFullStacks {
async fn resolve(
&self,
ListFullStacks { query }: ListFullStacks,
user: User,
) -> anyhow::Result<ListFullStacksResponse> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullStacksResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Stack>(query, &user, &all_tags)
.await
Ok(
resource::list_full_for_user::<Stack>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<GetStackActionState, User> for State {
impl Resolve<ReadArgs> for GetStackActionState {
async fn resolve(
&self,
GetStackActionState { stack }: GetStackActionState,
user: User,
) -> anyhow::Result<StackActionState> {
let stack = resource::get_check_permissions::<Stack>(
&stack,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<StackActionState> {
let stack = get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
@@ -242,15 +338,15 @@ impl Resolve<GetStackActionState, User> for State {
}
}
impl Resolve<GetStacksSummary, User> for State {
impl Resolve<ReadArgs> for GetStacksSummary {
async fn resolve(
&self,
GetStacksSummary {}: GetStacksSummary,
user: User,
) -> anyhow::Result<GetStacksSummaryResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetStacksSummaryResponse> {
let stacks = resource::list_full_for_user::<Stack>(
Default::default(),
&user,
user,
PermissionLevel::Read.into(),
&[],
)
.await
@@ -276,12 +372,11 @@ impl Resolve<GetStacksSummary, User> for State {
}
}
impl Resolve<GetStackWebhooksEnabled, User> for State {
impl Resolve<ReadArgs> for GetStackWebhooksEnabled {
async fn resolve(
&self,
GetStackWebhooksEnabled { stack }: GetStackWebhooksEnabled,
user: User,
) -> anyhow::Result<GetStackWebhooksEnabledResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetStackWebhooksEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetStackWebhooksEnabledResponse {
managed: false,
@@ -290,10 +385,10 @@ impl Resolve<GetStackWebhooksEnabled, User> for State {
});
};
let stack = resource::get_check_permissions::<Stack>(
&stack,
&user,
PermissionLevel::Read,
let stack = get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read.into(),
)
.await?;

View File

@@ -6,9 +6,7 @@ use komodo_client::{
permission::PermissionLevel,
sync::{
ResourceSync, ResourceSyncActionState, ResourceSyncListItem,
ResourceSyncState,
},
user::User,
},
};
use resolver_api::Resolve;
@@ -16,71 +14,82 @@ use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{
action_states, github_client, resource_sync_state_cache, State,
},
state::{action_states, github_client},
};
impl Resolve<GetResourceSync, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetResourceSync {
async fn resolve(
&self,
GetResourceSync { sync }: GetResourceSync,
user: User,
) -> anyhow::Result<ResourceSync> {
resource::get_check_permissions::<ResourceSync>(
&sync,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ResourceSync> {
Ok(
get_check_permissions::<ResourceSync>(
&self.sync,
user,
PermissionLevel::Read.into(),
)
.await?,
)
.await
}
}
impl Resolve<ListResourceSyncs, User> for State {
impl Resolve<ReadArgs> for ListResourceSyncs {
async fn resolve(
&self,
ListResourceSyncs { query }: ListResourceSyncs,
user: User,
) -> anyhow::Result<Vec<ResourceSyncListItem>> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<ResourceSyncListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<ResourceSync>(query, &user, &all_tags)
.await
Ok(
resource::list_for_user::<ResourceSync>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ListFullResourceSyncs, User> for State {
impl Resolve<ReadArgs> for ListFullResourceSyncs {
async fn resolve(
&self,
ListFullResourceSyncs { query }: ListFullResourceSyncs,
user: User,
) -> anyhow::Result<ListFullResourceSyncsResponse> {
let all_tags = if query.tags.is_empty() {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullResourceSyncsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<ResourceSync>(
query, &user, &all_tags,
Ok(
resource::list_full_for_user::<ResourceSync>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
.await
}
}
impl Resolve<GetResourceSyncActionState, User> for State {
impl Resolve<ReadArgs> for GetResourceSyncActionState {
async fn resolve(
&self,
GetResourceSyncActionState { sync }: GetResourceSyncActionState,
user: User,
) -> anyhow::Result<ResourceSyncActionState> {
let sync = resource::get_check_permissions::<ResourceSync>(
&sync,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ResourceSyncActionState> {
let sync = get_check_permissions::<ResourceSync>(
&self.sync,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
@@ -93,16 +102,16 @@ impl Resolve<GetResourceSyncActionState, User> for State {
}
}
impl Resolve<GetResourceSyncsSummary, User> for State {
impl Resolve<ReadArgs> for GetResourceSyncsSummary {
async fn resolve(
&self,
GetResourceSyncsSummary {}: GetResourceSyncsSummary,
user: User,
) -> anyhow::Result<GetResourceSyncsSummaryResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetResourceSyncsSummaryResponse> {
let resource_syncs =
resource::list_full_for_user::<ResourceSync>(
Default::default(),
&user,
user,
PermissionLevel::Read.into(),
&[],
)
.await
@@ -110,7 +119,6 @@ impl Resolve<GetResourceSyncsSummary, User> for State {
let mut res = GetResourceSyncsSummaryResponse::default();
let cache = resource_sync_state_cache();
let action_states = action_states();
for resource_sync in resource_syncs {
@@ -129,42 +137,29 @@ impl Resolve<GetResourceSyncsSummary, User> for State {
res.failed += 1;
continue;
}
match (
cache.get(&resource_sync.id).await.unwrap_or_default(),
action_states
.resource_sync
.get(&resource_sync.id)
.await
.unwrap_or_default()
.get()?,
) {
(_, action_states) if action_states.syncing => {
res.syncing += 1;
}
(ResourceSyncState::Ok, _) => res.ok += 1,
(ResourceSyncState::Failed, _) => res.failed += 1,
(ResourceSyncState::Unknown, _) => res.unknown += 1,
// will never come off the cache in the building state, since that comes from action states
(ResourceSyncState::Syncing, _) => {
unreachable!()
}
(ResourceSyncState::Pending, _) => {
unreachable!()
}
if action_states
.resource_sync
.get(&resource_sync.id)
.await
.unwrap_or_default()
.get()?
.syncing
{
res.syncing += 1;
continue;
}
res.ok += 1;
}
Ok(res)
}
}
impl Resolve<GetSyncWebhooksEnabled, User> for State {
impl Resolve<ReadArgs> for GetSyncWebhooksEnabled {
async fn resolve(
&self,
GetSyncWebhooksEnabled { sync }: GetSyncWebhooksEnabled,
user: User,
) -> anyhow::Result<GetSyncWebhooksEnabledResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetSyncWebhooksEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetSyncWebhooksEnabledResponse {
managed: false,
@@ -173,10 +168,10 @@ impl Resolve<GetSyncWebhooksEnabled, User> for State {
});
};
let sync = resource::get_check_permissions::<ResourceSync>(
&sync,
&user,
PermissionLevel::Read,
let sync = get_check_permissions::<ResourceSync>(
&self.sync,
user,
PermissionLevel::Read.into(),
)
.await?;

View File

@@ -1,39 +1,31 @@
use anyhow::Context;
use komodo_client::{
api::read::{GetTag, ListTags},
entities::{tag::Tag, user::User},
entities::tag::Tag,
};
use mongo_indexed::doc;
use mungos::{find::find_collect, mongodb::options::FindOptions};
use resolver_api::Resolve;
use crate::{
helpers::query::get_tag,
state::{db_client, State},
};
use crate::{helpers::query::get_tag, state::db_client};
impl Resolve<GetTag, User> for State {
async fn resolve(
&self,
GetTag { tag }: GetTag,
_: User,
) -> anyhow::Result<Tag> {
get_tag(&tag).await
use super::ReadArgs;
impl Resolve<ReadArgs> for GetTag {
async fn resolve(self, _: &ReadArgs) -> serror::Result<Tag> {
Ok(get_tag(&self.tag).await?)
}
}
impl Resolve<ListTags, User> for State {
async fn resolve(
&self,
ListTags { query }: ListTags,
_: User,
) -> anyhow::Result<Vec<Tag>> {
find_collect(
impl Resolve<ReadArgs> for ListTags {
async fn resolve(self, _: &ReadArgs) -> serror::Result<Vec<Tag>> {
let res = find_collect(
&db_client().tags,
query,
self.query,
FindOptions::builder().sort(doc! { "name": 1 }).build(),
)
.await
.context("failed to get tags from db")
.context("failed to get tags from db")?;
Ok(res)
}
}

View File

@@ -6,12 +6,11 @@ use komodo_client::{
ListUserGroups,
},
entities::{
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, permission::PermissionLevel,
procedure::Procedure, repo::Repo, resource::ResourceQuery,
server::Server, server_template::ServerTemplate, stack::Stack,
ResourceTarget, action::Action, alerter::Alerter, build::Build,
builder::Builder, deployment::Deployment,
permission::PermissionLevel, procedure::Procedure, repo::Repo,
resource::ResourceQuery, server::Server, stack::Stack,
sync::ResourceSync, toml::ResourcesToml, user::User,
ResourceTarget,
},
};
use mungos::find::find_collect;
@@ -21,187 +20,201 @@ use crate::{
helpers::query::{
get_all_tags, get_id_to_tags, get_user_user_group_ids,
},
permission::get_check_permissions,
resource,
state::{db_client, State},
state::db_client,
sync::{
toml::{convert_resource, ToToml, TOML_PRETTY_OPTIONS},
user_groups::convert_user_groups,
AllResourcesById,
toml::{ToToml, convert_resource},
user_groups::{convert_user_groups, user_group_to_toml},
variables::variable_to_toml,
},
};
impl Resolve<ExportAllResourcesToToml, User> for State {
async fn resolve(
&self,
ExportAllResourcesToToml { tags }: ExportAllResourcesToToml,
user: User,
) -> anyhow::Result<ExportAllResourcesToTomlResponse> {
let mut targets = Vec::<ResourceTarget>::new();
use super::ReadArgs;
let all_tags = if tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
targets.extend(
resource::list_for_user::<Alerter>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Alerter(resource.id)),
);
targets.extend(
resource::list_for_user::<Builder>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Builder(resource.id)),
);
targets.extend(
resource::list_for_user::<Server>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Server(resource.id)),
);
targets.extend(
resource::list_for_user::<Deployment>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Deployment(resource.id)),
);
targets.extend(
resource::list_for_user::<Stack>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Stack(resource.id)),
);
targets.extend(
resource::list_for_user::<Build>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Build(resource.id)),
);
targets.extend(
resource::list_for_user::<Repo>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Repo(resource.id)),
);
targets.extend(
resource::list_for_user::<Procedure>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Procedure(resource.id)),
);
targets.extend(
resource::list_for_user::<Action>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Action(resource.id)),
);
targets.extend(
resource::list_for_user::<ServerTemplate>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::ServerTemplate(resource.id)),
);
targets.extend(
resource::list_full_for_user::<ResourceSync>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
// These will already be filtered by [ExportResourcesToToml]
.map(|resource| ResourceTarget::ResourceSync(resource.id)),
);
let user_groups = if user.admin && tags.is_empty() {
find_collect(&db_client().user_groups, None, None)
.await
.context("failed to query db for user groups")?
.into_iter()
.map(|user_group| user_group.id)
.collect()
} else {
get_user_user_group_ids(&user.id).await?
};
self
.resolve(
ExportResourcesToToml {
targets,
user_groups,
include_variables: tags.is_empty(),
},
user,
)
.await
}
async fn get_all_targets(
tags: &[String],
user: &User,
) -> anyhow::Result<Vec<ResourceTarget>> {
let mut targets = Vec::<ResourceTarget>::new();
let all_tags = if tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
targets.extend(
resource::list_full_for_user::<Alerter>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Alerter(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Builder>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Builder(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Server>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Server(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Stack>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Stack(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Deployment>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Deployment(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Build>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Build(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Repo>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Repo(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Procedure>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Procedure(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Action>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Action(resource.id)),
);
targets.extend(
resource::list_full_for_user::<ResourceSync>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
// These will already be filtered by [ExportResourcesToToml]
.map(|resource| ResourceTarget::ResourceSync(resource.id)),
);
Ok(targets)
}
impl Resolve<ExportResourcesToToml, User> for State {
impl Resolve<ReadArgs> for ExportAllResourcesToToml {
async fn resolve(
&self,
self,
args: &ReadArgs,
) -> serror::Result<ExportAllResourcesToTomlResponse> {
let targets = if self.include_resources {
get_all_targets(&self.tags, &args.user).await?
} else {
Vec::new()
};
let user_groups = if self.include_user_groups {
if args.user.admin {
find_collect(&db_client().user_groups, None, None)
.await
.context("failed to query db for user groups")?
.into_iter()
.map(|user_group| user_group.id)
.collect()
} else {
get_user_user_group_ids(&args.user.id).await?
}
} else {
Vec::new()
};
ExportResourcesToToml {
targets,
user_groups,
include_variables: self.include_variables,
}
.resolve(args)
.await
}
}
impl Resolve<ReadArgs> for ExportResourcesToToml {
async fn resolve(
self,
args: &ReadArgs,
) -> serror::Result<ExportResourcesToTomlResponse> {
let ExportResourcesToToml {
targets,
user_groups,
include_variables,
}: ExportResourcesToToml,
user: User,
) -> anyhow::Result<ExportResourcesToTomlResponse> {
} = self;
let mut res = ResourcesToml::default();
let all = AllResourcesById::load().await?;
let id_to_tags = get_id_to_tags(None).await?;
let ReadArgs { user } = args;
for target in targets {
match target {
ResourceTarget::Alerter(id) => {
let alerter = resource::get_check_permissions::<Alerter>(
let mut alerter = get_check_permissions::<Alerter>(
&id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
Alerter::replace_ids(&mut alerter);
res.alerters.push(convert_resource::<Alerter>(
alerter,
false,
@@ -210,16 +223,18 @@ impl Resolve<ExportResourcesToToml, User> for State {
))
}
ResourceTarget::ResourceSync(id) => {
let sync = resource::get_check_permissions::<ResourceSync>(
let mut sync = get_check_permissions::<ResourceSync>(
&id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
if sync.config.file_contents.is_empty()
&& (sync.config.files_on_host
|| !sync.config.repo.is_empty())
|| !sync.config.repo.is_empty()
|| !sync.config.linked_repo.is_empty())
{
ResourceSync::replace_ids(&mut sync);
res.resource_syncs.push(convert_resource::<ResourceSync>(
sync,
false,
@@ -228,29 +243,14 @@ impl Resolve<ExportResourcesToToml, User> for State {
))
}
}
ResourceTarget::ServerTemplate(id) => {
let template = resource::get_check_permissions::<
ServerTemplate,
>(
&id, &user, PermissionLevel::Read
)
.await?;
res.server_templates.push(
convert_resource::<ServerTemplate>(
template,
false,
vec![],
&id_to_tags,
),
)
}
ResourceTarget::Server(id) => {
let server = resource::get_check_permissions::<Server>(
let mut server = get_check_permissions::<Server>(
&id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
Server::replace_ids(&mut server);
res.servers.push(convert_resource::<Server>(
server,
false,
@@ -259,14 +259,13 @@ impl Resolve<ExportResourcesToToml, User> for State {
))
}
ResourceTarget::Builder(id) => {
let mut builder =
resource::get_check_permissions::<Builder>(
&id,
&user,
PermissionLevel::Read,
)
.await?;
Builder::replace_ids(&mut builder, &all);
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,
@@ -275,13 +274,13 @@ impl Resolve<ExportResourcesToToml, User> for State {
))
}
ResourceTarget::Build(id) => {
let mut build = resource::get_check_permissions::<Build>(
let mut build = get_check_permissions::<Build>(
&id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
Build::replace_ids(&mut build, &all);
Build::replace_ids(&mut build);
res.builds.push(convert_resource::<Build>(
build,
false,
@@ -290,13 +289,13 @@ impl Resolve<ExportResourcesToToml, User> for State {
))
}
ResourceTarget::Deployment(id) => {
let mut deployment = resource::get_check_permissions::<
Deployment,
>(
&id, &user, PermissionLevel::Read
let mut deployment = get_check_permissions::<Deployment>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Deployment::replace_ids(&mut deployment, &all);
Deployment::replace_ids(&mut deployment);
res.deployments.push(convert_resource::<Deployment>(
deployment,
false,
@@ -305,13 +304,13 @@ impl Resolve<ExportResourcesToToml, User> for State {
))
}
ResourceTarget::Repo(id) => {
let mut repo = resource::get_check_permissions::<Repo>(
let mut repo = get_check_permissions::<Repo>(
&id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
Repo::replace_ids(&mut repo, &all);
Repo::replace_ids(&mut repo);
res.repos.push(convert_resource::<Repo>(
repo,
false,
@@ -320,13 +319,13 @@ impl Resolve<ExportResourcesToToml, User> for State {
))
}
ResourceTarget::Stack(id) => {
let mut stack = resource::get_check_permissions::<Stack>(
let mut stack = get_check_permissions::<Stack>(
&id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
Stack::replace_ids(&mut stack, &all);
Stack::replace_ids(&mut stack);
res.stacks.push(convert_resource::<Stack>(
stack,
false,
@@ -335,13 +334,13 @@ impl Resolve<ExportResourcesToToml, User> for State {
))
}
ResourceTarget::Procedure(id) => {
let mut procedure = resource::get_check_permissions::<
Procedure,
>(
&id, &user, PermissionLevel::Read
let mut procedure = get_check_permissions::<Procedure>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Procedure::replace_ids(&mut procedure, &all);
Procedure::replace_ids(&mut procedure);
res.procedures.push(convert_resource::<Procedure>(
procedure,
false,
@@ -350,13 +349,13 @@ impl Resolve<ExportResourcesToToml, User> for State {
));
}
ResourceTarget::Action(id) => {
let mut action = resource::get_check_permissions::<Action>(
let mut action = get_check_permissions::<Action>(
&id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
Action::replace_ids(&mut action, &all);
Action::replace_ids(&mut action);
res.actions.push(convert_resource::<Action>(
action,
false,
@@ -368,7 +367,7 @@ impl Resolve<ExportResourcesToToml, User> for State {
};
}
add_user_groups(user_groups, &mut res, &all, &user)
add_user_groups(user_groups, &mut res, args)
.await
.context("failed to add user groups")?;
@@ -397,18 +396,18 @@ impl Resolve<ExportResourcesToToml, User> for State {
async fn add_user_groups(
user_groups: Vec<String>,
res: &mut ResourcesToml,
all: &AllResourcesById,
user: &User,
args: &ReadArgs,
) -> anyhow::Result<()> {
let user_groups = State
.resolve(ListUserGroups {}, user.clone())
.await?
let user_groups = ListUserGroups {}
.resolve(args)
.await
.map_err(|e| e.error)?
.into_iter()
.filter(|ug| {
user_groups.contains(&ug.name) || user_groups.contains(&ug.id)
});
let mut ug = Vec::with_capacity(user_groups.size_hint().0);
convert_user_groups(user_groups, all, &mut ug).await?;
convert_user_groups(user_groups, &mut ug).await?;
res.user_groups = ug.into_iter().map(|ug| ug.1).collect();
Ok(())
@@ -491,14 +490,6 @@ fn serialize_resources_toml(
Builder::push_to_toml_string(builder, &mut toml)?;
}
for server_template in resources.server_templates {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[server_template]]\n");
ServerTemplate::push_to_toml_string(server_template, &mut toml)?;
}
for resource_sync in resources.resource_syncs {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
@@ -511,22 +502,14 @@ fn serialize_resources_toml(
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[variable]]\n");
toml.push_str(
&toml_pretty::to_string(variable, TOML_PRETTY_OPTIONS)
.context("failed to serialize variables to toml")?,
);
toml.push_str(&variable_to_toml(variable)?);
}
for user_group in &resources.user_groups {
for user_group in resources.user_groups {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[user_group]]\n");
toml.push_str(
&toml_pretty::to_string(user_group, TOML_PRETTY_OPTIONS)
.context("failed to serialize user_groups to toml")?,
);
toml.push_str(&user_group_to_toml(user_group)?);
}
Ok(toml)

View File

@@ -1,9 +1,10 @@
use std::collections::HashMap;
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use komodo_client::{
api::read::{GetUpdate, ListUpdates, ListUpdatesResponse},
entities::{
ResourceTarget,
action::Action,
alerter::Alerter,
build::Build,
@@ -13,12 +14,10 @@ use komodo_client::{
procedure::Procedure,
repo::Repo,
server::Server,
server_template::ServerTemplate,
stack::Stack,
sync::ResourceSync,
update::{Update, UpdateListItem},
user::User,
ResourceTarget,
},
};
use mungos::{
@@ -30,33 +29,33 @@ use resolver_api::Resolve;
use crate::{
config::core_config,
resource,
state::{db_client, State},
permission::{get_check_permissions, get_resource_ids_for_user},
state::db_client,
};
use super::ReadArgs;
const UPDATES_PER_PAGE: i64 = 100;
impl Resolve<ListUpdates, User> for State {
impl Resolve<ReadArgs> for ListUpdates {
async fn resolve(
&self,
ListUpdates { query, page }: ListUpdates,
user: User,
) -> anyhow::Result<ListUpdatesResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListUpdatesResponse> {
let query = if user.admin || core_config().transparent_mode {
query
self.query
} else {
let server_query =
resource::get_resource_ids_for_user::<Server>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Server", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Server" });
let server_query = get_resource_ids_for_user::<Server>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Server", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Server" });
let deployment_query =
resource::get_resource_ids_for_user::<Deployment>(&user)
get_resource_ids_for_user::<Deployment>(user)
.await?
.map(|ids| {
doc! {
@@ -65,38 +64,35 @@ impl Resolve<ListUpdates, User> for State {
})
.unwrap_or_else(|| doc! { "target.type": "Deployment" });
let stack_query =
resource::get_resource_ids_for_user::<Stack>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Stack", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Stack" });
let stack_query = get_resource_ids_for_user::<Stack>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Stack", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Stack" });
let build_query =
resource::get_resource_ids_for_user::<Build>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Build", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Build" });
let build_query = get_resource_ids_for_user::<Build>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Build", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Build" });
let repo_query =
resource::get_resource_ids_for_user::<Repo>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Repo", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Repo" });
let repo_query = get_resource_ids_for_user::<Repo>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Repo", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Repo" });
let procedure_query =
resource::get_resource_ids_for_user::<Procedure>(&user)
get_resource_ids_for_user::<Procedure>(user)
.await?
.map(|ids| {
doc! {
@@ -105,59 +101,45 @@ impl Resolve<ListUpdates, User> for State {
})
.unwrap_or_else(|| doc! { "target.type": "Procedure" });
let action_query =
resource::get_resource_ids_for_user::<Action>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Action", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Action" });
let builder_query =
resource::get_resource_ids_for_user::<Builder>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Builder", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Builder" });
let alerter_query =
resource::get_resource_ids_for_user::<Alerter>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Alerter", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Alerter" });
let server_template_query =
resource::get_resource_ids_for_user::<ServerTemplate>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "ServerTemplate", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ServerTemplate" });
let resource_sync_query =
resource::get_resource_ids_for_user::<ResourceSync>(
&user,
)
let action_query = get_resource_ids_for_user::<Action>(user)
.await?
.map(|ids| {
doc! {
"target.type": "ResourceSync", "target.id": { "$in": ids }
"target.type": "Action", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ResourceSync" });
.unwrap_or_else(|| doc! { "target.type": "Action" });
let mut query = query.unwrap_or_default();
let builder_query = get_resource_ids_for_user::<Builder>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Builder", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Builder" });
let alerter_query = get_resource_ids_for_user::<Alerter>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Alerter", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Alerter" });
let resource_sync_query = get_resource_ids_for_user::<
ResourceSync,
>(user)
.await?
.map(|ids| {
doc! {
"target.type": "ResourceSync", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ResourceSync" });
let mut query = self.query.unwrap_or_default();
query.extend(doc! {
"$or": [
server_query,
@@ -169,7 +151,6 @@ impl Resolve<ListUpdates, User> for State {
action_query,
alerter_query,
builder_query,
server_template_query,
resource_sync_query,
]
});
@@ -188,7 +169,7 @@ impl Resolve<ListUpdates, User> for State {
query,
FindOptions::builder()
.sort(doc! { "start_ts": -1 })
.skip(page as u64 * UPDATES_PER_PAGE as u64)
.skip(self.page as u64 * UPDATES_PER_PAGE as u64)
.limit(UPDATES_PER_PAGE)
.build(),
)
@@ -220,7 +201,7 @@ impl Resolve<ListUpdates, User> for State {
.collect::<Vec<_>>();
let next_page = if updates.len() == UPDATES_PER_PAGE as usize {
Some(page + 1)
Some(self.page + 1)
} else {
None
};
@@ -229,13 +210,12 @@ impl Resolve<ListUpdates, User> for State {
}
}
impl Resolve<GetUpdate, User> for State {
impl Resolve<ReadArgs> for GetUpdate {
async fn resolve(
&self,
GetUpdate { id }: GetUpdate,
user: User,
) -> anyhow::Result<Update> {
let update = find_one_by_id(&db_client().updates, &id)
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Update> {
let update = find_one_by_id(&db_client().updates, &self.id)
.await
.context("failed to query to db")?
.context("no update exists with given id")?;
@@ -244,95 +224,87 @@ impl Resolve<GetUpdate, User> for State {
}
match &update.target {
ResourceTarget::System(_) => {
return Err(anyhow!(
"user must be admin to view system updates"
))
return Err(
anyhow!("user must be admin to view system updates").into(),
);
}
ResourceTarget::Server(id) => {
resource::get_check_permissions::<Server>(
get_check_permissions::<Server>(
id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Deployment(id) => {
resource::get_check_permissions::<Deployment>(
get_check_permissions::<Deployment>(
id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Build(id) => {
resource::get_check_permissions::<Build>(
get_check_permissions::<Build>(
id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Repo(id) => {
resource::get_check_permissions::<Repo>(
get_check_permissions::<Repo>(
id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Builder(id) => {
resource::get_check_permissions::<Builder>(
get_check_permissions::<Builder>(
id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Alerter(id) => {
resource::get_check_permissions::<Alerter>(
get_check_permissions::<Alerter>(
id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Procedure(id) => {
resource::get_check_permissions::<Procedure>(
get_check_permissions::<Procedure>(
id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Action(id) => {
resource::get_check_permissions::<Action>(
get_check_permissions::<Action>(
id,
&user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::ServerTemplate(id) => {
resource::get_check_permissions::<ServerTemplate>(
id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::ResourceSync(id) => {
resource::get_check_permissions::<ResourceSync>(
get_check_permissions::<ResourceSync>(
id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Stack(id) => {
resource::get_check_permissions::<Stack>(
get_check_permissions::<Stack>(
id,
&user,
PermissionLevel::Read,
user,
PermissionLevel::Read.into(),
)
.await?;
}

View File

@@ -1,4 +1,4 @@
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use komodo_client::{
api::read::{
FindUser, FindUserResponse, GetUsername, GetUsernameResponse,
@@ -6,7 +6,7 @@ use komodo_client::{
ListApiKeysForServiceUserResponse, ListApiKeysResponse,
ListUsers, ListUsersResponse,
},
entities::user::{admin_service_user, User, UserConfig},
entities::user::{UserConfig, admin_service_user},
};
use mungos::{
by_id::find_one_by_id,
@@ -15,25 +15,23 @@ use mungos::{
};
use resolver_api::Resolve;
use crate::{
helpers::query::get_user,
state::{db_client, State},
};
use crate::{helpers::query::get_user, state::db_client};
impl Resolve<GetUsername, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetUsername {
async fn resolve(
&self,
GetUsername { user_id }: GetUsername,
_: User,
) -> anyhow::Result<GetUsernameResponse> {
if let Some(user) = admin_service_user(&user_id) {
self,
_: &ReadArgs,
) -> serror::Result<GetUsernameResponse> {
if let Some(user) = admin_service_user(&self.user_id) {
return Ok(GetUsernameResponse {
username: user.username,
avatar: None,
});
}
let user = find_one_by_id(&db_client().users, &user_id)
let user = find_one_by_id(&db_client().users, &self.user_id)
.await
.context("failed at mongo query for user")?
.context("no user found with id")?;
@@ -51,27 +49,27 @@ impl Resolve<GetUsername, User> for State {
}
}
impl Resolve<FindUser, User> for State {
impl Resolve<ReadArgs> for FindUser {
async fn resolve(
&self,
FindUser { user }: FindUser,
admin: User,
) -> anyhow::Result<FindUserResponse> {
self,
ReadArgs { user: admin }: &ReadArgs,
) -> serror::Result<FindUserResponse> {
if !admin.admin {
return Err(anyhow!("This method is admin only."));
return Err(anyhow!("This method is admin only.").into());
}
get_user(&user).await
Ok(get_user(&self.user).await?)
}
}
impl Resolve<ListUsers, User> for State {
impl Resolve<ReadArgs> for ListUsers {
async fn resolve(
&self,
ListUsers {}: ListUsers,
user: User,
) -> anyhow::Result<ListUsersResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListUsersResponse> {
if !user.admin {
return Err(anyhow!("this route is only accessable by admins"));
return Err(
anyhow!("this route is only accessable by admins").into(),
);
}
let mut users = find_collect(
&db_client().users,
@@ -85,12 +83,11 @@ impl Resolve<ListUsers, User> for State {
}
}
impl Resolve<ListApiKeys, User> for State {
impl Resolve<ReadArgs> for ListApiKeys {
async fn resolve(
&self,
ListApiKeys {}: ListApiKeys,
user: User,
) -> anyhow::Result<ListApiKeysResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListApiKeysResponse> {
let api_keys = find_collect(
&db_client().api_keys,
doc! { "user_id": &user.id },
@@ -108,20 +105,19 @@ impl Resolve<ListApiKeys, User> for State {
}
}
impl Resolve<ListApiKeysForServiceUser, User> for State {
impl Resolve<ReadArgs> for ListApiKeysForServiceUser {
async fn resolve(
&self,
ListApiKeysForServiceUser { user }: ListApiKeysForServiceUser,
admin: User,
) -> anyhow::Result<ListApiKeysForServiceUserResponse> {
self,
ReadArgs { user: admin }: &ReadArgs,
) -> serror::Result<ListApiKeysForServiceUserResponse> {
if !admin.admin {
return Err(anyhow!("This method is admin only."));
return Err(anyhow!("This method is admin only.").into());
}
let user = get_user(&user).await?;
let user = get_user(&self.user).await?;
let UserConfig::Service { .. } = user.config else {
return Err(anyhow!("Given user is not service user"));
return Err(anyhow!("Given user is not service user").into());
};
let api_keys = find_collect(
&db_client().api_keys,

View File

@@ -1,64 +1,60 @@
use std::str::FromStr;
use anyhow::Context;
use komodo_client::{
api::read::{
GetUserGroup, GetUserGroupResponse, ListUserGroups,
ListUserGroupsResponse,
},
entities::user::User,
};
use komodo_client::api::read::*;
use mungos::{
find::find_collect,
mongodb::{
bson::{doc, oid::ObjectId, Document},
bson::{Document, doc, oid::ObjectId},
options::FindOptions,
},
};
use resolver_api::Resolve;
use crate::state::{db_client, State};
use crate::state::db_client;
impl Resolve<GetUserGroup, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetUserGroup {
async fn resolve(
&self,
GetUserGroup { user_group }: GetUserGroup,
user: User,
) -> anyhow::Result<GetUserGroupResponse> {
let mut filter = match ObjectId::from_str(&user_group) {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetUserGroupResponse> {
let mut filter = match ObjectId::from_str(&self.user_group) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": &user_group },
Err(_) => doc! { "name": &self.user_group },
};
// Don't allow non admin users to get UserGroups they aren't a part of.
if !user.admin {
// Filter for only UserGroups which contain the users id
filter.insert("users", &user.id);
}
db_client()
let res = db_client()
.user_groups
.find_one(filter)
.await
.context("failed to query db for user groups")?
.context("no UserGroup found with given name or id")
.context("no UserGroup found with given name or id")?;
Ok(res)
}
}
impl Resolve<ListUserGroups, User> for State {
impl Resolve<ReadArgs> for ListUserGroups {
async fn resolve(
&self,
ListUserGroups {}: ListUserGroups,
user: User,
) -> anyhow::Result<ListUserGroupsResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListUserGroupsResponse> {
let mut filter = Document::new();
if !user.admin {
filter.insert("users", &user.id);
}
find_collect(
let res = find_collect(
&db_client().user_groups,
filter,
FindOptions::builder().sort(doc! { "name": 1 }).build(),
)
.await
.context("failed to query db for UserGroups")
.context("failed to query db for UserGroups")?;
Ok(res)
}
}

View File

@@ -1,27 +1,19 @@
use anyhow::Context;
use komodo_client::{
api::read::{
GetVariable, GetVariableResponse, ListVariables,
ListVariablesResponse,
},
entities::user::User,
};
use komodo_client::api::read::*;
use mongo_indexed::doc;
use mungos::{find::find_collect, mongodb::options::FindOptions};
use resolver_api::Resolve;
use crate::{
helpers::query::get_variable,
state::{db_client, State},
};
use crate::{helpers::query::get_variable, state::db_client};
impl Resolve<GetVariable, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetVariable {
async fn resolve(
&self,
GetVariable { name }: GetVariable,
user: User,
) -> anyhow::Result<GetVariableResponse> {
let mut variable = get_variable(&name).await?;
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetVariableResponse> {
let mut variable = get_variable(&self.name).await?;
if !variable.is_secret || user.admin {
return Ok(variable);
}
@@ -30,12 +22,11 @@ impl Resolve<GetVariable, User> for State {
}
}
impl Resolve<ListVariables, User> for State {
impl Resolve<ReadArgs> for ListVariables {
async fn resolve(
&self,
ListVariables {}: ListVariables,
user: User,
) -> anyhow::Result<ListVariablesResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListVariablesResponse> {
let variables = find_collect(
&db_client().variables,
None,

View File

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

View File

@@ -1,34 +1,42 @@
use std::{collections::VecDeque, time::Instant};
use anyhow::{anyhow, Context};
use axum::{middleware, routing::post, Extension, Json, Router};
use axum_extra::{headers::ContentType, TypedHeader};
use anyhow::{Context, anyhow};
use axum::{
Extension, Json, Router, extract::Path, middleware, routing::post,
};
use derive_variants::EnumVariants;
use komodo_client::{
api::user::{
CreateApiKey, CreateApiKeyResponse, DeleteApiKey,
DeleteApiKeyResponse, PushRecentlyViewed,
PushRecentlyViewedResponse, SetLastSeenUpdate,
SetLastSeenUpdateResponse,
},
api::user::*,
entities::{api_key::ApiKey, komodo_timestamp, user::User},
};
use mongo_indexed::doc;
use mungos::{by_id::update_one_by_id, mongodb::bson::to_bson};
use resolver_api::{derive::Resolver, Resolve, Resolver};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serde_json::json;
use typeshare::typeshare;
use uuid::Uuid;
use crate::{
auth::auth_request,
helpers::{query::get_user, random_string},
state::{db_client, State},
state::db_client,
};
use super::Variant;
pub struct UserArgs {
pub user: User,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
#[resolver_target(State)]
#[resolver_args(User)]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
)]
#[args(UserArgs)]
#[response(Response)]
#[error(serror::Error)]
#[serde(tag = "type", content = "params")]
enum UserRequest {
PushRecentlyViewed(PushRecentlyViewed),
@@ -40,54 +48,57 @@ enum UserRequest {
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler))
.layer(middleware::from_fn(auth_request))
}
async fn variant_handler(
user: Extension<User>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<axum::response::Response> {
let req: UserRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
}))?;
handler(user, Json(req)).await
}
#[instrument(name = "UserHandler", level = "debug", skip(user))]
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<UserRequest>,
) -> serror::Result<(TypedHeader<ContentType>, String)> {
) -> serror::Result<axum::response::Response> {
let timer = Instant::now();
let req_id = Uuid::new_v4();
debug!(
"/user request {req_id} | user: {} ({})",
user.username, user.id
);
let res =
State
.resolve_request(request, user)
.await
.map_err(|e| match e {
resolver_api::Error::Serialization(e) => {
anyhow!("{e:?}").context("response serialization error")
}
resolver_api::Error::Inner(e) => e,
});
let res = request.resolve(&UserArgs { user }).await;
if let Err(e) = &res {
warn!("/user request {req_id} error: {e:#}");
warn!("/user request {req_id} error: {:#}", e.error);
}
let elapsed = timer.elapsed();
debug!("/user request {req_id} | resolve time: {elapsed:?}");
Ok((TypedHeader(ContentType::json()), res?))
res.map(|res| res.0)
}
const RECENTLY_VIEWED_MAX: usize = 10;
impl Resolve<PushRecentlyViewed, User> for State {
impl Resolve<UserArgs> for PushRecentlyViewed {
#[instrument(
name = "PushRecentlyViewed",
level = "debug",
skip(self, user)
skip(user)
)]
async fn resolve(
&self,
PushRecentlyViewed { resource }: PushRecentlyViewed,
user: User,
) -> anyhow::Result<PushRecentlyViewedResponse> {
self,
UserArgs { user }: &UserArgs,
) -> serror::Result<PushRecentlyViewedResponse> {
let user = get_user(&user.id).await?;
let (resource_type, id) = resource.extract_variant_id();
let (resource_type, id) = self.resource.extract_variant_id();
let update = match user.recents.get(&resource_type) {
Some(recents) => {
let mut recents = recents
@@ -117,17 +128,16 @@ impl Resolve<PushRecentlyViewed, User> for State {
}
}
impl Resolve<SetLastSeenUpdate, User> for State {
impl Resolve<UserArgs> for SetLastSeenUpdate {
#[instrument(
name = "SetLastSeenUpdate",
level = "debug",
skip(self, user)
skip(user)
)]
async fn resolve(
&self,
SetLastSeenUpdate {}: SetLastSeenUpdate,
user: User,
) -> anyhow::Result<SetLastSeenUpdateResponse> {
self,
UserArgs { user }: &UserArgs,
) -> serror::Result<SetLastSeenUpdateResponse> {
update_one_by_id(
&db_client().users,
&user.id,
@@ -145,17 +155,12 @@ impl Resolve<SetLastSeenUpdate, User> for State {
const SECRET_LENGTH: usize = 40;
const BCRYPT_COST: u32 = 10;
impl Resolve<CreateApiKey, User> for State {
#[instrument(
name = "CreateApiKey",
level = "debug",
skip(self, user)
)]
impl Resolve<UserArgs> for CreateApiKey {
#[instrument(name = "CreateApiKey", level = "debug", skip(user))]
async fn resolve(
&self,
CreateApiKey { name, expires }: CreateApiKey,
user: User,
) -> anyhow::Result<CreateApiKeyResponse> {
self,
UserArgs { user }: &UserArgs,
) -> serror::Result<CreateApiKeyResponse> {
let user = get_user(&user.id).await?;
let key = format!("K-{}", random_string(SECRET_LENGTH));
@@ -164,12 +169,12 @@ impl Resolve<CreateApiKey, User> for State {
.context("failed at hashing secret string")?;
let api_key = ApiKey {
name,
name: self.name,
key: key.clone(),
secret: secret_hash,
user_id: user.id.clone(),
created_at: komodo_timestamp(),
expires,
expires: self.expires,
};
db_client()
.api_keys
@@ -180,26 +185,21 @@ impl Resolve<CreateApiKey, User> for State {
}
}
impl Resolve<DeleteApiKey, User> for State {
#[instrument(
name = "DeleteApiKey",
level = "debug",
skip(self, user)
)]
impl Resolve<UserArgs> for DeleteApiKey {
#[instrument(name = "DeleteApiKey", level = "debug", skip(user))]
async fn resolve(
&self,
DeleteApiKey { key }: DeleteApiKey,
user: User,
) -> anyhow::Result<DeleteApiKeyResponse> {
self,
UserArgs { user }: &UserArgs,
) -> serror::Result<DeleteApiKeyResponse> {
let client = db_client();
let key = client
.api_keys
.find_one(doc! { "key": &key })
.find_one(doc! { "key": &self.key })
.await
.context("failed at db query")?
.context("no api key with key found")?;
if user.id != key.user_id {
return Err(anyhow!("api key does not belong to user"));
return Err(anyhow!("api key does not belong to user").into());
}
client
.api_keys

View File

@@ -2,70 +2,69 @@ use komodo_client::{
api::write::*,
entities::{
action::Action, permission::PermissionLevel, update::Update,
user::User,
},
};
use resolver_api::Resolve;
use crate::{resource, state::State};
use crate::{permission::get_check_permissions, resource};
impl Resolve<CreateAction, User> for State {
#[instrument(name = "CreateAction", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateAction {
#[instrument(name = "CreateAction", skip(user))]
async fn resolve(
&self,
CreateAction { name, config }: CreateAction,
user: User,
) -> anyhow::Result<Action> {
resource::create::<Action>(&name, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Action> {
Ok(
resource::create::<Action>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<CopyAction, User> for State {
#[instrument(name = "CopyAction", skip(self, user))]
impl Resolve<WriteArgs> for CopyAction {
#[instrument(name = "CopyAction", skip(user))]
async fn resolve(
&self,
CopyAction { name, id }: CopyAction,
user: User,
) -> anyhow::Result<Action> {
let Action { config, .. } = resource::get_check_permissions::<
Action,
>(
&id, &user, PermissionLevel::Write
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Action> {
let Action { config, .. } = get_check_permissions::<Action>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::create::<Action>(&name, config.into(), &user).await
Ok(
resource::create::<Action>(&self.name, config.into(), user)
.await?,
)
}
}
impl Resolve<UpdateAction, User> for State {
#[instrument(name = "UpdateAction", skip(self, user))]
impl Resolve<WriteArgs> for UpdateAction {
#[instrument(name = "UpdateAction", skip(user))]
async fn resolve(
&self,
UpdateAction { id, config }: UpdateAction,
user: User,
) -> anyhow::Result<Action> {
resource::update::<Action>(&id, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Action> {
Ok(resource::update::<Action>(&self.id, self.config, user).await?)
}
}
impl Resolve<RenameAction, User> for State {
#[instrument(name = "RenameAction", skip(self, user))]
impl Resolve<WriteArgs> for RenameAction {
#[instrument(name = "RenameAction", skip(user))]
async fn resolve(
&self,
RenameAction { id, name }: RenameAction,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Action>(&id, &name, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Action>(&self.id, &self.name, user).await?)
}
}
impl Resolve<DeleteAction, User> for State {
#[instrument(name = "DeleteAction", skip(self, user))]
async fn resolve(
&self,
DeleteAction { id }: DeleteAction,
user: User,
) -> anyhow::Result<Action> {
resource::delete::<Action>(&id, &user).await
impl Resolve<WriteArgs> for DeleteAction {
#[instrument(name = "DeleteAction", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Action> {
Ok(resource::delete::<Action>(&self.id, args).await?)
}
}

View File

@@ -2,70 +2,75 @@ use komodo_client::{
api::write::*,
entities::{
alerter::Alerter, permission::PermissionLevel, update::Update,
user::User,
},
};
use resolver_api::Resolve;
use crate::{resource, state::State};
use crate::{permission::get_check_permissions, resource};
impl Resolve<CreateAlerter, User> for State {
#[instrument(name = "CreateAlerter", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateAlerter {
#[instrument(name = "CreateAlerter", skip(user))]
async fn resolve(
&self,
CreateAlerter { name, config }: CreateAlerter,
user: User,
) -> anyhow::Result<Alerter> {
resource::create::<Alerter>(&name, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Alerter> {
Ok(
resource::create::<Alerter>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<CopyAlerter, User> for State {
#[instrument(name = "CopyAlerter", skip(self, user))]
impl Resolve<WriteArgs> for CopyAlerter {
#[instrument(name = "CopyAlerter", skip(user))]
async fn resolve(
&self,
CopyAlerter { name, id }: CopyAlerter,
user: User,
) -> anyhow::Result<Alerter> {
let Alerter { config, .. } = resource::get_check_permissions::<
Alerter,
>(
&id, &user, PermissionLevel::Write
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Alerter> {
let Alerter { config, .. } = get_check_permissions::<Alerter>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::create::<Alerter>(&name, config.into(), &user).await
Ok(
resource::create::<Alerter>(&self.name, config.into(), user)
.await?,
)
}
}
impl Resolve<DeleteAlerter, User> for State {
#[instrument(name = "DeleteAlerter", skip(self, user))]
impl Resolve<WriteArgs> for DeleteAlerter {
#[instrument(name = "DeleteAlerter", skip(args))]
async fn resolve(
&self,
DeleteAlerter { id }: DeleteAlerter,
user: User,
) -> anyhow::Result<Alerter> {
resource::delete::<Alerter>(&id, &user).await
self,
args: &WriteArgs,
) -> serror::Result<Alerter> {
Ok(resource::delete::<Alerter>(&self.id, args).await?)
}
}
impl Resolve<UpdateAlerter, User> for State {
#[instrument(name = "UpdateAlerter", skip(self, user))]
impl Resolve<WriteArgs> for UpdateAlerter {
#[instrument(name = "UpdateAlerter", skip(user))]
async fn resolve(
&self,
UpdateAlerter { id, config }: UpdateAlerter,
user: User,
) -> anyhow::Result<Alerter> {
resource::update::<Alerter>(&id, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Alerter> {
Ok(
resource::update::<Alerter>(&self.id, self.config, user)
.await?,
)
}
}
impl Resolve<RenameAlerter, User> for State {
#[instrument(name = "RenameAlerter", skip(self, user))]
impl Resolve<WriteArgs> for RenameAlerter {
#[instrument(name = "RenameAlerter", skip(user))]
async fn resolve(
&self,
RenameAlerter { id, name }: RenameAlerter,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Alerter>(&id, &name, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Alerter>(&self.id, &self.name, user).await?)
}
}

View File

@@ -1,14 +1,19 @@
use anyhow::{anyhow, Context};
use std::{path::PathBuf, str::FromStr, time::Duration};
use anyhow::{Context, anyhow};
use formatting::format_serror;
use git::GitRes;
use komodo_client::{
api::write::*,
entities::{
CloneArgs, FileContents, NoData, Operation, all_logs_success,
build::{Build, BuildInfo, PartialBuildConfig},
builder::{Builder, BuilderConfig},
config::core::CoreConfig,
permission::PermissionLevel,
repo::Repo,
server::ServerState,
update::Update,
user::User,
CloneArgs, NoData,
},
};
use mongo_indexed::doc;
@@ -16,148 +21,383 @@ use mungos::mongodb::bson::to_document;
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
};
use periphery_client::{
PeripheryClient,
api::build::{
GetDockerfileContentsOnHost, WriteDockerfileContentsToHost,
},
};
use resolver_api::Resolve;
use tokio::fs;
use crate::{
config::core_config,
helpers::git_token,
helpers::{
git_token, periphery_client,
query::get_server_with_state,
update::{add_update, make_update},
},
permission::get_check_permissions,
resource,
state::{db_client, github_client, State},
state::{db_client, github_client},
};
impl Resolve<CreateBuild, User> for State {
#[instrument(name = "CreateBuild", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateBuild {
#[instrument(name = "CreateBuild", skip(user))]
async fn resolve(
&self,
CreateBuild { name, config }: CreateBuild,
user: User,
) -> anyhow::Result<Build> {
resource::create::<Build>(&name, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Build> {
Ok(
resource::create::<Build>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<CopyBuild, User> for State {
#[instrument(name = "CopyBuild", skip(self, user))]
impl Resolve<WriteArgs> for CopyBuild {
#[instrument(name = "CopyBuild", skip(user))]
async fn resolve(
&self,
CopyBuild { name, id }: CopyBuild,
user: User,
) -> anyhow::Result<Build> {
let Build { mut config, .. } =
resource::get_check_permissions::<Build>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Build> {
let Build { mut config, .. } = get_check_permissions::<Build>(
&self.id,
user,
PermissionLevel::Read.into(),
)
.await?;
// reset version to 0.0.0
config.version = Default::default();
resource::create::<Build>(&name, config.into(), &user).await
Ok(
resource::create::<Build>(&self.name, config.into(), user)
.await?,
)
}
}
impl Resolve<DeleteBuild, User> for State {
#[instrument(name = "DeleteBuild", skip(self, user))]
async fn resolve(
&self,
DeleteBuild { id }: DeleteBuild,
user: User,
) -> anyhow::Result<Build> {
resource::delete::<Build>(&id, &user).await
impl Resolve<WriteArgs> for DeleteBuild {
#[instrument(name = "DeleteBuild", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Build> {
Ok(resource::delete::<Build>(&self.id, args).await?)
}
}
impl Resolve<UpdateBuild, User> for State {
#[instrument(name = "UpdateBuild", skip(self, user))]
impl Resolve<WriteArgs> for UpdateBuild {
#[instrument(name = "UpdateBuild", skip(user))]
async fn resolve(
&self,
UpdateBuild { id, config }: UpdateBuild,
user: User,
) -> anyhow::Result<Build> {
resource::update::<Build>(&id, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Build> {
Ok(resource::update::<Build>(&self.id, self.config, user).await?)
}
}
impl Resolve<RenameBuild, User> for State {
#[instrument(name = "RenameBuild", skip(self, user))]
impl Resolve<WriteArgs> for RenameBuild {
#[instrument(name = "RenameBuild", skip(user))]
async fn resolve(
&self,
RenameBuild { id, name }: RenameBuild,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Build>(&id, &name, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Build>(&self.id, &self.name, user).await?)
}
}
impl Resolve<RefreshBuildCache, User> for State {
#[instrument(
name = "RefreshBuildCache",
level = "debug",
skip(self, user)
)]
async fn resolve(
&self,
RefreshBuildCache { build }: RefreshBuildCache,
user: User,
) -> anyhow::Result<NoData> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// build should be able to do this.
let build = resource::get_check_permissions::<Build>(
&build,
&user,
PermissionLevel::Execute,
impl Resolve<WriteArgs> for WriteBuildFileContents {
#[instrument(name = "WriteBuildFileContents", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
let build = get_check_permissions::<Build>(
&self.build,
&args.user,
PermissionLevel::Write.into(),
)
.await?;
if build.config.repo.is_empty()
|| build.config.git_provider.is_empty()
if !build.config.files_on_host
&& build.config.repo.is_empty()
&& build.config.linked_repo.is_empty()
{
// Nothing to do here
return Ok(NoData {});
return Err(anyhow!(
"Build is not configured to use Files on Host or Git Repo, can't write dockerfile contents"
).into());
}
let config = core_config();
let mut update =
make_update(&build, Operation::WriteDockerfile, &args.user);
let mut clone_args: CloneArgs = (&build).into();
let repo_path =
clone_args.unique_path(&core_config().repo_directory)?;
clone_args.destination = Some(repo_path.display().to_string());
// Don't want to run these on core.
clone_args.on_clone = None;
clone_args.on_pull = None;
update.push_simple_log("Dockerfile to write", &self.contents);
let access_token = if let Some(username) = &clone_args.account {
git_token(&clone_args.provider, username, |https| {
clone_args.https = https
if build.config.files_on_host {
match get_on_host_periphery(&build)
.await?
.request(WriteDockerfileContentsToHost {
name: build.name,
build_path: build.config.build_path,
dockerfile_path: build.config.dockerfile_path,
contents: self.contents,
})
.await
.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {username}", clone_args.provider),
)?
.context("Failed to write dockerfile contents to host")
{
Ok(log) => {
update.logs.push(log);
}
Err(e) => {
update.push_error_log(
"Write Dockerfile Contents",
format_serror(&e.into()),
);
}
};
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
if let Err(e) =
(RefreshBuildCache { build: build.id }).resolve(args).await
{
update.push_error_log(
"Refresh build cache",
format_serror(&e.error.into()),
);
}
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
} else {
write_dockerfile_contents_git(self, args, build, update).await
}
}
}
async fn write_dockerfile_contents_git(
req: WriteBuildFileContents,
args: &WriteArgs,
build: Build,
mut update: Update,
) -> serror::Result<Update> {
let WriteBuildFileContents { build: _, contents } = req;
let mut clone_args: CloneArgs = if !build.config.files_on_host
&& !build.config.linked_repo.is_empty()
{
(&crate::resource::get::<Repo>(&build.config.linked_repo).await?)
.into()
} else {
(&build).into()
};
let root = clone_args.unique_path(&core_config().repo_directory)?;
clone_args.destination = Some(root.display().to_string());
let build_path = build
.config
.build_path
.parse::<PathBuf>()
.context("Invalid build path")?;
let dockerfile_path = build
.config
.dockerfile_path
.parse::<PathBuf>()
.context("Invalid dockerfile path")?;
let full_path = root.join(&build_path).join(&dockerfile_path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent).await.with_context(|| {
format!(
"Failed to initialize dockerfile parent directory {parent:?}"
)
})?;
}
let access_token = if let Some(account) = &clone_args.account {
git_token(&clone_args.provider, account, |https| clone_args.https = https)
.await
.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", clone_args.provider),
)?
} else {
None
};
// Ensure the folder is initialized as git repo.
// This allows a new file to be committed on a branch that may not exist.
if !root.join(".git").exists() {
git::init_folder_as_repo(
&root,
&clone_args,
access_token.as_deref(),
&mut update.logs,
)
.await;
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
}
// Pull latest changes to repo to ensure linear commit history
match git::pull_or_clone(
clone_args,
&core_config().repo_directory,
access_token,
Default::default(),
Default::default(),
Default::default(),
Default::default(),
)
.await
.context("Failed to pull latest changes before commit")
{
Ok(res) => update.logs.extend(res.logs),
Err(e) => {
update.push_error_log("Pull Repo", format_serror(&e.into()));
update.finalize();
return Ok(update);
}
};
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
if let Err(e) =
fs::write(&full_path, &contents).await.with_context(|| {
format!("Failed to write dockerfile contents to {full_path:?}")
})
{
update
.push_error_log("Write Dockerfile", format_serror(&e.into()));
} else {
update.push_simple_log(
"Write Dockerfile",
format!("File written to {full_path:?}"),
);
};
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
let commit_res = git::commit_file(
&format!("{}: Commit Dockerfile", args.user.username),
&root,
&build_path.join(&dockerfile_path),
&build.config.branch,
)
.await;
update.logs.extend(commit_res.logs);
if let Err(e) = (RefreshBuildCache { build: build.name })
.resolve(args)
.await
{
update.push_error_log(
"Refresh build cache",
format_serror(&e.error.into()),
);
}
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
impl Resolve<WriteArgs> for RefreshBuildCache {
#[instrument(
name = "RefreshBuildCache",
level = "debug",
skip(user)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// build should be able to do this.
let build = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Execute.into(),
)
.await?;
let repo = if !build.config.files_on_host
&& !build.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&build.config.linked_repo)
.await?
.into()
} else {
None
};
let GitRes {
hash: latest_hash,
message: latest_message,
..
} = git::pull_or_clone(
clone_args,
&config.repo_directory,
access_token,
&[],
"",
None,
&[],
)
.await
.context("failed to clone build repo")?;
let (
remote_path,
remote_contents,
remote_error,
latest_hash,
latest_message,
) = if build.config.files_on_host {
// =============
// FILES ON HOST
// =============
match get_on_host_dockerfile(&build).await {
Ok(FileContents { path, contents }) => {
(Some(path), Some(contents), None, None, None)
}
Err(e) => {
(None, None, Some(format_serror(&e.into())), None, None)
}
}
} else if let Some(repo) = &repo {
let Some(res) = get_git_remote(&build, repo.into()).await?
else {
// Nothing to do here
return Ok(NoData {});
};
res
} else if !build.config.repo.is_empty() {
let Some(res) = get_git_remote(&build, (&build).into()).await?
else {
// Nothing to do here
return Ok(NoData {});
};
res
} else {
// =============
// UI BASED FILE
// =============
(None, None, None, None, None)
};
let info = BuildInfo {
last_built_at: build.info.last_built_at,
built_hash: build.info.built_hash,
built_message: build.info.built_message,
built_contents: build.info.built_contents,
remote_path,
remote_contents,
remote_error,
latest_hash,
latest_message,
};
@@ -178,39 +418,169 @@ impl Resolve<RefreshBuildCache, User> for State {
}
}
impl Resolve<CreateBuildWebhook, User> for State {
#[instrument(name = "CreateBuildWebhook", skip(self, user))]
async fn get_on_host_periphery(
build: &Build,
) -> anyhow::Result<PeripheryClient> {
if build.config.builder_id.is_empty() {
return Err(anyhow!("No builder associated with build"));
}
let builder = resource::get::<Builder>(&build.config.builder_id)
.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) => {
let periphery = PeripheryClient::new(
config.address,
config.passkey,
Duration::from_secs(3),
);
periphery.health_check().await?;
Ok(periphery)
}
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)
}
}
}
/// The successful case will be included as Some(remote_contents).
/// The error case will be included as Some(remote_error)
async fn get_on_host_dockerfile(
build: &Build,
) -> anyhow::Result<FileContents> {
get_on_host_periphery(build)
.await?
.request(GetDockerfileContentsOnHost {
name: build.name.clone(),
build_path: build.config.build_path.clone(),
dockerfile_path: build.config.dockerfile_path.clone(),
})
.await
}
async fn get_git_remote(
build: &Build,
mut clone_args: CloneArgs,
) -> anyhow::Result<
Option<(
Option<String>,
Option<String>,
Option<String>,
Option<String>,
Option<String>,
)>,
> {
if clone_args.provider.is_empty() {
// Nothing to do here
return Ok(None);
}
let config = core_config();
let repo_path = clone_args.unique_path(&config.repo_directory)?;
clone_args.destination = Some(repo_path.display().to_string());
// Don't want to run these on core.
clone_args.on_clone = None;
clone_args.on_pull = None;
let access_token = if let Some(username) = &clone_args.account {
git_token(&clone_args.provider, username, |https| {
clone_args.https = https
})
.await
.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {username}", clone_args.provider),
)?
} else {
None
};
let GitRes { hash, message, .. } = git::pull_or_clone(
clone_args,
&config.repo_directory,
access_token,
&[],
"",
None,
&[],
)
.await
.context("failed to clone build repo")?;
let relative_path = PathBuf::from_str(&build.config.build_path)
.context("Invalid build path")?
.join(&build.config.dockerfile_path);
let full_path = repo_path.join(&relative_path);
let (contents, error) =
match fs::read_to_string(&full_path).await.with_context(|| {
format!("Failed to read dockerfile contents at {full_path:?}")
}) {
Ok(contents) => (Some(contents), None),
Err(e) => (None, Some(format_serror(&e.into()))),
};
Ok(Some((
Some(relative_path.display().to_string()),
contents,
error,
hash,
message,
)))
}
impl Resolve<WriteArgs> for CreateBuildWebhook {
#[instrument(name = "CreateBuildWebhook", skip(args))]
async fn resolve(
&self,
CreateBuildWebhook { build }: CreateBuildWebhook,
user: User,
) -> anyhow::Result<CreateBuildWebhookResponse> {
self,
args: &WriteArgs,
) -> serror::Result<CreateBuildWebhookResponse> {
let Some(github) = github_client() else {
return Err(anyhow!(
"github_webhook_app is not configured in core config toml"
));
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let build = resource::get_check_permissions::<Build>(
&build,
&user,
PermissionLevel::Write,
let WriteArgs { user } = args;
let build = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Write.into(),
)
.await?;
if build.config.repo.is_empty() {
return Err(anyhow!(
"No repo configured, can't create webhook"
));
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = build.config.repo.split('/');
let owner = split.next().context("Build repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(anyhow!(
"Cannot manage repo webhooks under owner {owner}"
));
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
@@ -271,64 +641,65 @@ impl Resolve<CreateBuildWebhook, User> for State {
.context("failed to create webhook")?;
if !build.config.webhook_enabled {
self
.resolve(
UpdateBuild {
id: build.id,
config: PartialBuildConfig {
webhook_enabled: Some(true),
..Default::default()
},
},
user,
)
.await
.context("failed to update build to enable webhook")?;
UpdateBuild {
id: build.id,
config: PartialBuildConfig {
webhook_enabled: Some(true),
..Default::default()
},
}
.resolve(args)
.await
.map_err(|e| e.error)
.context("failed to update build to enable webhook")?;
}
Ok(NoData {})
}
}
impl Resolve<DeleteBuildWebhook, User> for State {
#[instrument(name = "DeleteBuildWebhook", skip(self, user))]
impl Resolve<WriteArgs> for DeleteBuildWebhook {
#[instrument(name = "DeleteBuildWebhook", skip(user))]
async fn resolve(
&self,
DeleteBuildWebhook { build }: DeleteBuildWebhook,
user: User,
) -> anyhow::Result<DeleteBuildWebhookResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteBuildWebhookResponse> {
let Some(github) = github_client() else {
return Err(anyhow!(
"github_webhook_app is not configured in core config toml"
));
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let build = resource::get_check_permissions::<Build>(
&build,
&user,
PermissionLevel::Write,
let build = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Write.into(),
)
.await?;
if build.config.git_provider != "github.com" {
return Err(anyhow!(
"Can only manage github.com repo webhooks"
));
return Err(
anyhow!("Can only manage github.com repo webhooks").into(),
);
}
if build.config.repo.is_empty() {
return Err(anyhow!(
"No repo configured, can't delete webhook"
));
return Err(
anyhow!("No repo configured, can't delete webhook").into(),
);
}
let mut split = build.config.repo.split('/');
let owner = split.next().context("Build repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(anyhow!(
"Cannot manage repo webhooks under owner {owner}"
));
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =

View File

@@ -2,70 +2,75 @@ use komodo_client::{
api::write::*,
entities::{
builder::Builder, permission::PermissionLevel, update::Update,
user::User,
},
};
use resolver_api::Resolve;
use crate::{resource, state::State};
use crate::{permission::get_check_permissions, resource};
impl Resolve<CreateBuilder, User> for State {
#[instrument(name = "CreateBuilder", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateBuilder {
#[instrument(name = "CreateBuilder", skip(user))]
async fn resolve(
&self,
CreateBuilder { name, config }: CreateBuilder,
user: User,
) -> anyhow::Result<Builder> {
resource::create::<Builder>(&name, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Builder> {
Ok(
resource::create::<Builder>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<CopyBuilder, User> for State {
#[instrument(name = "CopyBuilder", skip(self, user))]
impl Resolve<WriteArgs> for CopyBuilder {
#[instrument(name = "CopyBuilder", skip(user))]
async fn resolve(
&self,
CopyBuilder { name, id }: CopyBuilder,
user: User,
) -> anyhow::Result<Builder> {
let Builder { config, .. } = resource::get_check_permissions::<
Builder,
>(
&id, &user, PermissionLevel::Write
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Builder> {
let Builder { config, .. } = get_check_permissions::<Builder>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::create::<Builder>(&name, config.into(), &user).await
Ok(
resource::create::<Builder>(&self.name, config.into(), user)
.await?,
)
}
}
impl Resolve<DeleteBuilder, User> for State {
#[instrument(name = "DeleteBuilder", skip(self, user))]
impl Resolve<WriteArgs> for DeleteBuilder {
#[instrument(name = "DeleteBuilder", skip(args))]
async fn resolve(
&self,
DeleteBuilder { id }: DeleteBuilder,
user: User,
) -> anyhow::Result<Builder> {
resource::delete::<Builder>(&id, &user).await
self,
args: &WriteArgs,
) -> serror::Result<Builder> {
Ok(resource::delete::<Builder>(&self.id, args).await?)
}
}
impl Resolve<UpdateBuilder, User> for State {
#[instrument(name = "UpdateBuilder", skip(self, user))]
impl Resolve<WriteArgs> for UpdateBuilder {
#[instrument(name = "UpdateBuilder", skip(user))]
async fn resolve(
&self,
UpdateBuilder { id, config }: UpdateBuilder,
user: User,
) -> anyhow::Result<Builder> {
resource::update::<Builder>(&id, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Builder> {
Ok(
resource::update::<Builder>(&self.id, self.config, user)
.await?,
)
}
}
impl Resolve<RenameBuilder, User> for State {
#[instrument(name = "RenameBuilder", skip(self, user))]
impl Resolve<WriteArgs> for RenameBuilder {
#[instrument(name = "RenameBuilder", skip(user))]
async fn resolve(
&self,
RenameBuilder { id, name }: RenameBuilder,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Builder>(&id, &name, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Builder>(&self.id, &self.name, user).await?)
}
}

View File

@@ -1,19 +1,22 @@
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use komodo_client::{
api::write::*,
entities::{
deployment::{Deployment, DeploymentState},
Operation,
deployment::{
Deployment, DeploymentImage, DeploymentState,
PartialDeploymentConfig, RestartMode,
},
docker::container::RestartPolicyNameEnum,
komodo_timestamp,
permission::PermissionLevel,
server::Server,
to_komodo_name,
server::{Server, ServerState},
to_container_compatible_name,
update::Update,
user::User,
Operation,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::doc};
use periphery_client::api;
use periphery_client::api::{self, container::InspectContainer};
use resolver_api::Resolve;
use crate::{
@@ -22,72 +25,174 @@ use crate::{
query::get_deployment_state,
update::{add_update, make_update},
},
permission::get_check_permissions,
resource,
state::{action_states, db_client, State},
state::{action_states, db_client, server_status_cache},
};
impl Resolve<CreateDeployment, User> for State {
#[instrument(name = "CreateDeployment", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateDeployment {
#[instrument(name = "CreateDeployment", skip(user))]
async fn resolve(
&self,
CreateDeployment { name, config }: CreateDeployment,
user: User,
) -> anyhow::Result<Deployment> {
resource::create::<Deployment>(&name, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
Ok(
resource::create::<Deployment>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<CopyDeployment, User> for State {
#[instrument(name = "CopyDeployment", skip(self, user))]
impl Resolve<WriteArgs> for CopyDeployment {
#[instrument(name = "CopyDeployment", skip(user))]
async fn resolve(
&self,
CopyDeployment { name, id }: CopyDeployment,
user: User,
) -> anyhow::Result<Deployment> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
let Deployment { config, .. } =
resource::get_check_permissions::<Deployment>(
&id,
&user,
PermissionLevel::Write,
get_check_permissions::<Deployment>(
&self.id,
user,
PermissionLevel::Read.into(),
)
.await?;
resource::create::<Deployment>(&name, config.into(), &user).await
Ok(
resource::create::<Deployment>(&self.name, config.into(), user)
.await?,
)
}
}
impl Resolve<DeleteDeployment, User> for State {
#[instrument(name = "DeleteDeployment", skip(self, user))]
impl Resolve<WriteArgs> for CreateDeploymentFromContainer {
#[instrument(name = "CreateDeploymentFromContainer", skip(user))]
async fn resolve(
&self,
DeleteDeployment { id }: DeleteDeployment,
user: User,
) -> anyhow::Result<Deployment> {
resource::delete::<Deployment>(&id, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.inspect().attach(),
)
.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 container = periphery_client(&server)?
.request(InspectContainer {
name: self.name.clone(),
})
.await
.context("Failed to inspect container")?;
let mut config = PartialDeploymentConfig {
server_id: server.id.into(),
..Default::default()
};
if let Some(container_config) = container.config {
config.image = container_config
.image
.map(|image| DeploymentImage::Image { image });
config.command = container_config.cmd.join(" ").into();
config.environment = container_config
.env
.into_iter()
.map(|env| format!(" {env}"))
.collect::<Vec<_>>()
.join("\n")
.into();
config.labels = container_config
.labels
.into_iter()
.map(|(key, val)| format!(" {key}: {val}"))
.collect::<Vec<_>>()
.join("\n")
.into();
}
if let Some(host_config) = container.host_config {
config.volumes = host_config
.binds
.into_iter()
.map(|bind| format!(" {bind}"))
.collect::<Vec<_>>()
.join("\n")
.into();
config.network = host_config.network_mode;
config.ports = host_config
.port_bindings
.into_iter()
.filter_map(|(container, mut host)| {
let host = host.pop()?.host_port?;
Some(format!(" {host}:{}", container.replace("/tcp", "")))
})
.collect::<Vec<_>>()
.join("\n")
.into();
config.restart = host_config.restart_policy.map(|restart| {
match restart.name {
RestartPolicyNameEnum::Always => RestartMode::Always,
RestartPolicyNameEnum::No
| RestartPolicyNameEnum::Empty => RestartMode::NoRestart,
RestartPolicyNameEnum::UnlessStopped => {
RestartMode::UnlessStopped
}
RestartPolicyNameEnum::OnFailure => RestartMode::OnFailure,
}
});
}
Ok(
resource::create::<Deployment>(&self.name, config, user)
.await?,
)
}
}
impl Resolve<UpdateDeployment, User> for State {
#[instrument(name = "UpdateDeployment", skip(self, user))]
impl Resolve<WriteArgs> for DeleteDeployment {
#[instrument(name = "DeleteDeployment", skip(args))]
async fn resolve(
&self,
UpdateDeployment { id, config }: UpdateDeployment,
user: User,
) -> anyhow::Result<Deployment> {
resource::update::<Deployment>(&id, config, &user).await
self,
args: &WriteArgs,
) -> serror::Result<Deployment> {
Ok(resource::delete::<Deployment>(&self.id, args).await?)
}
}
impl Resolve<RenameDeployment, User> for State {
#[instrument(name = "RenameDeployment", skip(self, user))]
impl Resolve<WriteArgs> for UpdateDeployment {
#[instrument(name = "UpdateDeployment", skip(user))]
async fn resolve(
&self,
RenameDeployment { id, name }: RenameDeployment,
user: User,
) -> anyhow::Result<Update> {
let deployment = resource::get_check_permissions::<Deployment>(
&id,
&user,
PermissionLevel::Write,
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
Ok(
resource::update::<Deployment>(&self.id, self.config, user)
.await?,
)
}
}
impl Resolve<WriteArgs> for RenameDeployment {
#[instrument(name = "RenameDeployment", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
let deployment = get_check_permissions::<Deployment>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.await?;
@@ -102,18 +207,22 @@ impl Resolve<RenameDeployment, User> for State {
let _action_guard =
action_state.update(|state| state.renaming = true)?;
let name = to_komodo_name(&name);
let name = to_container_compatible_name(&self.name);
let container_state = get_deployment_state(&deployment).await?;
let container_state =
get_deployment_state(&deployment.id).await?;
if container_state == DeploymentState::Unknown {
return Err(anyhow!(
"Cannot rename Deployment when container status is unknown"
));
return Err(
anyhow!(
"Cannot rename Deployment when container status is unknown"
)
.into(),
);
}
let mut update =
make_update(&deployment, Operation::RenameDeployment, &user);
make_update(&deployment, Operation::RenameDeployment, user);
update_one_by_id(
&db_client().deployments,

View File

@@ -2,117 +2,109 @@ use anyhow::anyhow;
use komodo_client::{
api::write::{UpdateDescription, UpdateDescriptionResponse},
entities::{
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, procedure::Procedure, repo::Repo,
server::Server, server_template::ServerTemplate, stack::Stack,
sync::ResourceSync, user::User, ResourceTarget,
ResourceTarget, action::Action, alerter::Alerter, build::Build,
builder::Builder, deployment::Deployment, procedure::Procedure,
repo::Repo, server::Server, stack::Stack, sync::ResourceSync,
},
};
use resolver_api::Resolve;
use crate::{resource, state::State};
use crate::resource;
impl Resolve<UpdateDescription, User> for State {
#[instrument(name = "UpdateDescription", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for UpdateDescription {
#[instrument(name = "UpdateDescription", skip(user))]
async fn resolve(
&self,
UpdateDescription {
target,
description,
}: UpdateDescription,
user: User,
) -> anyhow::Result<UpdateDescriptionResponse> {
match target {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateDescriptionResponse> {
match self.target {
ResourceTarget::System(_) => {
return Err(anyhow!(
"cannot update description of System resource target"
))
return Err(
anyhow!(
"cannot update description of System resource target"
)
.into(),
);
}
ResourceTarget::Server(id) => {
resource::update_description::<Server>(
&id,
&description,
&user,
&self.description,
user,
)
.await?;
}
ResourceTarget::Deployment(id) => {
resource::update_description::<Deployment>(
&id,
&description,
&user,
&self.description,
user,
)
.await?;
}
ResourceTarget::Build(id) => {
resource::update_description::<Build>(
&id,
&description,
&user,
&self.description,
user,
)
.await?;
}
ResourceTarget::Repo(id) => {
resource::update_description::<Repo>(
&id,
&description,
&user,
&self.description,
user,
)
.await?;
}
ResourceTarget::Builder(id) => {
resource::update_description::<Builder>(
&id,
&description,
&user,
&self.description,
user,
)
.await?;
}
ResourceTarget::Alerter(id) => {
resource::update_description::<Alerter>(
&id,
&description,
&user,
&self.description,
user,
)
.await?;
}
ResourceTarget::Procedure(id) => {
resource::update_description::<Procedure>(
&id,
&description,
&user,
&self.description,
user,
)
.await?;
}
ResourceTarget::Action(id) => {
resource::update_description::<Action>(
&id,
&description,
&user,
)
.await?;
}
ResourceTarget::ServerTemplate(id) => {
resource::update_description::<ServerTemplate>(
&id,
&description,
&user,
&self.description,
user,
)
.await?;
}
ResourceTarget::ResourceSync(id) => {
resource::update_description::<ResourceSync>(
&id,
&description,
&user,
&self.description,
user,
)
.await?;
}
ResourceTarget::Stack(id) => {
resource::update_description::<Stack>(
&id,
&description,
&user,
&self.description,
user,
)
.await?;
}

View File

@@ -1,17 +1,22 @@
use std::time::Instant;
use anyhow::{anyhow, Context};
use axum::{middleware, routing::post, Extension, Router};
use axum_extra::{headers::ContentType, TypedHeader};
use anyhow::Context;
use axum::{
Extension, Router, extract::Path, middleware, routing::post,
};
use derive_variants::{EnumVariants, ExtractVariant};
use komodo_client::{api::write::*, entities::user::User};
use resolver_api::{derive::Resolver, Resolver};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
use crate::{auth::auth_request, state::State};
use crate::auth::auth_request;
use super::Variant;
mod action;
mod alerter;
@@ -24,7 +29,6 @@ mod procedure;
mod provider;
mod repo;
mod server;
mod server_template;
mod service_user;
mod stack;
mod sync;
@@ -33,13 +37,18 @@ mod user;
mod user_group;
mod variable;
pub struct WriteArgs {
pub user: User,
}
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolver, EnumVariants,
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
)]
#[variant_derive(Debug)]
#[resolver_target(State)]
#[resolver_args(User)]
#[args(WriteArgs)]
#[response(Response)]
#[error(serror::Error)]
#[serde(tag = "type", content = "params")]
pub enum WriteRequest {
// ==== USER ====
@@ -60,6 +69,7 @@ pub enum WriteRequest {
AddUserToUserGroup(AddUserToUserGroup),
RemoveUserFromUserGroup(RemoveUserFromUserGroup),
SetUsersInUserGroup(SetUsersInUserGroup),
SetEveryoneUserGroup(SetEveryoneUserGroup),
// ==== PERMISSIONS ====
UpdateUserAdmin(UpdateUserAdmin),
@@ -76,10 +86,25 @@ pub enum WriteRequest {
UpdateServer(UpdateServer),
RenameServer(RenameServer),
CreateNetwork(CreateNetwork),
CreateTerminal(CreateTerminal),
DeleteTerminal(DeleteTerminal),
DeleteAllTerminals(DeleteAllTerminals),
// ==== STACK ====
CreateStack(CreateStack),
CopyStack(CopyStack),
DeleteStack(DeleteStack),
UpdateStack(UpdateStack),
RenameStack(RenameStack),
WriteStackFileContents(WriteStackFileContents),
RefreshStackCache(RefreshStackCache),
CreateStackWebhook(CreateStackWebhook),
DeleteStackWebhook(DeleteStackWebhook),
// ==== DEPLOYMENT ====
CreateDeployment(CreateDeployment),
CopyDeployment(CopyDeployment),
CreateDeploymentFromContainer(CreateDeploymentFromContainer),
DeleteDeployment(DeleteDeployment),
UpdateDeployment(UpdateDeployment),
RenameDeployment(RenameDeployment),
@@ -90,6 +115,7 @@ pub enum WriteRequest {
DeleteBuild(DeleteBuild),
UpdateBuild(UpdateBuild),
RenameBuild(RenameBuild),
WriteBuildFileContents(WriteBuildFileContents),
RefreshBuildCache(RefreshBuildCache),
CreateBuildWebhook(CreateBuildWebhook),
DeleteBuildWebhook(DeleteBuildWebhook),
@@ -101,13 +127,6 @@ pub enum WriteRequest {
UpdateBuilder(UpdateBuilder),
RenameBuilder(RenameBuilder),
// ==== SERVER TEMPLATE ====
CreateServerTemplate(CreateServerTemplate),
CopyServerTemplate(CopyServerTemplate),
DeleteServerTemplate(DeleteServerTemplate),
UpdateServerTemplate(UpdateServerTemplate),
RenameServerTemplate(RenameServerTemplate),
// ==== REPO ====
CreateRepo(CreateRepo),
CopyRepo(CopyRepo),
@@ -151,21 +170,11 @@ pub enum WriteRequest {
CreateSyncWebhook(CreateSyncWebhook),
DeleteSyncWebhook(DeleteSyncWebhook),
// ==== STACK ====
CreateStack(CreateStack),
CopyStack(CopyStack),
DeleteStack(DeleteStack),
UpdateStack(UpdateStack),
RenameStack(RenameStack),
WriteStackFileContents(WriteStackFileContents),
RefreshStackCache(RefreshStackCache),
CreateStackWebhook(CreateStackWebhook),
DeleteStackWebhook(DeleteStackWebhook),
// ==== TAG ====
CreateTag(CreateTag),
DeleteTag(DeleteTag),
RenameTag(RenameTag),
UpdateTagColor(UpdateTagColor),
UpdateTagsOnResource(UpdateTagsOnResource),
// ==== VARIABLE ====
@@ -187,24 +196,33 @@ pub enum WriteRequest {
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler))
.layer(middleware::from_fn(auth_request))
}
async fn variant_handler(
user: Extension<User>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<axum::response::Response> {
let req: WriteRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
}))?;
handler(user, Json(req)).await
}
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<WriteRequest>,
) -> serror::Result<(TypedHeader<ContentType>, String)> {
) -> serror::Result<axum::response::Response> {
let req_id = Uuid::new_v4();
let res = tokio::spawn(task(req_id, request, user))
.await
.context("failure in spawned task");
if let Err(e) = &res {
warn!("/write request {req_id} spawn error: {e:#}");
}
Ok((TypedHeader(ContentType::json()), res??))
res?
}
#[instrument(
@@ -219,28 +237,19 @@ async fn task(
req_id: Uuid,
request: WriteRequest,
user: User,
) -> anyhow::Result<String> {
) -> serror::Result<axum::response::Response> {
info!("/write request | user: {}", user.username);
let timer = Instant::now();
let res =
State
.resolve_request(request, user)
.await
.map_err(|e| match e {
resolver_api::Error::Serialization(e) => {
anyhow!("{e:?}").context("response serialization error")
}
resolver_api::Error::Inner(e) => e,
});
let res = request.resolve(&WriteArgs { user }).await;
if let Err(e) = &res {
warn!("/write request {req_id} error: {e:#}");
warn!("/write request {req_id} error: {:#}", e.error);
}
let elapsed = timer.elapsed();
debug!("/write request {req_id} | resolve time: {elapsed:?}");
res
res.map(|res| res.0)
}

View File

@@ -1,60 +1,56 @@
use std::str::FromStr;
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use komodo_client::{
api::write::{
UpdatePermissionOnResourceType,
UpdatePermissionOnResourceTypeResponse, UpdatePermissionOnTarget,
UpdatePermissionOnTargetResponse, UpdateUserAdmin,
UpdateUserAdminResponse, UpdateUserBasePermissions,
UpdateUserBasePermissionsResponse,
},
api::write::*,
entities::{
permission::{UserTarget, UserTargetVariant},
user::User,
ResourceTarget, ResourceTargetVariant,
permission::{UserTarget, UserTargetVariant},
},
};
use mungos::{
by_id::{find_one_by_id, update_one_by_id},
mongodb::{
bson::{doc, oid::ObjectId, Document},
bson::{Document, doc, oid::ObjectId, to_bson},
options::UpdateOptions,
},
};
use resolver_api::Resolve;
use crate::{
helpers::query::get_user,
state::{db_client, State},
};
use crate::{helpers::query::get_user, state::db_client};
impl Resolve<UpdateUserAdmin, User> for State {
use super::WriteArgs;
impl Resolve<WriteArgs> for UpdateUserAdmin {
#[instrument(name = "UpdateUserAdmin", skip(super_admin))]
async fn resolve(
&self,
UpdateUserAdmin { user_id, admin }: UpdateUserAdmin,
super_admin: User,
) -> anyhow::Result<UpdateUserAdminResponse> {
self,
WriteArgs { user: super_admin }: &WriteArgs,
) -> serror::Result<UpdateUserAdminResponse> {
if !super_admin.super_admin {
return Err(anyhow!("Only super admins can call this method."));
return Err(
anyhow!("Only super admins can call this method.").into(),
);
}
let user = find_one_by_id(&db_client().users, &user_id)
let user = find_one_by_id(&db_client().users, &self.user_id)
.await
.context("failed to query mongo for user")?
.context("did not find user with given id")?;
if !user.enabled {
return Err(anyhow!("User is disabled. Enable user first."));
return Err(
anyhow!("User is disabled. Enable user first.").into(),
);
}
if user.super_admin {
return Err(anyhow!("Cannot update other super admins"));
return Err(anyhow!("Cannot update other super admins").into());
}
update_one_by_id(
&db_client().users,
&user_id,
doc! { "$set": { "admin": admin } },
&self.user_id,
doc! { "$set": { "admin": self.admin } },
None,
)
.await?;
@@ -63,35 +59,39 @@ impl Resolve<UpdateUserAdmin, User> for State {
}
}
impl Resolve<UpdateUserBasePermissions, User> for State {
#[instrument(name = "UpdateUserBasePermissions", skip(self, admin))]
impl Resolve<WriteArgs> for UpdateUserBasePermissions {
#[instrument(name = "UpdateUserBasePermissions", skip(admin))]
async fn resolve(
&self,
UpdateUserBasePermissions {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdateUserBasePermissionsResponse> {
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
let UpdateUserBasePermissions {
user_id,
enabled,
create_servers,
create_builds,
}: UpdateUserBasePermissions,
admin: User,
) -> anyhow::Result<UpdateUserBasePermissionsResponse> {
if !admin.admin {
return Err(anyhow!("this method is admin only"));
}
} = self;
let user = find_one_by_id(&db_client().users, &user_id)
.await
.context("failed to query mongo for user")?
.context("did not find user with given id")?;
if user.super_admin {
return Err(anyhow!(
"Cannot use this method to update super admins permissions"
));
return Err(
anyhow!(
"Cannot use this method to update super admins permissions"
)
.into(),
);
}
if user.admin && !admin.super_admin {
return Err(anyhow!(
"Only super admins can use this method to update other admins permissions"
));
).into());
}
let mut update_doc = Document::new();
if let Some(enabled) = enabled {
@@ -116,34 +116,35 @@ impl Resolve<UpdateUserBasePermissions, User> for State {
}
}
impl Resolve<UpdatePermissionOnResourceType, User> for State {
#[instrument(
name = "UpdatePermissionOnResourceType",
skip(self, admin)
)]
impl Resolve<WriteArgs> for UpdatePermissionOnResourceType {
#[instrument(name = "UpdatePermissionOnResourceType", skip(admin))]
async fn resolve(
&self,
UpdatePermissionOnResourceType {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdatePermissionOnResourceTypeResponse> {
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
let Self {
user_target,
resource_type,
permission,
}: UpdatePermissionOnResourceType,
admin: User,
) -> anyhow::Result<UpdatePermissionOnResourceTypeResponse> {
if !admin.admin {
return Err(anyhow!("this method is admin only"));
}
} = self;
// Some extra checks if user target is an actual User
if let UserTarget::User(user_id) = &user_target {
let user = get_user(user_id).await?;
if user.admin {
return Err(anyhow!(
return Err(
anyhow!(
"cannot use this method to update other admins permissions"
));
)
.into(),
);
}
if !user.enabled {
return Err(anyhow!("user not enabled"));
return Err(anyhow!("user not enabled").into());
}
}
@@ -152,9 +153,11 @@ impl Resolve<UpdatePermissionOnResourceType, User> for State {
let id = ObjectId::from_str(&user_target_id)
.context("id is not ObjectId")?;
let field = format!("all.{resource_type}");
let filter = doc! { "_id": id };
let update = doc! { "$set": { &field: permission.as_ref() } };
let field = format!("all.{resource_type}");
let set =
to_bson(&permission).context("permission is not Bson")?;
let update = doc! { "$set": { &field: &set } };
match user_target_variant {
UserTargetVariant::User => {
@@ -163,7 +166,7 @@ impl Resolve<UpdatePermissionOnResourceType, User> for State {
.update_one(filter, update)
.await
.with_context(|| {
format!("failed to set {field}: {permission} on db")
format!("failed to set {field}: {set} on db")
})?;
}
UserTargetVariant::UserGroup => {
@@ -172,7 +175,7 @@ impl Resolve<UpdatePermissionOnResourceType, User> for State {
.update_one(filter, update)
.await
.with_context(|| {
format!("failed to set {field}: {permission} on db")
format!("failed to set {field}: {set} on db")
})?;
}
}
@@ -181,31 +184,35 @@ impl Resolve<UpdatePermissionOnResourceType, User> for State {
}
}
impl Resolve<UpdatePermissionOnTarget, User> for State {
#[instrument(name = "UpdatePermissionOnTarget", skip(self, admin))]
impl Resolve<WriteArgs> for UpdatePermissionOnTarget {
#[instrument(name = "UpdatePermissionOnTarget", skip(admin))]
async fn resolve(
&self,
UpdatePermissionOnTarget {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdatePermissionOnTargetResponse> {
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
let UpdatePermissionOnTarget {
user_target,
resource_target,
permission,
}: UpdatePermissionOnTarget,
admin: User,
) -> anyhow::Result<UpdatePermissionOnTargetResponse> {
if !admin.admin {
return Err(anyhow!("this method is admin only"));
}
} = self;
// Some extra checks if user target is an actual User
// Some extra checks relevant if user target is an actual User
if let UserTarget::User(user_id) = &user_target {
let user = get_user(user_id).await?;
if user.admin {
return Err(anyhow!(
"cannot use this method to update other admins permissions"
));
}
if !user.enabled {
return Err(anyhow!("user not enabled"));
return Err(anyhow!("user not enabled").into());
}
if user.admin {
return Err(
anyhow!(
"cannot use this method to update other admins permissions"
)
.into(),
);
}
}
@@ -218,6 +225,9 @@ impl Resolve<UpdatePermissionOnTarget, User> for State {
let (user_target_variant, resource_variant) =
(user_target_variant.as_ref(), resource_variant.as_ref());
let specific = to_bson(&permission.specific)
.context("permission.specific is not valid Bson")?;
db_client()
.permissions
.update_one(
@@ -233,7 +243,8 @@ impl Resolve<UpdatePermissionOnTarget, User> for State {
"user_target.id": user_target_id,
"resource_target.type": resource_variant,
"resource_target.id": resource_id,
"level": permission.as_ref(),
"level": permission.level.as_ref(),
"specific": specific
}
},
)
@@ -247,7 +258,7 @@ impl Resolve<UpdatePermissionOnTarget, User> for State {
/// checks if inner id is actually a `name`, and replaces it with id if so.
async fn extract_user_target_with_validation(
user_target: &UserTarget,
) -> anyhow::Result<(UserTargetVariant, String)> {
) -> serror::Result<(UserTargetVariant, String)> {
match user_target {
UserTarget::User(ident) => {
let filter = match ObjectId::from_str(ident) {
@@ -283,7 +294,7 @@ async fn extract_user_target_with_validation(
/// checks if inner id is actually a `name`, and replaces it with id if so.
async fn extract_resource_target_with_validation(
resource_target: &ResourceTarget,
) -> anyhow::Result<(ResourceTargetVariant, String)> {
) -> serror::Result<(ResourceTargetVariant, String)> {
match resource_target {
ResourceTarget::System(_) => {
let res = resource_target.extract_variant_id();
@@ -401,20 +412,6 @@ async fn extract_resource_target_with_validation(
.id;
Ok((ResourceTargetVariant::Action, id))
}
ResourceTarget::ServerTemplate(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.server_templates
.find_one(filter)
.await
.context("failed to query db for server templates")?
.context("no matching server template found")?
.id;
Ok((ResourceTargetVariant::ServerTemplate, id))
}
ResourceTarget::ResourceSync(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },

View File

@@ -1,72 +1,80 @@
use komodo_client::{
api::write::*,
entities::{
permission::PermissionLevel, procedure::Procedure,
update::Update, user::User,
permission::PermissionLevel, procedure::Procedure, update::Update,
},
};
use resolver_api::Resolve;
use crate::{resource, state::State};
use crate::{permission::get_check_permissions, resource};
impl Resolve<CreateProcedure, User> for State {
#[instrument(name = "CreateProcedure", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateProcedure {
#[instrument(name = "CreateProcedure", skip(user))]
async fn resolve(
&self,
CreateProcedure { name, config }: CreateProcedure,
user: User,
) -> anyhow::Result<CreateProcedureResponse> {
resource::create::<Procedure>(&name, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateProcedureResponse> {
Ok(
resource::create::<Procedure>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<CopyProcedure, User> for State {
#[instrument(name = "CopyProcedure", skip(self, user))]
impl Resolve<WriteArgs> for CopyProcedure {
#[instrument(name = "CopyProcedure", skip(user))]
async fn resolve(
&self,
CopyProcedure { name, id }: CopyProcedure,
user: User,
) -> anyhow::Result<CopyProcedureResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CopyProcedureResponse> {
let Procedure { config, .. } =
resource::get_check_permissions::<Procedure>(
&id,
&user,
PermissionLevel::Write,
get_check_permissions::<Procedure>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::create::<Procedure>(&name, config.into(), &user).await
Ok(
resource::create::<Procedure>(&self.name, config.into(), user)
.await?,
)
}
}
impl Resolve<UpdateProcedure, User> for State {
#[instrument(name = "UpdateProcedure", skip(self, user))]
impl Resolve<WriteArgs> for UpdateProcedure {
#[instrument(name = "UpdateProcedure", skip(user))]
async fn resolve(
&self,
UpdateProcedure { id, config }: UpdateProcedure,
user: User,
) -> anyhow::Result<UpdateProcedureResponse> {
resource::update::<Procedure>(&id, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateProcedureResponse> {
Ok(
resource::update::<Procedure>(&self.id, self.config, user)
.await?,
)
}
}
impl Resolve<RenameProcedure, User> for State {
#[instrument(name = "RenameProcedure", skip(self, user))]
impl Resolve<WriteArgs> for RenameProcedure {
#[instrument(name = "RenameProcedure", skip(user))]
async fn resolve(
&self,
RenameProcedure { id, name }: RenameProcedure,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Procedure>(&id, &name, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(
resource::rename::<Procedure>(&self.id, &self.name, user)
.await?,
)
}
}
impl Resolve<DeleteProcedure, User> for State {
#[instrument(name = "DeleteProcedure", skip(self, user))]
impl Resolve<WriteArgs> for DeleteProcedure {
#[instrument(name = "DeleteProcedure", skip(args))]
async fn resolve(
&self,
DeleteProcedure { id }: DeleteProcedure,
user: User,
) -> anyhow::Result<DeleteProcedureResponse> {
resource::delete::<Procedure>(&id, &user).await
self,
args: &WriteArgs,
) -> serror::Result<DeleteProcedureResponse> {
Ok(resource::delete::<Procedure>(&self.id, args).await?)
}
}

View File

@@ -1,10 +1,9 @@
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use komodo_client::{
api::write::*,
entities::{
provider::{DockerRegistryAccount, GitProviderAccount},
user::User,
Operation, ResourceTarget,
provider::{DockerRegistryAccount, GitProviderAccount},
},
};
use mungos::{
@@ -15,35 +14,37 @@ use resolver_api::Resolve;
use crate::{
helpers::update::{add_update, make_update},
state::{db_client, State},
state::db_client,
};
impl Resolve<CreateGitProviderAccount, User> for State {
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateGitProviderAccount {
async fn resolve(
&self,
CreateGitProviderAccount { account }: CreateGitProviderAccount,
user: User,
) -> anyhow::Result<CreateGitProviderAccountResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateGitProviderAccountResponse> {
if !user.admin {
return Err(anyhow!(
"only admins can create git provider accounts"
));
return Err(
anyhow!("only admins can create git provider accounts")
.into(),
);
}
let mut account: GitProviderAccount = account.into();
let mut account: GitProviderAccount = self.account.into();
if account.domain.is_empty() {
return Err(anyhow!("domain cannot be empty string."));
return Err(anyhow!("domain cannot be empty string.").into());
}
if account.username.is_empty() {
return Err(anyhow!("username cannot be empty string."));
return Err(anyhow!("username cannot be empty string.").into());
}
let mut update = make_update(
ResourceTarget::system(),
Operation::CreateGitProviderAccount,
&user,
user,
);
account.id = db_client()
@@ -77,62 +78,63 @@ impl Resolve<CreateGitProviderAccount, User> for State {
}
}
impl Resolve<UpdateGitProviderAccount, User> for State {
impl Resolve<WriteArgs> for UpdateGitProviderAccount {
async fn resolve(
&self,
UpdateGitProviderAccount { id, mut account }: UpdateGitProviderAccount,
user: User,
) -> anyhow::Result<UpdateGitProviderAccountResponse> {
mut self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateGitProviderAccountResponse> {
if !user.admin {
return Err(anyhow!(
"only admins can update git provider accounts"
));
return Err(
anyhow!("only admins can update git provider accounts")
.into(),
);
}
if let Some(domain) = &account.domain {
if let Some(domain) = &self.account.domain {
if domain.is_empty() {
return Err(anyhow!(
"cannot update git provider with empty domain"
));
return Err(
anyhow!("cannot update git provider with empty domain")
.into(),
);
}
}
if let Some(username) = &account.username {
if let Some(username) = &self.account.username {
if username.is_empty() {
return Err(anyhow!(
"cannot update git provider with empty username"
));
return Err(
anyhow!("cannot update git provider with empty username")
.into(),
);
}
}
// Ensure update does not change id
account.id = None;
self.account.id = None;
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateGitProviderAccount,
&user,
user,
);
let account = to_document(&account).context(
let account = to_document(&self.account).context(
"failed to serialize partial git provider account to bson",
)?;
let db = db_client();
update_one_by_id(
&db.git_accounts,
&id,
&self.id,
doc! { "$set": account },
None,
)
.await
.context("failed to update git provider account on db")?;
let Some(account) =
find_one_by_id(&db.git_accounts, &id)
.await
.context("failed to query db for git accounts")?
let Some(account) = find_one_by_id(&db.git_accounts, &self.id)
.await
.context("failed to query db for git accounts")?
else {
return Err(anyhow!("no account found with given id"));
return Err(anyhow!("no account found with given id").into());
};
update.push_simple_log(
@@ -156,33 +158,32 @@ impl Resolve<UpdateGitProviderAccount, User> for State {
}
}
impl Resolve<DeleteGitProviderAccount, User> for State {
impl Resolve<WriteArgs> for DeleteGitProviderAccount {
async fn resolve(
&self,
DeleteGitProviderAccount { id }: DeleteGitProviderAccount,
user: User,
) -> anyhow::Result<DeleteGitProviderAccountResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteGitProviderAccountResponse> {
if !user.admin {
return Err(anyhow!(
"only admins can delete git provider accounts"
));
return Err(
anyhow!("only admins can delete git provider accounts")
.into(),
);
}
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateGitProviderAccount,
&user,
user,
);
let db = db_client();
let Some(account) =
find_one_by_id(&db.git_accounts, &id)
.await
.context("failed to query db for git accounts")?
let Some(account) = find_one_by_id(&db.git_accounts, &self.id)
.await
.context("failed to query db for git accounts")?
else {
return Err(anyhow!("no account found with given id"));
return Err(anyhow!("no account found with given id").into());
};
delete_one_by_id(&db.git_accounts, &id, None)
delete_one_by_id(&db.git_accounts, &self.id, None)
.await
.context("failed to delete git account on db")?;
@@ -207,32 +208,34 @@ impl Resolve<DeleteGitProviderAccount, User> for State {
}
}
impl Resolve<CreateDockerRegistryAccount, User> for State {
impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
async fn resolve(
&self,
CreateDockerRegistryAccount { account }: CreateDockerRegistryAccount,
user: User,
) -> anyhow::Result<CreateDockerRegistryAccountResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateDockerRegistryAccountResponse> {
if !user.admin {
return Err(anyhow!(
"only admins can create docker registry account accounts"
));
return Err(
anyhow!(
"only admins can create docker registry account accounts"
)
.into(),
);
}
let mut account: DockerRegistryAccount = account.into();
let mut account: DockerRegistryAccount = self.account.into();
if account.domain.is_empty() {
return Err(anyhow!("domain cannot be empty string."));
return Err(anyhow!("domain cannot be empty string.").into());
}
if account.username.is_empty() {
return Err(anyhow!("username cannot be empty string."));
return Err(anyhow!("username cannot be empty string.").into());
}
let mut update = make_update(
ResourceTarget::system(),
Operation::CreateDockerRegistryAccount,
&user,
user,
);
account.id = db_client()
@@ -268,50 +271,56 @@ impl Resolve<CreateDockerRegistryAccount, User> for State {
}
}
impl Resolve<UpdateDockerRegistryAccount, User> for State {
impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
async fn resolve(
&self,
UpdateDockerRegistryAccount { id, mut account }: UpdateDockerRegistryAccount,
user: User,
) -> anyhow::Result<UpdateDockerRegistryAccountResponse> {
mut self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateDockerRegistryAccountResponse> {
if !user.admin {
return Err(anyhow!(
"only admins can update docker registry accounts"
));
return Err(
anyhow!("only admins can update docker registry accounts")
.into(),
);
}
if let Some(domain) = &account.domain {
if let Some(domain) = &self.account.domain {
if domain.is_empty() {
return Err(anyhow!(
"cannot update docker registry account with empty domain"
));
return Err(
anyhow!(
"cannot update docker registry account with empty domain"
)
.into(),
);
}
}
if let Some(username) = &account.username {
if let Some(username) = &self.account.username {
if username.is_empty() {
return Err(anyhow!(
"cannot update docker registry account with empty username"
));
return Err(
anyhow!(
"cannot update docker registry account with empty username"
)
.into(),
);
}
}
account.id = None;
self.account.id = None;
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateDockerRegistryAccount,
&user,
user,
);
let account = to_document(&account).context(
let account = to_document(&self.account).context(
"failed to serialize partial docker registry account account to bson",
)?;
let db = db_client();
update_one_by_id(
&db.registry_accounts,
&id,
&self.id,
doc! { "$set": account },
None,
)
@@ -320,11 +329,12 @@ impl Resolve<UpdateDockerRegistryAccount, User> for State {
"failed to update docker registry account account on db",
)?;
let Some(account) = find_one_by_id(&db.registry_accounts, &id)
.await
.context("failed to query db for registry accounts")?
let Some(account) =
find_one_by_id(&db.registry_accounts, &self.id)
.await
.context("failed to query db for registry accounts")?
else {
return Err(anyhow!("no account found with given id"));
return Err(anyhow!("no account found with given id").into());
};
update.push_simple_log(
@@ -348,32 +358,33 @@ impl Resolve<UpdateDockerRegistryAccount, User> for State {
}
}
impl Resolve<DeleteDockerRegistryAccount, User> for State {
impl Resolve<WriteArgs> for DeleteDockerRegistryAccount {
async fn resolve(
&self,
DeleteDockerRegistryAccount { id }: DeleteDockerRegistryAccount,
user: User,
) -> anyhow::Result<DeleteDockerRegistryAccountResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteDockerRegistryAccountResponse> {
if !user.admin {
return Err(anyhow!(
"only admins can delete docker registry accounts"
));
return Err(
anyhow!("only admins can delete docker registry accounts")
.into(),
);
}
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateDockerRegistryAccount,
&user,
user,
);
let db = db_client();
let Some(account) = find_one_by_id(&db.registry_accounts, &id)
.await
.context("failed to query db for git accounts")?
let Some(account) =
find_one_by_id(&db.registry_accounts, &self.id)
.await
.context("failed to query db for git accounts")?
else {
return Err(anyhow!("no account found with given id"));
return Err(anyhow!("no account found with given id").into());
};
delete_one_by_id(&db.registry_accounts, &id, None)
delete_one_by_id(&db.registry_accounts, &self.id, None)
.await
.context("failed to delete registry account on db")?;

View File

@@ -1,18 +1,17 @@
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use formatting::format_serror;
use git::GitRes;
use komodo_client::{
api::write::*,
entities::{
CloneArgs, NoData, Operation,
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
repo::{PartialRepoConfig, Repo, RepoInfo},
server::Server,
to_komodo_name,
to_path_compatible_name,
update::{Log, Update},
user::User,
CloneArgs, NoData, Operation,
},
};
use mongo_indexed::doc;
@@ -29,79 +28,78 @@ use crate::{
git_token, periphery_client,
update::{add_update, make_update},
},
permission::get_check_permissions,
resource,
state::{action_states, db_client, github_client, State},
state::{action_states, db_client, github_client},
};
impl Resolve<CreateRepo, User> for State {
#[instrument(name = "CreateRepo", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateRepo {
#[instrument(name = "CreateRepo", skip(user))]
async fn resolve(
&self,
CreateRepo { name, config }: CreateRepo,
user: User,
) -> anyhow::Result<Repo> {
resource::create::<Repo>(&name, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Repo> {
Ok(resource::create::<Repo>(&self.name, self.config, user).await?)
}
}
impl Resolve<CopyRepo, User> for State {
#[instrument(name = "CopyRepo", skip(self, user))]
impl Resolve<WriteArgs> for CopyRepo {
#[instrument(name = "CopyRepo", skip(user))]
async fn resolve(
&self,
CopyRepo { name, id }: CopyRepo,
user: User,
) -> anyhow::Result<Repo> {
let Repo { config, .. } =
resource::get_check_permissions::<Repo>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
resource::create::<Repo>(&name, config.into(), &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Repo> {
let Repo { config, .. } = get_check_permissions::<Repo>(
&self.id,
user,
PermissionLevel::Read.into(),
)
.await?;
Ok(
resource::create::<Repo>(&self.name, config.into(), user)
.await?,
)
}
}
impl Resolve<DeleteRepo, User> for State {
#[instrument(name = "DeleteRepo", skip(self, user))]
async fn resolve(
&self,
DeleteRepo { id }: DeleteRepo,
user: User,
) -> anyhow::Result<Repo> {
resource::delete::<Repo>(&id, &user).await
impl Resolve<WriteArgs> for DeleteRepo {
#[instrument(name = "DeleteRepo", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Repo> {
Ok(resource::delete::<Repo>(&self.id, args).await?)
}
}
impl Resolve<UpdateRepo, User> for State {
#[instrument(name = "UpdateRepo", skip(self, user))]
impl Resolve<WriteArgs> for UpdateRepo {
#[instrument(name = "UpdateRepo", skip(user))]
async fn resolve(
&self,
UpdateRepo { id, config }: UpdateRepo,
user: User,
) -> anyhow::Result<Repo> {
resource::update::<Repo>(&id, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Repo> {
Ok(resource::update::<Repo>(&self.id, self.config, user).await?)
}
}
impl Resolve<RenameRepo, User> for State {
#[instrument(name = "RenameRepo", skip(self, user))]
impl Resolve<WriteArgs> for RenameRepo {
#[instrument(name = "RenameRepo", skip(user))]
async fn resolve(
&self,
RenameRepo { id, name }: RenameRepo,
user: User,
) -> anyhow::Result<Update> {
let repo = resource::get_check_permissions::<Repo>(
&id,
&user,
PermissionLevel::Write,
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
let repo = get_check_permissions::<Repo>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.await?;
if repo.config.server_id.is_empty()
|| !repo.config.path.is_empty()
{
return resource::rename::<Repo>(&repo.id, &name, &user).await;
return Ok(
resource::rename::<Repo>(&repo.id, &self.name, user).await?,
);
}
// get the action state for the repo (or insert default).
@@ -113,9 +111,9 @@ impl Resolve<RenameRepo, User> for State {
let _action_guard =
action_state.update(|state| state.renaming = true)?;
let name = to_komodo_name(&name);
let name = to_path_compatible_name(&self.name);
let mut update = make_update(&repo, Operation::RenameRepo, &user);
let mut update = make_update(&repo, Operation::RenameRepo, user);
update_one_by_id(
&db_client().repos,
@@ -133,7 +131,7 @@ impl Resolve<RenameRepo, User> for State {
let log = match periphery_client(&server)?
.request(api::git::RenameRepo {
curr_name: to_komodo_name(&repo.name),
curr_name: to_path_compatible_name(&repo.name),
new_name: name.clone(),
})
.await
@@ -159,23 +157,22 @@ impl Resolve<RenameRepo, User> for State {
}
}
impl Resolve<RefreshRepoCache, User> for State {
impl Resolve<WriteArgs> for RefreshRepoCache {
#[instrument(
name = "RefreshRepoCache",
level = "debug",
skip(self, user)
skip(user)
)]
async fn resolve(
&self,
RefreshRepoCache { repo }: RefreshRepoCache,
user: User,
) -> anyhow::Result<NoData> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// repo should be able to do this.
let repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
PermissionLevel::Execute,
let repo = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute.into(),
)
.await?;
@@ -245,39 +242,42 @@ impl Resolve<RefreshRepoCache, User> for State {
}
}
impl Resolve<CreateRepoWebhook, User> for State {
#[instrument(name = "CreateRepoWebhook", skip(self, user))]
impl Resolve<WriteArgs> for CreateRepoWebhook {
#[instrument(name = "CreateRepoWebhook", skip(args))]
async fn resolve(
&self,
CreateRepoWebhook { repo, action }: CreateRepoWebhook,
user: User,
) -> anyhow::Result<CreateRepoWebhookResponse> {
self,
args: &WriteArgs,
) -> serror::Result<CreateRepoWebhookResponse> {
let Some(github) = github_client() else {
return Err(anyhow!(
"github_webhook_app is not configured in core config toml"
));
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
PermissionLevel::Write,
let repo = get_check_permissions::<Repo>(
&self.repo,
&args.user,
PermissionLevel::Write.into(),
)
.await?;
if repo.config.repo.is_empty() {
return Err(anyhow!(
"No repo configured, can't create webhook"
));
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = repo.config.repo.split('/');
let owner = split.next().context("Repo repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(anyhow!(
"Cannot manage repo webhooks under owner {owner}"
));
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo_name =
@@ -310,7 +310,7 @@ impl Resolve<CreateRepoWebhook, User> for State {
} else {
webhook_base_url
};
let url = match action {
let url = match self.action {
RepoWebhookAction::Clone => {
format!("{host}/listener/github/repo/{}/clone", repo.id)
}
@@ -348,64 +348,65 @@ impl Resolve<CreateRepoWebhook, User> for State {
.context("failed to create webhook")?;
if !repo.config.webhook_enabled {
self
.resolve(
UpdateRepo {
id: repo.id,
config: PartialRepoConfig {
webhook_enabled: Some(true),
..Default::default()
},
},
user,
)
.await
.context("failed to update repo to enable webhook")?;
UpdateRepo {
id: repo.id,
config: PartialRepoConfig {
webhook_enabled: Some(true),
..Default::default()
},
}
.resolve(args)
.await
.map_err(|e| e.error)
.context("failed to update repo to enable webhook")?;
}
Ok(NoData {})
}
}
impl Resolve<DeleteRepoWebhook, User> for State {
#[instrument(name = "DeleteRepoWebhook", skip(self, user))]
impl Resolve<WriteArgs> for DeleteRepoWebhook {
#[instrument(name = "DeleteRepoWebhook", skip(user))]
async fn resolve(
&self,
DeleteRepoWebhook { repo, action }: DeleteRepoWebhook,
user: User,
) -> anyhow::Result<DeleteRepoWebhookResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteRepoWebhookResponse> {
let Some(github) = github_client() else {
return Err(anyhow!(
"github_webhook_app is not configured in core config toml"
));
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
PermissionLevel::Write,
let repo = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Write.into(),
)
.await?;
if repo.config.git_provider != "github.com" {
return Err(anyhow!(
"Can only manage github.com repo webhooks"
));
return Err(
anyhow!("Can only manage github.com repo webhooks").into(),
);
}
if repo.config.repo.is_empty() {
return Err(anyhow!(
"No repo configured, can't create webhook"
));
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = repo.config.repo.split('/');
let owner = split.next().context("Repo repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(anyhow!(
"Cannot manage repo webhooks under owner {owner}"
));
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo_name =
@@ -431,7 +432,7 @@ impl Resolve<DeleteRepoWebhook, User> for State {
} else {
webhook_base_url
};
let url = match action {
let url = match self.action {
RepoWebhookAction::Clone => {
format!("{host}/listener/github/repo/{}/clone", repo.id)
}

View File

@@ -1,12 +1,13 @@
use anyhow::Context;
use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
NoData, Operation,
permission::PermissionLevel,
server::Server,
to_docker_compatible_name,
update::{Update, UpdateStatus},
user::User,
Operation,
},
};
use periphery_client::api;
@@ -17,77 +18,77 @@ use crate::{
periphery_client,
update::{add_update, make_update, update_update},
},
permission::get_check_permissions,
resource,
state::State,
};
impl Resolve<CreateServer, User> for State {
#[instrument(name = "CreateServer", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateServer {
#[instrument(name = "CreateServer", skip(user))]
async fn resolve(
&self,
CreateServer { name, config }: CreateServer,
user: User,
) -> anyhow::Result<Server> {
resource::create::<Server>(&name, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Server> {
Ok(
resource::create::<Server>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<DeleteServer, User> for State {
#[instrument(name = "DeleteServer", skip(self, user))]
async fn resolve(
&self,
DeleteServer { id }: DeleteServer,
user: User,
) -> anyhow::Result<Server> {
resource::delete::<Server>(&id, &user).await
impl Resolve<WriteArgs> for DeleteServer {
#[instrument(name = "DeleteServer", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Server> {
Ok(resource::delete::<Server>(&self.id, args).await?)
}
}
impl Resolve<UpdateServer, User> for State {
#[instrument(name = "UpdateServer", skip(self, user))]
impl Resolve<WriteArgs> for UpdateServer {
#[instrument(name = "UpdateServer", skip(user))]
async fn resolve(
&self,
UpdateServer { id, config }: UpdateServer,
user: User,
) -> anyhow::Result<Server> {
resource::update::<Server>(&id, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Server> {
Ok(resource::update::<Server>(&self.id, self.config, user).await?)
}
}
impl Resolve<RenameServer, User> for State {
#[instrument(name = "RenameServer", skip(self, user))]
impl Resolve<WriteArgs> for RenameServer {
#[instrument(name = "RenameServer", skip(user))]
async fn resolve(
&self,
RenameServer { id, name }: RenameServer,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Server>(&id, &name, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Server>(&self.id, &self.name, user).await?)
}
}
impl Resolve<CreateNetwork, User> for State {
#[instrument(name = "CreateNetwork", skip(self, user))]
impl Resolve<WriteArgs> for CreateNetwork {
#[instrument(name = "CreateNetwork", skip(user))]
async fn resolve(
&self,
CreateNetwork { server, name }: CreateNetwork,
user: User,
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Write,
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write.into(),
)
.await?;
let periphery = periphery_client(&server)?;
let mut update =
make_update(&server, Operation::CreateNetwork, &user);
make_update(&server, Operation::CreateNetwork, user);
update.status = UpdateStatus::InProgress;
update.id = add_update(update.clone()).await?;
match periphery
.request(api::network::CreateNetwork { name, driver: None })
.request(api::network::CreateNetwork {
name: to_docker_compatible_name(&self.name),
driver: None,
})
.await
{
Ok(log) => update.logs.push(log),
@@ -103,3 +104,81 @@ impl Resolve<CreateNetwork, User> for State {
Ok(update)
}
}
impl Resolve<WriteArgs> for CreateTerminal {
#[instrument(name = "CreateTerminal", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write.terminal(),
)
.await?;
let periphery = periphery_client(&server)?;
periphery
.request(api::terminal::CreateTerminal {
name: self.name,
command: self.command,
recreate: self.recreate,
})
.await
.context("Failed to create terminal on periphery")?;
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteTerminal {
#[instrument(name = "DeleteTerminal", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write.terminal(),
)
.await?;
let periphery = periphery_client(&server)?;
periphery
.request(api::terminal::DeleteTerminal {
terminal: self.terminal,
})
.await
.context("Failed to delete terminal on periphery")?;
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteAllTerminals {
#[instrument(name = "DeleteAllTerminals", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write.terminal(),
)
.await?;
let periphery = periphery_client(&server)?;
periphery
.request(api::terminal::DeleteAllTerminals {})
.await
.context("Failed to delete all terminals on periphery")?;
Ok(NoData {})
}
}

View File

@@ -1,76 +0,0 @@
use komodo_client::{
api::write::{
CopyServerTemplate, CreateServerTemplate, DeleteServerTemplate,
RenameServerTemplate, UpdateServerTemplate,
},
entities::{
permission::PermissionLevel, server_template::ServerTemplate,
update::Update, user::User,
},
};
use resolver_api::Resolve;
use crate::{resource, state::State};
impl Resolve<CreateServerTemplate, User> for State {
#[instrument(name = "CreateServerTemplate", skip(self, user))]
async fn resolve(
&self,
CreateServerTemplate { name, config }: CreateServerTemplate,
user: User,
) -> anyhow::Result<ServerTemplate> {
resource::create::<ServerTemplate>(&name, config, &user).await
}
}
impl Resolve<CopyServerTemplate, User> for State {
#[instrument(name = "CopyServerTemplate", skip(self, user))]
async fn resolve(
&self,
CopyServerTemplate { name, id }: CopyServerTemplate,
user: User,
) -> anyhow::Result<ServerTemplate> {
let ServerTemplate { config, .. } =
resource::get_check_permissions::<ServerTemplate>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
resource::create::<ServerTemplate>(&name, config.into(), &user)
.await
}
}
impl Resolve<DeleteServerTemplate, User> for State {
#[instrument(name = "DeleteServerTemplate", skip(self, user))]
async fn resolve(
&self,
DeleteServerTemplate { id }: DeleteServerTemplate,
user: User,
) -> anyhow::Result<ServerTemplate> {
resource::delete::<ServerTemplate>(&id, &user).await
}
}
impl Resolve<UpdateServerTemplate, User> for State {
#[instrument(name = "UpdateServerTemplate", skip(self, user))]
async fn resolve(
&self,
UpdateServerTemplate { id, config }: UpdateServerTemplate,
user: User,
) -> anyhow::Result<ServerTemplate> {
resource::update::<ServerTemplate>(&id, config, &user).await
}
}
impl Resolve<RenameServerTemplate, User> for State {
#[instrument(name = "RenameServerTemplate", skip(self, user))]
async fn resolve(
&self,
RenameServerTemplate { id, name }: RenameServerTemplate,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<ServerTemplate>(&id, &name, &user).await
}
}

View File

@@ -1,17 +1,8 @@
use std::str::FromStr;
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use komodo_client::{
api::{
user::CreateApiKey,
write::{
CreateApiKeyForServiceUser, CreateApiKeyForServiceUserResponse,
CreateServiceUser, CreateServiceUserResponse,
DeleteApiKeyForServiceUser, DeleteApiKeyForServiceUserResponse,
UpdateServiceUserDescription,
UpdateServiceUserDescriptionResponse,
},
},
api::{user::CreateApiKey, write::*},
entities::{
komodo_timestamp,
user::{User, UserConfig},
@@ -23,28 +14,30 @@ use mungos::{
};
use resolver_api::Resolve;
use crate::state::{db_client, State};
use crate::{api::user::UserArgs, state::db_client};
impl Resolve<CreateServiceUser, User> for State {
#[instrument(name = "CreateServiceUser", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateServiceUser {
#[instrument(name = "CreateServiceUser", skip(user))]
async fn resolve(
&self,
CreateServiceUser {
username,
description,
}: CreateServiceUser,
user: User,
) -> anyhow::Result<CreateServiceUserResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin"));
return Err(anyhow!("user not admin").into());
}
if ObjectId::from_str(&username).is_ok() {
return Err(anyhow!("username cannot be valid ObjectId"));
if ObjectId::from_str(&self.username).is_ok() {
return Err(
anyhow!("username cannot be valid ObjectId").into(),
);
}
let config = UserConfig::Service { description };
let config = UserConfig::Service {
description: self.description,
};
let mut user = User {
id: Default::default(),
username,
username: self.username,
config,
enabled: true,
admin: false,
@@ -69,88 +62,81 @@ impl Resolve<CreateServiceUser, User> for State {
}
}
impl Resolve<UpdateServiceUserDescription, User> for State {
#[instrument(
name = "UpdateServiceUserDescription",
skip(self, user)
)]
impl Resolve<WriteArgs> for UpdateServiceUserDescription {
#[instrument(name = "UpdateServiceUserDescription", skip(user))]
async fn resolve(
&self,
UpdateServiceUserDescription {
username,
description,
}: UpdateServiceUserDescription,
user: User,
) -> anyhow::Result<UpdateServiceUserDescriptionResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateServiceUserDescriptionResponse> {
if !user.admin {
return Err(anyhow!("user not admin"));
return Err(anyhow!("user not admin").into());
}
let db = db_client();
let service_user = db
.users
.find_one(doc! { "username": &username })
.find_one(doc! { "username": &self.username })
.await
.context("failed to query db for user")?
.context("no user with given username")?;
let UserConfig::Service { .. } = &service_user.config else {
return Err(anyhow!("user is not service user"));
return Err(anyhow!("user is not service user").into());
};
db.users
.update_one(
doc! { "username": &username },
doc! { "$set": { "config.data.description": description } },
doc! { "username": &self.username },
doc! { "$set": { "config.data.description": self.description } },
)
.await
.context("failed to update user on db")?;
db.users
.find_one(doc! { "username": &username })
let res = db
.users
.find_one(doc! { "username": &self.username })
.await
.context("failed to query db for user")?
.context("user with username not found")
.context("user with username not found")?;
Ok(res)
}
}
impl Resolve<CreateApiKeyForServiceUser, User> for State {
#[instrument(name = "CreateApiKeyForServiceUser", skip(self, user))]
impl Resolve<WriteArgs> for CreateApiKeyForServiceUser {
#[instrument(name = "CreateApiKeyForServiceUser", skip(user))]
async fn resolve(
&self,
CreateApiKeyForServiceUser {
user_id,
name,
expires,
}: CreateApiKeyForServiceUser,
user: User,
) -> anyhow::Result<CreateApiKeyForServiceUserResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateApiKeyForServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin"));
return Err(anyhow!("user not admin").into());
}
let service_user = find_one_by_id(&db_client().users, &user_id)
.await
.context("failed to query db for user")?
.context("no user found with id")?;
let service_user =
find_one_by_id(&db_client().users, &self.user_id)
.await
.context("failed to query db for user")?
.context("no user found with id")?;
let UserConfig::Service { .. } = &service_user.config else {
return Err(anyhow!("user is not service user"));
return Err(anyhow!("user is not service user").into());
};
self
.resolve(CreateApiKey { name, expires }, service_user)
.await
CreateApiKey {
name: self.name,
expires: self.expires,
}
.resolve(&UserArgs { user: service_user })
.await
}
}
impl Resolve<DeleteApiKeyForServiceUser, User> for State {
#[instrument(name = "DeleteApiKeyForServiceUser", skip(self, user))]
impl Resolve<WriteArgs> for DeleteApiKeyForServiceUser {
#[instrument(name = "DeleteApiKeyForServiceUser", skip(user))]
async fn resolve(
&self,
DeleteApiKeyForServiceUser { key }: DeleteApiKeyForServiceUser,
user: User,
) -> anyhow::Result<DeleteApiKeyForServiceUserResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteApiKeyForServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin"));
return Err(anyhow!("user not admin").into());
}
let db = db_client();
let api_key = db
.api_keys
.find_one(doc! { "key": &key })
.find_one(doc! { "key": &self.key })
.await
.context("failed to query db for api key")?
.context("did not find matching api key")?;
@@ -160,10 +146,10 @@ impl Resolve<DeleteApiKeyForServiceUser, User> for State {
.context("failed to query db for user")?
.context("no user found with id")?;
let UserConfig::Service { .. } = &service_user.config else {
return Err(anyhow!("user is not service user"));
return Err(anyhow!("user is not service user").into());
};
db.api_keys
.delete_one(doc! { "key": key })
.delete_one(doc! { "key": self.key })
.await
.context("failed to delete api key on db")?;
Ok(DeleteApiKeyForServiceUserResponse {})

View File

@@ -1,15 +1,16 @@
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
FileContents, NoData, Operation,
config::core::CoreConfig,
permission::PermissionLevel,
repo::Repo,
server::ServerState,
stack::{PartialStackConfig, Stack, StackInfo},
update::Update,
user::{stack_user, User},
FileContents, NoData, Operation,
user::stack_user,
},
};
use mungos::mongodb::bson::{doc, to_document};
@@ -23,109 +24,125 @@ use periphery_client::api::compose::{
use resolver_api::Resolve;
use crate::{
api::execute::pull_stack_inner,
config::core_config,
helpers::{
git_token, periphery_client,
periphery_client,
query::get_server_with_state,
stack_git_token,
update::{add_update, make_update},
},
permission::get_check_permissions,
resource,
stack::{
get_stack_and_server,
remote::{get_remote_compose_contents, RemoteComposeContents},
remote::{RemoteComposeContents, get_repo_compose_contents},
services::extract_services_into_res,
},
state::{db_client, github_client, State},
state::{db_client, github_client},
};
impl Resolve<CreateStack, User> for State {
#[instrument(name = "CreateStack", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateStack {
#[instrument(name = "CreateStack", skip(user))]
async fn resolve(
&self,
CreateStack { name, config }: CreateStack,
user: User,
) -> anyhow::Result<Stack> {
resource::create::<Stack>(&name, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Stack> {
Ok(
resource::create::<Stack>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<CopyStack, User> for State {
#[instrument(name = "CopyStack", skip(self, user))]
impl Resolve<WriteArgs> for CopyStack {
#[instrument(name = "CopyStack", skip(user))]
async fn resolve(
&self,
CopyStack { name, id }: CopyStack,
user: User,
) -> anyhow::Result<Stack> {
let Stack { config, .. } =
resource::get_check_permissions::<Stack>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
resource::create::<Stack>(&name, config.into(), &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Stack> {
let Stack { config, .. } = get_check_permissions::<Stack>(
&self.id,
user,
PermissionLevel::Read.into(),
)
.await?;
Ok(
resource::create::<Stack>(&self.name, config.into(), user)
.await?,
)
}
}
impl Resolve<DeleteStack, User> for State {
#[instrument(name = "DeleteStack", skip(self, user))]
async fn resolve(
&self,
DeleteStack { id }: DeleteStack,
user: User,
) -> anyhow::Result<Stack> {
resource::delete::<Stack>(&id, &user).await
impl Resolve<WriteArgs> for DeleteStack {
#[instrument(name = "DeleteStack", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Stack> {
Ok(resource::delete::<Stack>(&self.id, args).await?)
}
}
impl Resolve<UpdateStack, User> for State {
#[instrument(name = "UpdateStack", skip(self, user))]
impl Resolve<WriteArgs> for UpdateStack {
#[instrument(name = "UpdateStack", skip(user))]
async fn resolve(
&self,
UpdateStack { id, config }: UpdateStack,
user: User,
) -> anyhow::Result<Stack> {
resource::update::<Stack>(&id, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Stack> {
Ok(resource::update::<Stack>(&self.id, self.config, user).await?)
}
}
impl Resolve<RenameStack, User> for State {
#[instrument(name = "RenameStack", skip(self, user))]
impl Resolve<WriteArgs> for RenameStack {
#[instrument(name = "RenameStack", skip(user))]
async fn resolve(
&self,
RenameStack { id, name }: RenameStack,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Stack>(&id, &name, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Stack>(&self.id, &self.name, user).await?)
}
}
impl Resolve<WriteStackFileContents, User> for State {
impl Resolve<WriteArgs> for WriteStackFileContents {
#[instrument(name = "WriteStackFileContents", skip(user))]
async fn resolve(
&self,
WriteStackFileContents {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
let WriteStackFileContents {
stack,
file_path,
contents,
}: WriteStackFileContents,
user: User,
) -> anyhow::Result<Update> {
} = self;
let (mut stack, server) = get_stack_and_server(
&stack,
&user,
PermissionLevel::Write,
user,
PermissionLevel::Write.into(),
true,
)
.await?;
if !stack.config.files_on_host && stack.config.repo.is_empty() {
let mut repo = if !stack.config.files_on_host
&& !stack.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&stack.config.linked_repo)
.await?
.into()
} else {
None
};
if !stack.config.files_on_host
&& stack.config.repo.is_empty()
&& stack.config.linked_repo.is_empty()
{
return Err(anyhow!(
"Stack is not configured to use Files on Host or Git Repo, can't write file contents"
));
"Stack is not configured to use Files on Host, Git Repo, or Linked Repo, can't write file contents"
).into());
}
let mut update =
make_update(&stack, Operation::WriteStackContents, &user);
make_update(&stack, Operation::WriteStackContents, user);
update.push_simple_log("File contents to write", &contents);
@@ -147,32 +164,19 @@ impl Resolve<WriteStackFileContents, User> for State {
}
Err(e) => {
update.push_error_log(
"Write file contents",
"Write File Contents",
format_serror(&e.into()),
);
}
};
} else {
let git_token = if !stack.config.git_account.is_empty() {
git_token(
&stack.config.git_provider,
&stack.config.git_account,
|https| stack.config.git_https = https,
)
.await
.with_context(|| {
format!(
"Failed to get git token. | {} | {}",
stack.config.git_account, stack.config.git_provider
)
})?
} else {
None
};
let git_token =
stack_git_token(&mut stack, repo.as_mut()).await?;
match periphery_client(&server)?
.request(WriteCommitComposeContents {
stack,
username: Some(user.username),
repo,
username: Some(user.username.clone()),
file_path,
contents,
git_token,
@@ -185,19 +189,19 @@ impl Resolve<WriteStackFileContents, User> for State {
}
Err(e) => {
update.push_error_log(
"Write file contents",
"Write File Contents",
format_serror(&e.into()),
);
}
};
}
if let Err(e) = State
.resolve(
RefreshStackCache { stack: stack_id },
stack_user().to_owned(),
)
if let Err(e) = (RefreshStackCache { stack: stack_id })
.resolve(&WriteArgs {
user: stack_user().to_owned(),
})
.await
.map_err(|e| e.error)
.context(
"Failed to refresh stack cache after writing file contents",
)
@@ -209,34 +213,44 @@ impl Resolve<WriteStackFileContents, User> for State {
}
update.finalize();
add_update(update.clone()).await?;
update.id = add_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<RefreshStackCache, User> for State {
impl Resolve<WriteArgs> for RefreshStackCache {
#[instrument(
name = "RefreshStackCache",
level = "debug",
skip(self, user)
skip(user)
)]
async fn resolve(
&self,
RefreshStackCache { stack }: RefreshStackCache,
user: User,
) -> anyhow::Result<NoData> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// stack should be able to do this.
let stack = resource::get_check_permissions::<Stack>(
&stack,
&user,
PermissionLevel::Execute,
let stack = get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Execute.into(),
)
.await?;
let repo = if !stack.config.files_on_host
&& !stack.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&stack.config.linked_repo)
.await?
.into()
} else {
None
};
let file_contents_empty = stack.config.file_contents.is_empty();
let repo_empty = stack.config.repo.is_empty();
let repo_empty =
stack.config.repo.is_empty() && repo.as_ref().is_none();
if !stack.config.files_on_host
&& file_contents_empty
@@ -258,54 +272,56 @@ impl Resolve<RefreshStackCache, User> for State {
// =============
// FILES ON HOST
// =============
if stack.config.server_id.is_empty() {
(vec![], None, None, None, None)
let (server, state) = if stack.config.server_id.is_empty() {
(None, ServerState::Disabled)
} else {
let (server, status) =
let (server, state) =
get_server_with_state(&stack.config.server_id).await?;
if status != ServerState::Ok {
(vec![], None, None, None, None)
} else {
let GetComposeContentsOnHostResponse { contents, errors } =
match periphery_client(&server)?
.request(GetComposeContentsOnHost {
file_paths: stack.file_paths().to_vec(),
name: stack.name.clone(),
run_directory: stack.config.run_directory.clone(),
})
.await
.context(
"failed to get compose file contents from host",
) {
Ok(res) => res,
Err(e) => GetComposeContentsOnHostResponse {
contents: Default::default(),
errors: vec![FileContents {
path: stack.config.run_directory.clone(),
contents: format_serror(&e.into()),
}],
},
};
(Some(server), state)
};
if state != ServerState::Ok {
(vec![], None, None, None, None)
} else if let Some(server) = server {
let GetComposeContentsOnHostResponse { contents, errors } =
match periphery_client(&server)?
.request(GetComposeContentsOnHost {
file_paths: stack.file_paths().to_vec(),
name: stack.name.clone(),
run_directory: stack.config.run_directory.clone(),
})
.await
.context("failed to get compose file contents from host")
{
Ok(res) => res,
Err(e) => GetComposeContentsOnHostResponse {
contents: Default::default(),
errors: vec![FileContents {
path: stack.config.run_directory.clone(),
contents: format_serror(&e.into()),
}],
},
};
let project_name = stack.project_name(true);
let project_name = stack.project_name(true);
let mut services = Vec::new();
let mut services = Vec::new();
for contents in &contents {
if let Err(e) = extract_services_into_res(
&project_name,
&contents.contents,
&mut services,
) {
warn!(
"failed to extract stack services, things won't works correctly. stack: {} | {e:#}",
stack.name
);
}
for contents in &contents {
if let Err(e) = extract_services_into_res(
&project_name,
&contents.contents,
&mut services,
) {
warn!(
"failed to extract stack services, things won't works correctly. stack: {} | {e:#}",
stack.name
);
}
(services, Some(contents), Some(errors), None, None)
}
(services, Some(contents), Some(errors), None, None)
} else {
(vec![], None, None, None, None)
}
} else if !repo_empty {
// ================
@@ -317,9 +333,12 @@ impl Resolve<RefreshStackCache, User> for State {
hash: latest_hash,
message: latest_message,
..
} =
get_remote_compose_contents(&stack, Some(&mut missing_files))
.await?;
} = get_repo_compose_contents(
&stack,
repo.as_ref(),
Some(&mut missing_files),
)
.await?;
let project_name = stack.project_name(true);
@@ -357,21 +376,22 @@ impl Resolve<RefreshStackCache, User> for State {
&mut services,
) {
warn!(
"failed to extract stack services, things won't works correctly. stack: {} | {e:#}",
"Failed to extract Stack services for {}, things may not work correctly. | {e:#}",
stack.name
);
services.extend(stack.info.latest_services);
services.extend(stack.info.latest_services.clone());
};
(services, None, None, None, None)
};
let info = StackInfo {
missing_files,
deployed_services: stack.info.deployed_services,
deployed_project_name: stack.info.deployed_project_name,
deployed_contents: stack.info.deployed_contents,
deployed_hash: stack.info.deployed_hash,
deployed_message: stack.info.deployed_message,
deployed_services: stack.info.deployed_services.clone(),
deployed_project_name: stack.info.deployed_project_name.clone(),
deployed_contents: stack.info.deployed_contents.clone(),
deployed_config: stack.info.deployed_config.clone(),
deployed_hash: stack.info.deployed_hash.clone(),
deployed_message: stack.info.deployed_message.clone(),
latest_services,
remote_contents,
remote_errors,
@@ -391,43 +411,66 @@ impl Resolve<RefreshStackCache, User> for State {
.await
.context("failed to update stack info on db")?;
if (stack.config.poll_for_updates || stack.config.auto_update)
&& !stack.config.server_id.is_empty()
{
let (server, state) =
get_server_with_state(&stack.config.server_id).await?;
if state == ServerState::Ok {
let name = stack.name.clone();
if let Err(e) =
pull_stack_inner(stack, Vec::new(), &server, repo, None)
.await
{
warn!(
"Failed to pull latest images for Stack {name} | {e:#}",
);
}
}
}
Ok(NoData {})
}
}
impl Resolve<CreateStackWebhook, User> for State {
#[instrument(name = "CreateStackWebhook", skip(self, user))]
impl Resolve<WriteArgs> for CreateStackWebhook {
#[instrument(name = "CreateStackWebhook", skip(args))]
async fn resolve(
&self,
CreateStackWebhook { stack, action }: CreateStackWebhook,
user: User,
) -> anyhow::Result<CreateStackWebhookResponse> {
self,
args: &WriteArgs,
) -> serror::Result<CreateStackWebhookResponse> {
let WriteArgs { user } = args;
let Some(github) = github_client() else {
return Err(anyhow!(
"github_webhook_app is not configured in core config toml"
));
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let stack = resource::get_check_permissions::<Stack>(
&stack,
&user,
PermissionLevel::Write,
let stack = get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Write.into(),
)
.await?;
if stack.config.repo.is_empty() {
return Err(anyhow!(
"No repo configured, can't create webhook"
));
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = stack.config.repo.split('/');
let owner = split.next().context("Stack repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(anyhow!(
"Cannot manage repo webhooks under owner {owner}"
));
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
@@ -460,7 +503,7 @@ impl Resolve<CreateStackWebhook, User> for State {
} else {
webhook_base_url
};
let url = match action {
let url = match self.action {
StackWebhookAction::Refresh => {
format!("{host}/listener/github/stack/{}/refresh", stack.id)
}
@@ -495,64 +538,65 @@ impl Resolve<CreateStackWebhook, User> for State {
.context("failed to create webhook")?;
if !stack.config.webhook_enabled {
self
.resolve(
UpdateStack {
id: stack.id,
config: PartialStackConfig {
webhook_enabled: Some(true),
..Default::default()
},
},
user,
)
.await
.context("failed to update stack to enable webhook")?;
UpdateStack {
id: stack.id,
config: PartialStackConfig {
webhook_enabled: Some(true),
..Default::default()
},
}
.resolve(args)
.await
.map_err(|e| e.error)
.context("failed to update stack to enable webhook")?;
}
Ok(NoData {})
}
}
impl Resolve<DeleteStackWebhook, User> for State {
#[instrument(name = "DeleteStackWebhook", skip(self, user))]
impl Resolve<WriteArgs> for DeleteStackWebhook {
#[instrument(name = "DeleteStackWebhook", skip(user))]
async fn resolve(
&self,
DeleteStackWebhook { stack, action }: DeleteStackWebhook,
user: User,
) -> anyhow::Result<DeleteStackWebhookResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteStackWebhookResponse> {
let Some(github) = github_client() else {
return Err(anyhow!(
"github_webhook_app is not configured in core config toml"
));
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let stack = resource::get_check_permissions::<Stack>(
&stack,
&user,
PermissionLevel::Write,
let stack = get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Write.into(),
)
.await?;
if stack.config.git_provider != "github.com" {
return Err(anyhow!(
"Can only manage github.com repo webhooks"
));
return Err(
anyhow!("Can only manage github.com repo webhooks").into(),
);
}
if stack.config.repo.is_empty() {
return Err(anyhow!(
"No repo configured, can't create webhook"
));
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = stack.config.repo.split('/');
let owner = split.next().context("Stack repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(anyhow!(
"Cannot manage repo webhooks under owner {owner}"
));
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
@@ -578,7 +622,7 @@ impl Resolve<DeleteStackWebhook, User> for State {
} else {
webhook_base_url
};
let url = match action {
let url = match self.action {
StackWebhookAction::Refresh => {
format!("{host}/listener/github/stack/{}/refresh", stack.id)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,25 @@
use std::str::FromStr;
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use komodo_client::{
api::write::{
CreateTag, DeleteTag, RenameTag, UpdateTagsOnResource,
UpdateTagsOnResourceResponse,
CreateTag, DeleteTag, RenameTag, UpdateTagColor,
UpdateTagsOnResource, UpdateTagsOnResourceResponse,
},
entities::{
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, permission::PermissionLevel,
procedure::Procedure, repo::Repo, server::Server,
server_template::ServerTemplate, stack::Stack,
sync::ResourceSync, tag::Tag, user::User, ResourceTarget,
ResourceTarget,
action::Action,
alerter::Alerter,
build::Build,
builder::Builder,
deployment::Deployment,
permission::PermissionLevel,
procedure::Procedure,
repo::Repo,
server::Server,
stack::Stack,
sync::ResourceSync,
tag::{Tag, TagColor},
},
};
use mungos::{
@@ -22,24 +30,27 @@ use resolver_api::Resolve;
use crate::{
helpers::query::{get_tag, get_tag_check_owner},
permission::get_check_permissions,
resource,
state::{db_client, State},
state::db_client,
};
impl Resolve<CreateTag, User> for State {
#[instrument(name = "CreateTag", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateTag {
#[instrument(name = "CreateTag", skip(user))]
async fn resolve(
&self,
CreateTag { name }: CreateTag,
user: User,
) -> anyhow::Result<Tag> {
if ObjectId::from_str(&name).is_ok() {
return Err(anyhow!("tag name cannot be ObjectId"));
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Tag> {
if ObjectId::from_str(&self.name).is_ok() {
return Err(anyhow!("tag name cannot be ObjectId").into());
}
let mut tag = Tag {
id: Default::default(),
name,
name: self.name,
color: TagColor::Slate,
owner: user.id.clone(),
};
@@ -57,167 +68,180 @@ impl Resolve<CreateTag, User> for State {
}
}
impl Resolve<RenameTag, User> for State {
#[instrument(name = "RenameTag", skip(self, user))]
impl Resolve<WriteArgs> for RenameTag {
#[instrument(name = "RenameTag", skip(user))]
async fn resolve(
&self,
RenameTag { id, name }: RenameTag,
user: User,
) -> anyhow::Result<Tag> {
if ObjectId::from_str(&name).is_ok() {
return Err(anyhow!("tag name cannot be ObjectId"));
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Tag> {
if ObjectId::from_str(&self.name).is_ok() {
return Err(anyhow!("tag name cannot be ObjectId").into());
}
get_tag_check_owner(&id, &user).await?;
get_tag_check_owner(&self.id, user).await?;
update_one_by_id(
&db_client().tags,
&id,
doc! { "$set": { "name": name } },
&self.id,
doc! { "$set": { "name": self.name } },
None,
)
.await
.context("failed to rename tag on db")?;
get_tag(&id).await
Ok(get_tag(&self.id).await?)
}
}
impl Resolve<DeleteTag, User> for State {
#[instrument(name = "DeleteTag", skip(self, user))]
impl Resolve<WriteArgs> for UpdateTagColor {
#[instrument(name = "UpdateTagColor", skip(user))]
async fn resolve(
&self,
DeleteTag { id }: DeleteTag,
user: User,
) -> anyhow::Result<Tag> {
let tag = get_tag_check_owner(&id, &user).await?;
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Tag> {
let tag = get_tag_check_owner(&self.tag, user).await?;
update_one_by_id(
&db_client().tags,
&tag.id,
doc! { "$set": { "color": self.color.as_ref() } },
None,
)
.await
.context("failed to rename tag on db")?;
Ok(get_tag(&self.tag).await?)
}
}
impl Resolve<WriteArgs> for DeleteTag {
#[instrument(name = "DeleteTag", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Tag> {
let tag = get_tag_check_owner(&self.id, user).await?;
tokio::try_join!(
resource::remove_tag_from_all::<Server>(&id),
resource::remove_tag_from_all::<Deployment>(&id),
resource::remove_tag_from_all::<Stack>(&id),
resource::remove_tag_from_all::<Build>(&id),
resource::remove_tag_from_all::<Repo>(&id),
resource::remove_tag_from_all::<Builder>(&id),
resource::remove_tag_from_all::<Alerter>(&id),
resource::remove_tag_from_all::<Procedure>(&id),
resource::remove_tag_from_all::<ServerTemplate>(&id),
resource::remove_tag_from_all::<Server>(&self.id),
resource::remove_tag_from_all::<Deployment>(&self.id),
resource::remove_tag_from_all::<Stack>(&self.id),
resource::remove_tag_from_all::<Build>(&self.id),
resource::remove_tag_from_all::<Repo>(&self.id),
resource::remove_tag_from_all::<Builder>(&self.id),
resource::remove_tag_from_all::<Alerter>(&self.id),
resource::remove_tag_from_all::<Procedure>(&self.id),
)?;
delete_one_by_id(&db_client().tags, &id, None).await?;
delete_one_by_id(&db_client().tags, &self.id, None).await?;
Ok(tag)
}
}
impl Resolve<UpdateTagsOnResource, User> for State {
#[instrument(name = "UpdateTagsOnResource", skip(self, user))]
impl Resolve<WriteArgs> for UpdateTagsOnResource {
#[instrument(name = "UpdateTagsOnResource", skip(args))]
async fn resolve(
&self,
UpdateTagsOnResource { target, tags }: UpdateTagsOnResource,
user: User,
) -> anyhow::Result<UpdateTagsOnResourceResponse> {
match target {
ResourceTarget::System(_) => return Err(anyhow!("")),
self,
args: &WriteArgs,
) -> serror::Result<UpdateTagsOnResourceResponse> {
let WriteArgs { user } = args;
match self.target {
ResourceTarget::System(_) => {
return Err(anyhow!("Invalid target type: System").into());
}
ResourceTarget::Build(id) => {
resource::get_check_permissions::<Build>(
get_check_permissions::<Build>(
&id,
&user,
PermissionLevel::Write,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::update_tags::<Build>(&id, tags, user).await?;
resource::update_tags::<Build>(&id, self.tags, args).await?;
}
ResourceTarget::Builder(id) => {
resource::get_check_permissions::<Builder>(
get_check_permissions::<Builder>(
&id,
&user,
PermissionLevel::Write,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::update_tags::<Builder>(&id, tags, user).await?
resource::update_tags::<Builder>(&id, self.tags, args).await?
}
ResourceTarget::Deployment(id) => {
resource::get_check_permissions::<Deployment>(
get_check_permissions::<Deployment>(
&id,
&user,
PermissionLevel::Write,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::update_tags::<Deployment>(&id, tags, user).await?
}
ResourceTarget::Server(id) => {
resource::get_check_permissions::<Server>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Server>(&id, tags, user).await?
}
ResourceTarget::Repo(id) => {
resource::get_check_permissions::<Repo>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Repo>(&id, tags, user).await?
}
ResourceTarget::Alerter(id) => {
resource::get_check_permissions::<Alerter>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Alerter>(&id, tags, user).await?
}
ResourceTarget::Procedure(id) => {
resource::get_check_permissions::<Procedure>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Procedure>(&id, tags, user).await?
}
ResourceTarget::Action(id) => {
resource::get_check_permissions::<Action>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Action>(&id, tags, user).await?
}
ResourceTarget::ServerTemplate(id) => {
resource::get_check_permissions::<ServerTemplate>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<ServerTemplate>(&id, tags, user)
resource::update_tags::<Deployment>(&id, self.tags, args)
.await?
}
ResourceTarget::ResourceSync(id) => {
resource::get_check_permissions::<ResourceSync>(
ResourceTarget::Server(id) => {
get_check_permissions::<Server>(
&id,
&user,
PermissionLevel::Write,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::update_tags::<ResourceSync>(&id, tags, user).await?
resource::update_tags::<Server>(&id, self.tags, args).await?
}
ResourceTarget::Repo(id) => {
get_check_permissions::<Repo>(
&id,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::update_tags::<Repo>(&id, self.tags, args).await?
}
ResourceTarget::Alerter(id) => {
get_check_permissions::<Alerter>(
&id,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::update_tags::<Alerter>(&id, self.tags, args).await?
}
ResourceTarget::Procedure(id) => {
get_check_permissions::<Procedure>(
&id,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::update_tags::<Procedure>(&id, self.tags, args)
.await?
}
ResourceTarget::Action(id) => {
get_check_permissions::<Action>(
&id,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::update_tags::<Action>(&id, self.tags, args).await?
}
ResourceTarget::ResourceSync(id) => {
get_check_permissions::<ResourceSync>(
&id,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::update_tags::<ResourceSync>(&id, self.tags, args)
.await?
}
ResourceTarget::Stack(id) => {
resource::get_check_permissions::<Stack>(
get_check_permissions::<Stack>(
&id,
&user,
PermissionLevel::Write,
user,
PermissionLevel::Write.into(),
)
.await?;
resource::update_tags::<Stack>(&id, tags, user).await?
resource::update_tags::<Stack>(&id, self.tags, args).await?
}
};
Ok(UpdateTagsOnResourceResponse {})

View File

@@ -1,52 +1,59 @@
use std::str::FromStr;
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use komodo_client::{
api::write::{
DeleteUser, DeleteUserResponse, UpdateUserPassword,
UpdateUserPasswordResponse, UpdateUserUsername,
UpdateUserUsernameResponse,
},
entities::{
user::{User, UserConfig},
NoData,
},
entities::{NoData, user::UserConfig},
};
use mungos::mongodb::bson::{doc, oid::ObjectId};
use resolver_api::Resolve;
use crate::{
helpers::hash_password,
state::{db_client, State},
config::core_config, helpers::hash_password, state::db_client,
};
use super::WriteArgs;
//
impl Resolve<UpdateUserUsername, User> for State {
impl Resolve<WriteArgs> for UpdateUserUsername {
async fn resolve(
&self,
UpdateUserUsername { username }: UpdateUserUsername,
user: User,
) -> anyhow::Result<UpdateUserUsernameResponse> {
if username.is_empty() {
return Err(anyhow!("Username cannot be empty."));
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateUserUsernameResponse> {
for locked_username in &core_config().lock_login_credentials_for {
if locked_username == "__ALL__"
|| *locked_username == user.username
{
return Err(
anyhow!("User not allowed to update their username.")
.into(),
);
}
}
if self.username.is_empty() {
return Err(anyhow!("Username cannot be empty.").into());
}
let db = db_client();
if db
.users
.find_one(doc! { "username": &username })
.find_one(doc! { "username": &self.username })
.await
.context("Failed to query for existing users")?
.is_some()
{
return Err(anyhow!("Username already taken."));
return Err(anyhow!("Username already taken.").into());
}
let id = ObjectId::from_str(&user.id)
.context("User id not valid ObjectId.")?;
db.users
.update_one(
doc! { "_id": id },
doc! { "$set": { "username": username } },
doc! { "$set": { "username": self.username } },
)
.await
.context("Failed to update user username on database.")?;
@@ -56,21 +63,30 @@ impl Resolve<UpdateUserUsername, User> for State {
//
impl Resolve<UpdateUserPassword, User> for State {
impl Resolve<WriteArgs> for UpdateUserPassword {
async fn resolve(
&self,
UpdateUserPassword { password }: UpdateUserPassword,
user: User,
) -> anyhow::Result<UpdateUserPasswordResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateUserPasswordResponse> {
for locked_username in &core_config().lock_login_credentials_for {
if locked_username == "__ALL__"
|| *locked_username == user.username
{
return Err(
anyhow!("User not allowed to update their password.")
.into(),
);
}
}
let UserConfig::Local { .. } = user.config else {
return Err(anyhow!("User is not local user"));
return Err(anyhow!("User is not local user").into());
};
if password.is_empty() {
return Err(anyhow!("Password cannot be empty."));
if self.password.is_empty() {
return Err(anyhow!("Password cannot be empty.").into());
}
let id = ObjectId::from_str(&user.id)
.context("User id not valid ObjectId.")?;
let hashed_password = hash_password(password)?;
let hashed_password = hash_password(self.password)?;
db_client()
.users
.update_one(
@@ -87,22 +103,21 @@ impl Resolve<UpdateUserPassword, User> for State {
//
impl Resolve<DeleteUser, User> for State {
impl Resolve<WriteArgs> for DeleteUser {
async fn resolve(
&self,
DeleteUser { user }: DeleteUser,
admin: User,
) -> anyhow::Result<DeleteUserResponse> {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<DeleteUserResponse> {
if !admin.admin {
return Err(anyhow!("Calling user is not admin."));
return Err(anyhow!("Calling user is not admin.").into());
}
if admin.username == user || admin.id == user {
return Err(anyhow!("User cannot delete themselves."));
if admin.username == self.user || admin.id == self.user {
return Err(anyhow!("User cannot delete themselves.").into());
}
let query = if let Ok(id) = ObjectId::from_str(&user) {
let query = if let Ok(id) = ObjectId::from_str(&self.user) {
doc! { "_id": id }
} else {
doc! { "username": user }
doc! { "username": self.user }
};
let db = db_client();
let Some(user) = db
@@ -111,15 +126,20 @@ impl Resolve<DeleteUser, User> for State {
.await
.context("Failed to query database for users.")?
else {
return Err(anyhow!("No user found with given id / username"));
return Err(
anyhow!("No user found with given id / username").into(),
);
};
if user.super_admin {
return Err(anyhow!("Cannot delete a super admin user."));
return Err(
anyhow!("Cannot delete a super admin user.").into(),
);
}
if user.admin && !admin.super_admin {
return Err(anyhow!(
"Only a Super Admin can delete an admin user."
));
return Err(
anyhow!("Only a Super Admin can delete an admin user.")
.into(),
);
}
db.users
.delete_one(query)

View File

@@ -1,12 +1,9 @@
use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use komodo_client::{
api::write::{
AddUserToUserGroup, CreateUserGroup, DeleteUserGroup,
RemoveUserFromUserGroup, RenameUserGroup, SetUsersInUserGroup,
},
entities::{komodo_timestamp, user::User, user_group::UserGroup},
api::write::*,
entities::{komodo_timestamp, user_group::UserGroup},
};
use mungos::{
by_id::{delete_one_by_id, find_one_by_id, update_one_by_id},
@@ -15,23 +12,26 @@ use mungos::{
};
use resolver_api::Resolve;
use crate::state::{db_client, State};
use crate::state::db_client;
impl Resolve<CreateUserGroup, User> for State {
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateUserGroup {
#[instrument(name = "CreateUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
&self,
CreateUserGroup { name }: CreateUserGroup,
admin: User,
) -> anyhow::Result<UserGroup> {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only"));
return Err(anyhow!("This call is admin-only").into());
}
let user_group = UserGroup {
name: self.name,
id: Default::default(),
everyone: Default::default(),
users: Default::default(),
all: Default::default(),
updated_at: komodo_timestamp(),
name,
};
let db = db_client();
let id = db
@@ -43,63 +43,65 @@ impl Resolve<CreateUserGroup, User> for State {
.as_object_id()
.context("inserted id is not ObjectId")?
.to_string();
find_one_by_id(&db.user_groups, &id)
let res = find_one_by_id(&db.user_groups, &id)
.await
.context("failed to query db for user groups")?
.context("user group at id not found")
.context("user group at id not found")?;
Ok(res)
}
}
impl Resolve<RenameUserGroup, User> for State {
impl Resolve<WriteArgs> for RenameUserGroup {
#[instrument(name = "RenameUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
&self,
RenameUserGroup { id, name }: RenameUserGroup,
admin: User,
) -> anyhow::Result<UserGroup> {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only"));
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
update_one_by_id(
&db.user_groups,
&id,
doc! { "$set": { "name": name } },
&self.id,
doc! { "$set": { "name": self.name } },
None,
)
.await
.context("failed to rename UserGroup on db")?;
find_one_by_id(&db.user_groups, &id)
let res = find_one_by_id(&db.user_groups, &self.id)
.await
.context("failed to query db for UserGroups")?
.context("no user group with given id")
.context("no user group with given id")?;
Ok(res)
}
}
impl Resolve<DeleteUserGroup, User> for State {
impl Resolve<WriteArgs> for DeleteUserGroup {
#[instrument(name = "DeleteUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
&self,
DeleteUserGroup { id }: DeleteUserGroup,
admin: User,
) -> anyhow::Result<UserGroup> {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only"));
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
let ug = find_one_by_id(&db.user_groups, &id)
let ug = find_one_by_id(&db.user_groups, &self.id)
.await
.context("failed to query db for UserGroups")?
.context("no UserGroup found with given id")?;
delete_one_by_id(&db.user_groups, &id, None)
delete_one_by_id(&db.user_groups, &self.id, None)
.await
.context("failed to delete UserGroup from db")?;
db.permissions
.delete_many(doc! {
"user_target.type": "UserGroup",
"user_target.id": id,
"user_target.id": self.id,
})
.await
.context("failed to clean up UserGroups permissions. User Group has been deleted")?;
@@ -108,21 +110,21 @@ impl Resolve<DeleteUserGroup, User> for State {
}
}
impl Resolve<AddUserToUserGroup, User> for State {
impl Resolve<WriteArgs> for AddUserToUserGroup {
#[instrument(name = "AddUserToUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
&self,
AddUserToUserGroup { user_group, user }: AddUserToUserGroup,
admin: User,
) -> anyhow::Result<UserGroup> {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only"));
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
let filter = match ObjectId::from_str(&user) {
let filter = match ObjectId::from_str(&self.user) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "username": &user },
Err(_) => doc! { "username": &self.user },
};
let user = db
.users
@@ -131,9 +133,9 @@ impl Resolve<AddUserToUserGroup, User> for State {
.context("failed to query mongo for users")?
.context("no matching user found")?;
let filter = match ObjectId::from_str(&user_group) {
let filter = match ObjectId::from_str(&self.user_group) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": &user_group },
Err(_) => doc! { "name": &self.user_group },
};
db.user_groups
.update_one(
@@ -142,32 +144,31 @@ impl Resolve<AddUserToUserGroup, User> for State {
)
.await
.context("failed to add user to group on db")?;
db.user_groups
let res = db
.user_groups
.find_one(filter)
.await
.context("failed to query db for UserGroups")?
.context("no user group with given id")
.context("no user group with given id")?;
Ok(res)
}
}
impl Resolve<RemoveUserFromUserGroup, User> for State {
impl Resolve<WriteArgs> for RemoveUserFromUserGroup {
#[instrument(name = "RemoveUserFromUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
&self,
RemoveUserFromUserGroup {
user_group,
user,
}: RemoveUserFromUserGroup,
admin: User,
) -> anyhow::Result<UserGroup> {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only"));
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
let filter = match ObjectId::from_str(&user) {
let filter = match ObjectId::from_str(&self.user) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "username": &user },
Err(_) => doc! { "username": &self.user },
};
let user = db
.users
@@ -176,9 +177,9 @@ impl Resolve<RemoveUserFromUserGroup, User> for State {
.context("failed to query mongo for users")?
.context("no matching user found")?;
let filter = match ObjectId::from_str(&user_group) {
let filter = match ObjectId::from_str(&self.user_group) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": &user_group },
Err(_) => doc! { "name": &self.user_group },
};
db.user_groups
.update_one(
@@ -187,22 +188,24 @@ impl Resolve<RemoveUserFromUserGroup, User> for State {
)
.await
.context("failed to add user to group on db")?;
db.user_groups
let res = db
.user_groups
.find_one(filter)
.await
.context("failed to query db for UserGroups")?
.context("no user group with given id")
.context("no user group with given id")?;
Ok(res)
}
}
impl Resolve<SetUsersInUserGroup, User> for State {
impl Resolve<WriteArgs> for SetUsersInUserGroup {
#[instrument(name = "SetUsersInUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
&self,
SetUsersInUserGroup { user_group, users }: SetUsersInUserGroup,
admin: User,
) -> anyhow::Result<UserGroup> {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only"));
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
@@ -215,7 +218,8 @@ impl Resolve<SetUsersInUserGroup, User> for State {
.collect::<HashMap<_, _>>();
// Make sure all users are user ids
let users = users
let users = self
.users
.into_iter()
.filter_map(|user| match ObjectId::from_str(&user) {
Ok(_) => Some(user),
@@ -223,18 +227,50 @@ impl Resolve<SetUsersInUserGroup, User> for State {
})
.collect::<Vec<_>>();
let filter = match ObjectId::from_str(&user_group) {
let filter = match ObjectId::from_str(&self.user_group) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": &user_group },
Err(_) => doc! { "name": &self.user_group },
};
db.user_groups
.update_one(filter.clone(), doc! { "$set": { "users": users } })
.await
.context("failed to set users on user group")?;
db.user_groups
let res = db
.user_groups
.find_one(filter)
.await
.context("failed to query db for UserGroups")?
.context("no user group with given id")
.context("no user group with given id")?;
Ok(res)
}
}
impl Resolve<WriteArgs> for SetEveryoneUserGroup {
#[instrument(name = "SetEveryoneUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
let filter = match ObjectId::from_str(&self.user_group) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": &self.user_group },
};
db.user_groups
.update_one(filter.clone(), doc! { "$set": { "everyone": self.everyone } })
.await
.context("failed to set everyone on user group")?;
let res = db
.user_groups
.find_one(filter)
.await
.context("failed to query db for UserGroups")?
.context("no user group with given id")?;
Ok(res)
}
}

View File

@@ -1,15 +1,7 @@
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use komodo_client::{
api::write::{
CreateVariable, CreateVariableResponse, DeleteVariable,
DeleteVariableResponse, UpdateVariableDescription,
UpdateVariableDescriptionResponse, UpdateVariableIsSecret,
UpdateVariableIsSecretResponse, UpdateVariableValue,
UpdateVariableValueResponse,
},
entities::{
user::User, variable::Variable, Operation, ResourceTarget,
},
api::write::*,
entities::{Operation, ResourceTarget, variable::Variable},
};
use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
@@ -19,23 +11,26 @@ use crate::{
query::get_variable,
update::{add_update, make_update},
},
state::{db_client, State},
state::db_client,
};
impl Resolve<CreateVariable, User> for State {
#[instrument(name = "CreateVariable", skip(self, user, value))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateVariable {
#[instrument(name = "CreateVariable", skip(user, self), fields(name = &self.name))]
async fn resolve(
&self,
CreateVariable {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateVariableResponse> {
let CreateVariable {
name,
value,
description,
is_secret,
}: CreateVariable,
user: User,
) -> anyhow::Result<CreateVariableResponse> {
} = self;
if !user.admin {
return Err(anyhow!("only admins can create variables"));
return Err(anyhow!("only admins can create variables").into());
}
let variable = Variable {
@@ -54,7 +49,7 @@ impl Resolve<CreateVariable, User> for State {
let mut update = make_update(
ResourceTarget::system(),
Operation::CreateVariable,
&user,
user,
);
update
@@ -63,21 +58,22 @@ impl Resolve<CreateVariable, User> for State {
add_update(update).await?;
get_variable(&variable.name).await
Ok(get_variable(&variable.name).await?)
}
}
impl Resolve<UpdateVariableValue, User> for State {
#[instrument(name = "UpdateVariableValue", skip(self, user, value))]
impl Resolve<WriteArgs> for UpdateVariableValue {
#[instrument(name = "UpdateVariableValue", skip(user, self), fields(name = &self.name))]
async fn resolve(
&self,
UpdateVariableValue { name, value }: UpdateVariableValue,
user: User,
) -> anyhow::Result<UpdateVariableValueResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateVariableValueResponse> {
if !user.admin {
return Err(anyhow!("only admins can update variables"));
return Err(anyhow!("only admins can update variables").into());
}
let UpdateVariableValue { name, value } = self;
let variable = get_variable(&name).await?;
if value == variable.value {
@@ -96,7 +92,7 @@ impl Resolve<UpdateVariableValue, User> for State {
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateVariableValue,
&user,
user,
);
let log = if variable.is_secret {
@@ -116,74 +112,71 @@ impl Resolve<UpdateVariableValue, User> for State {
add_update(update).await?;
get_variable(&name).await
Ok(get_variable(&name).await?)
}
}
impl Resolve<UpdateVariableDescription, User> for State {
#[instrument(name = "UpdateVariableDescription", skip(self, user))]
impl Resolve<WriteArgs> for UpdateVariableDescription {
#[instrument(name = "UpdateVariableDescription", skip(user))]
async fn resolve(
&self,
UpdateVariableDescription { name, description }: UpdateVariableDescription,
user: User,
) -> anyhow::Result<UpdateVariableDescriptionResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateVariableDescriptionResponse> {
if !user.admin {
return Err(anyhow!("only admins can update variables"));
return Err(anyhow!("only admins can update variables").into());
}
db_client()
.variables
.update_one(
doc! { "name": &name },
doc! { "$set": { "description": &description } },
doc! { "name": &self.name },
doc! { "$set": { "description": &self.description } },
)
.await
.context("failed to update variable description on db")?;
get_variable(&name).await
Ok(get_variable(&self.name).await?)
}
}
impl Resolve<UpdateVariableIsSecret, User> for State {
#[instrument(name = "UpdateVariableIsSecret", skip(self, user))]
impl Resolve<WriteArgs> for UpdateVariableIsSecret {
#[instrument(name = "UpdateVariableIsSecret", skip(user))]
async fn resolve(
&self,
UpdateVariableIsSecret { name, is_secret }: UpdateVariableIsSecret,
user: User,
) -> anyhow::Result<UpdateVariableIsSecretResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateVariableIsSecretResponse> {
if !user.admin {
return Err(anyhow!("only admins can update variables"));
return Err(anyhow!("only admins can update variables").into());
}
db_client()
.variables
.update_one(
doc! { "name": &name },
doc! { "$set": { "is_secret": is_secret } },
doc! { "name": &self.name },
doc! { "$set": { "is_secret": self.is_secret } },
)
.await
.context("failed to update variable is secret on db")?;
get_variable(&name).await
Ok(get_variable(&self.name).await?)
}
}
impl Resolve<DeleteVariable, User> for State {
impl Resolve<WriteArgs> for DeleteVariable {
async fn resolve(
&self,
DeleteVariable { name }: DeleteVariable,
user: User,
) -> anyhow::Result<DeleteVariableResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteVariableResponse> {
if !user.admin {
return Err(anyhow!("only admins can delete variables"));
return Err(anyhow!("only admins can delete variables").into());
}
let variable = get_variable(&name).await?;
let variable = get_variable(&self.name).await?;
db_client()
.variables
.delete_one(doc! { "name": &name })
.delete_one(doc! { "name": &self.name })
.await
.context("failed to delete variable on db")?;
let mut update = make_update(
ResourceTarget::system(),
Operation::DeleteVariable,
&user,
user,
);
update

View File

@@ -1,11 +1,11 @@
use std::sync::OnceLock;
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use komodo_client::entities::config::core::{
CoreConfig, OauthCredentials,
};
use reqwest::StatusCode;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use tokio::sync::Mutex;
use crate::{
@@ -47,15 +47,21 @@ impl GithubOauthClient {
return None;
}
if host.is_empty() {
warn!("github oauth is enabled, but 'config.host' is not configured");
warn!(
"github oauth is enabled, but 'config.host' is not configured"
);
return None;
}
if id.is_empty() {
warn!("github oauth is enabled, but 'config.github_oauth.id' is not configured");
warn!(
"github oauth is enabled, but 'config.github_oauth.id' is not configured"
);
return None;
}
if secret.is_empty() {
warn!("github oauth is enabled, but 'config.github_oauth.secret' is not configured");
warn!(
"github oauth is enabled, but 'config.github_oauth.secret' is not configured"
);
return None;
}
GithubOauthClient {

View File

@@ -1,6 +1,6 @@
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use axum::{
extract::Query, response::Redirect, routing::get, Router,
Router, extract::Query, response::Redirect, routing::get,
};
use komodo_client::entities::{
komodo_timestamp,
@@ -13,8 +13,7 @@ use serde::Deserialize;
use serror::AddStatusCode;
use crate::{
config::core_config,
state::{db_client, jwt_client},
config::core_config, helpers::random_string, state::{db_client, jwt_client}
};
use self::client::github_oauth_client;
@@ -72,7 +71,7 @@ async fn callback(
.context("failed at find user query from database")?;
let jwt = match user {
Some(user) => jwt_client()
.generate(user.id)
.encode(user.id)
.context("failed to generate jwt")?,
None => {
let ts = komodo_timestamp();
@@ -82,9 +81,23 @@ async fn callback(
if !no_users_exist && core_config.disable_user_registration {
return Err(anyhow!("User registration is disabled"));
}
let mut username = github_user.login;
// Modify username if it already exists
if db_client
.users
.find_one(doc! { "username": &username })
.await
.context("Failed to query users collection")?
.is_some()
{
username += "-";
username += &random_string(5);
};
let user = User {
id: Default::default(),
username: github_user.login,
username,
enabled: no_users_exist || core_config.enable_new_users,
admin: no_users_exist,
super_admin: no_users_exist,
@@ -109,7 +122,7 @@ async fn callback(
.context("inserted_id is not ObjectId")?
.to_string();
jwt_client()
.generate(user_id)
.encode(user_id)
.context("failed to generate jwt")?
}
};

View File

@@ -1,13 +1,12 @@
use std::sync::OnceLock;
use anyhow::{anyhow, Context};
use jwt::Token;
use anyhow::{Context, anyhow};
use jsonwebtoken::{DecodingKey, Validation, decode};
use komodo_client::entities::config::core::{
CoreConfig, OauthCredentials,
};
use reqwest::StatusCode;
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
use serde::{Deserialize, de::DeserializeOwned};
use tokio::sync::Mutex;
use crate::{
@@ -49,15 +48,21 @@ impl GoogleOauthClient {
return None;
}
if host.is_empty() {
warn!("google oauth is enabled, but 'config.host' is not configured");
warn!(
"google oauth is enabled, but 'config.host' is not configured"
);
return None;
}
if id.is_empty() {
warn!("google oauth is enabled, but 'config.google_oauth.id' is not configured");
warn!(
"google oauth is enabled, but 'config.google_oauth.id' is not configured"
);
return None;
}
if secret.is_empty() {
warn!("google oauth is enabled, but 'config.google_oauth.secret' is not configured");
warn!(
"google oauth is enabled, but 'config.google_oauth.secret' is not configured"
);
return None;
}
let scopes = urlencoding::encode(
@@ -139,10 +144,16 @@ impl GoogleOauthClient {
&self,
id_token: &str,
) -> anyhow::Result<GoogleUser> {
let t: Token<Value, GoogleUser, jwt::Unverified> =
Token::parse_unverified(id_token)
.context("failed to parse id_token")?;
Ok(t.claims().to_owned())
let mut v = Validation::new(Default::default());
v.insecure_disable_signature_validation();
v.validate_aud = false;
let res = decode::<GoogleUser>(
id_token,
&DecodingKey::from_secret(b""),
&v,
)
.context("failed to decode google id token")?;
Ok(res.claims)
}
#[instrument(level = "debug", skip(self))]

View File

@@ -1,7 +1,7 @@
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use async_timing_util::unix_timestamp_ms;
use axum::{
extract::Query, response::Redirect, routing::get, Router,
Router, extract::Query, response::Redirect, routing::get,
};
use komodo_client::entities::user::{User, UserConfig};
use mongo_indexed::Document;
@@ -12,6 +12,7 @@ use serror::AddStatusCode;
use crate::{
config::core_config,
helpers::random_string,
state::{db_client, jwt_client},
};
@@ -81,7 +82,7 @@ async fn callback(
.context("failed at find user query from mongo")?;
let jwt = match user {
Some(user) => jwt_client()
.generate(user.id)
.encode(user.id)
.context("failed to generate jwt")?,
None => {
let ts = unix_timestamp_ms() as i64;
@@ -91,15 +92,28 @@ async fn callback(
if !no_users_exist && core_config.disable_user_registration {
return Err(anyhow!("User registration is disabled"));
}
let mut username = google_user
.email
.split('@')
.collect::<Vec<&str>>()
.first()
.unwrap()
.to_string();
// Modify username if it already exists
if db_client
.users
.find_one(doc! { "username": &username })
.await
.context("Failed to query users collection")?
.is_some()
{
username += "-";
username += &random_string(5);
};
let user = User {
id: Default::default(),
username: google_user
.email
.split('@')
.collect::<Vec<&str>>()
.first()
.unwrap()
.to_string(),
username,
enabled: no_users_exist || core_config.enable_new_users,
admin: no_users_exist,
super_admin: no_users_exist,
@@ -124,7 +138,7 @@ async fn callback(
.context("inserted_id is not ObjectId")?
.to_string();
jwt_client()
.generate(user_id)
.encode(user_id)
.context("failed to generate jwt")?
}
};

View File

@@ -1,15 +1,15 @@
use std::collections::HashMap;
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use async_timing_util::{
get_timelength_in_ms, unix_timestamp_ms, Timelength,
Timelength, get_timelength_in_ms, unix_timestamp_ms,
};
use jsonwebtoken::{
DecodingKey, EncodingKey, Header, Validation, decode, encode,
};
use hmac::{Hmac, Mac};
use jwt::SignWithKey;
use komodo_client::entities::config::core::CoreConfig;
use mungos::mongodb::bson::doc;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use tokio::sync::Mutex;
use crate::helpers::random_string;
@@ -24,7 +24,10 @@ pub struct JwtClaims {
}
pub struct JwtClient {
pub key: Hmac<Sha256>,
header: Header,
validation: Validation,
encoding_key: EncodingKey,
decoding_key: DecodingKey,
ttl_ms: u128,
exchange_tokens: ExchangeTokenMap,
}
@@ -36,10 +39,11 @@ impl JwtClient {
} else {
config.jwt_secret.clone()
};
let key = Hmac::new_from_slice(secret.as_bytes())
.context("failed at taking HmacSha256 of jwt secret")?;
Ok(JwtClient {
key,
header: Header::default(),
validation: Validation::new(Default::default()),
encoding_key: EncodingKey::from_secret(secret.as_bytes()),
decoding_key: DecodingKey::from_secret(secret.as_bytes()),
ttl_ms: get_timelength_in_ms(
config.jwt_ttl.to_string().parse()?,
),
@@ -47,7 +51,7 @@ impl JwtClient {
})
}
pub fn generate(&self, user_id: String) -> anyhow::Result<String> {
pub fn encode(&self, user_id: String) -> anyhow::Result<String> {
let iat = unix_timestamp_ms();
let exp = iat + self.ttl_ms;
let claims = JwtClaims {
@@ -55,10 +59,14 @@ impl JwtClient {
iat,
exp,
};
let jwt = claims
.sign_with_key(&self.key)
.context("failed at signing claim")?;
Ok(jwt)
encode(&self.header, &claims, &self.encoding_key)
.context("failed at signing claim")
}
pub fn decode(&self, jwt: &str) -> anyhow::Result<JwtClaims> {
decode::<JwtClaims>(jwt, &self.decoding_key, &self.validation)
.map(|res| res.claims)
.context("failed to decode token claims")
}
#[instrument(level = "debug", skip_all)]

View File

@@ -1,8 +1,7 @@
use std::str::FromStr;
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use async_timing_util::unix_timestamp_ms;
use axum::http::HeaderMap;
use komodo_client::{
api::auth::{
CreateLocalUser, CreateLocalUserResponse, LoginLocalUser,
@@ -15,50 +14,52 @@ use mungos::mongodb::bson::{doc, oid::ObjectId};
use resolver_api::Resolve;
use crate::{
api::auth::AuthArgs,
config::core_config,
helpers::hash_password,
state::{db_client, jwt_client, State},
state::{db_client, jwt_client},
};
impl Resolve<CreateLocalUser, HeaderMap> for State {
impl Resolve<AuthArgs> for CreateLocalUser {
#[instrument(name = "CreateLocalUser", skip(self))]
async fn resolve(
&self,
CreateLocalUser { username, password }: CreateLocalUser,
_: HeaderMap,
) -> anyhow::Result<CreateLocalUserResponse> {
self,
_: &AuthArgs,
) -> serror::Result<CreateLocalUserResponse> {
let core_config = core_config();
if !core_config.local_auth {
return Err(anyhow!("Local auth is not enabled"));
return Err(anyhow!("Local auth is not enabled").into());
}
if username.is_empty() {
return Err(anyhow!("Username cannot be empty string"));
if self.username.is_empty() {
return Err(anyhow!("Username cannot be empty string").into());
}
if ObjectId::from_str(&username).is_ok() {
return Err(anyhow!("Username cannot be valid ObjectId"));
if ObjectId::from_str(&self.username).is_ok() {
return Err(
anyhow!("Username cannot be valid ObjectId").into(),
);
}
if password.is_empty() {
return Err(anyhow!("Password cannot be empty string"));
if self.password.is_empty() {
return Err(anyhow!("Password cannot be empty string").into());
}
let hashed_password = hash_password(password)?;
let hashed_password = hash_password(self.password)?;
let no_users_exist =
db_client().users.find_one(Document::new()).await?.is_none();
if !no_users_exist && core_config.disable_user_registration {
return Err(anyhow!("User registration is disabled"));
return Err(anyhow!("User registration is disabled").into());
}
let ts = unix_timestamp_ms() as i64;
let user = User {
id: Default::default(),
username,
username: self.username,
enabled: no_users_exist || core_config.enable_new_users,
admin: no_users_exist,
super_admin: no_users_exist,
@@ -84,51 +85,53 @@ impl Resolve<CreateLocalUser, HeaderMap> for State {
.to_string();
let jwt = jwt_client()
.generate(user_id)
.encode(user_id)
.context("failed to generate jwt for user")?;
Ok(CreateLocalUserResponse { jwt })
}
}
impl Resolve<LoginLocalUser, HeaderMap> for State {
impl Resolve<AuthArgs> for LoginLocalUser {
#[instrument(name = "LoginLocalUser", level = "debug", skip(self))]
async fn resolve(
&self,
LoginLocalUser { username, password }: LoginLocalUser,
_: HeaderMap,
) -> anyhow::Result<LoginLocalUserResponse> {
self,
_: &AuthArgs,
) -> serror::Result<LoginLocalUserResponse> {
if !core_config().local_auth {
return Err(anyhow!("local auth is not enabled"));
return Err(anyhow!("local auth is not enabled").into());
}
let user = db_client()
.users
.find_one(doc! { "username": &username })
.find_one(doc! { "username": &self.username })
.await
.context("failed at db query for users")?
.with_context(|| {
format!("did not find user with username {username}")
format!("did not find user with username {}", self.username)
})?;
let UserConfig::Local {
password: user_pw_hash,
} = user.config
else {
return Err(anyhow!(
"non-local auth users can not log in with a password"
));
return Err(
anyhow!(
"non-local auth users can not log in with a password"
)
.into(),
);
};
let verified = bcrypt::verify(password, &user_pw_hash)
let verified = bcrypt::verify(self.password, &user_pw_hash)
.context("failed at verify password")?;
if !verified {
return Err(anyhow!("invalid credentials"));
return Err(anyhow!("invalid credentials").into());
}
let jwt = jwt_client()
.generate(user.id)
.encode(user.id)
.context("failed at generating jwt for user")?;
Ok(LoginLocalUserResponse { jwt })

View File

@@ -1,5 +1,4 @@
use ::jwt::VerifyWithKey;
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use async_timing_util::unix_timestamp_ms;
use axum::{
extract::Request, http::HeaderMap, middleware::Next,
@@ -71,7 +70,9 @@ pub async fn get_user_id_from_headers(
}
_ => {
// AUTH FAIL
Err(anyhow!("must attach either AUTHORIZATION header with jwt OR pass X-API-KEY and X-API-SECRET"))
Err(anyhow!(
"must attach either AUTHORIZATION header with jwt OR pass X-API-KEY and X-API-SECRET"
))
}
}
}
@@ -93,9 +94,7 @@ pub async fn authenticate_check_enabled(
pub async fn auth_jwt_get_user_id(
jwt: &str,
) -> anyhow::Result<String> {
let claims: JwtClaims = jwt
.verify_with_key(&jwt_client().key)
.context("failed to verify claims")?;
let claims: JwtClaims = jwt_client().decode(jwt)?;
if claims.exp > unix_timestamp_ms() {
Ok(claims.id)
} else {

View File

@@ -1,67 +1,93 @@
use std::sync::OnceLock;
use std::{sync::OnceLock, time::Duration};
use anyhow::Context;
use arc_swap::ArcSwapOption;
use openidconnect::{
core::{CoreClient, CoreProviderMetadata},
reqwest::async_http_client,
ClientId, ClientSecret, IssuerUrl, RedirectUrl,
Client, ClientId, ClientSecret, EmptyAdditionalClaims,
EndpointMaybeSet, EndpointNotSet, EndpointSet, IssuerUrl,
RedirectUrl, StandardErrorResponse, core::*,
};
use crate::config::core_config;
static DEFAULT_OIDC_CLIENT: OnceLock<Option<CoreClient>> =
OnceLock::new();
type OidcClient = Client<
EmptyAdditionalClaims,
CoreAuthDisplay,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJsonWebKey,
CoreAuthPrompt,
StandardErrorResponse<CoreErrorResponseType>,
CoreTokenResponse,
CoreTokenIntrospectionResponse,
CoreRevocableToken,
CoreRevocationErrorResponse,
EndpointSet,
EndpointNotSet,
EndpointNotSet,
EndpointNotSet,
EndpointMaybeSet,
EndpointMaybeSet,
>;
pub fn default_oidc_client() -> Option<&'static CoreClient> {
DEFAULT_OIDC_CLIENT
.get()
.expect("OIDC client get before init")
.as_ref()
pub fn oidc_client() -> &'static ArcSwapOption<OidcClient> {
static OIDC_CLIENT: OnceLock<ArcSwapOption<OidcClient>> =
OnceLock::new();
OIDC_CLIENT.get_or_init(Default::default)
}
pub async fn init_default_oidc_client() {
/// The OIDC client must be reinitialized to
/// pick up the latest provider JWKs. This
/// function spawns a management thread to do this
/// on a loop.
pub async fn spawn_oidc_client_management() {
let config = core_config();
if !config.oidc_enabled
|| config.oidc_provider.is_empty()
|| config.oidc_client_id.is_empty()
|| config.oidc_client_secret.is_empty()
{
DEFAULT_OIDC_CLIENT
.set(None)
.expect("Default OIDC client initialized twice");
return;
}
async {
// Use OpenID Connect Discovery to fetch the provider metadata.
let provider_metadata = CoreProviderMetadata::discover_async(
IssuerUrl::new(config.oidc_provider.clone())?,
async_http_client,
)
.await
.context(
"Failed to get OIDC /.well-known/openid-configuration",
)?;
// Create an OpenID Connect client by specifying the client ID, client secret, authorization URL
// and token URL.
let client = CoreClient::from_provider_metadata(
provider_metadata,
ClientId::new(config.oidc_client_id.to_string()),
Some(ClientSecret::new(config.oidc_client_secret.to_string())),
)
// Set the URL the user will be redirected to after the authorization process.
.set_redirect_uri(RedirectUrl::new(format!(
"{}/auth/oidc/callback",
core_config().host
))?);
DEFAULT_OIDC_CLIENT
.set(Some(client))
.expect("Default OIDC client initialized twice");
anyhow::Ok(())
if let Err(e) = reset_oidc_client().await {
error!("Failed to initialize OIDC client | {e:#}");
}
.await
.context("Failed to init default OIDC client")
.unwrap();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(60)).await;
if let Err(e) = reset_oidc_client().await {
warn!("Failed to reinitialize OIDC client | {e:#}");
}
}
});
}
async fn reset_oidc_client() -> anyhow::Result<()> {
let config = core_config();
// Use OpenID Connect Discovery to fetch the provider metadata.
let provider_metadata = CoreProviderMetadata::discover_async(
IssuerUrl::new(config.oidc_provider.clone())?,
super::reqwest_client(),
)
.await
.context("Failed to get OIDC /.well-known/openid-configuration")?;
let client = CoreClient::from_provider_metadata(
provider_metadata,
ClientId::new(config.oidc_client_id.to_string()),
// The secret may be empty / ommitted if auth provider supports PKCE
if config.oidc_client_secret.is_empty() {
None
} else {
Some(ClientSecret::new(config.oidc_client_secret.to_string()))
},
)
// Set the URL the user will be redirected to after the authorization process.
.set_redirect_uri(RedirectUrl::new(format!(
"{}/auth/oidc/callback",
core_config().host
))?);
oidc_client().store(Some(client.into()));
Ok(())
}

View File

@@ -1,20 +1,21 @@
use std::sync::OnceLock;
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use axum::{
extract::Query, response::Redirect, routing::get, Router,
Router, extract::Query, response::Redirect, routing::get,
};
use client::default_oidc_client;
use client::oidc_client;
use dashmap::DashMap;
use komodo_client::entities::{
komodo_timestamp,
user::{User, UserConfig},
};
use mungos::mongodb::bson::{doc, Document};
use mungos::mongodb::bson::{Document, doc};
use openidconnect::{
core::CoreAuthenticationFlow, AccessTokenHash, AuthorizationCode,
CsrfToken, Nonce, OAuth2TokenResponse, PkceCodeChallenge,
PkceCodeVerifier, Scope, TokenResponse,
AccessTokenHash, AuthorizationCode, CsrfToken,
EmptyAdditionalClaims, Nonce, OAuth2TokenResponse,
PkceCodeChallenge, PkceCodeVerifier, Scope, TokenResponse,
core::{CoreAuthenticationFlow, CoreGenderClaim},
};
use reqwest::StatusCode;
use serde::Deserialize;
@@ -22,6 +23,7 @@ use serror::AddStatusCode;
use crate::{
config::core_config,
helpers::random_string,
state::{db_client, jwt_client},
};
@@ -29,16 +31,28 @@ use super::RedirectQuery;
pub mod client;
fn reqwest_client() -> &'static reqwest::Client {
static REQWEST: OnceLock<reqwest::Client> = OnceLock::new();
REQWEST.get_or_init(|| {
reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.expect("Invalid OIDC reqwest client")
})
}
/// CSRF tokens can only be used once from the callback,
/// and must be used within this timeframe
const CSRF_VALID_FOR_MS: i64 = 120_000; // 2 minutes for user to log in.
type RedirectUrl = Option<String>;
type CsrfMap =
/// Maps the csrf secrets to other information added in the "login" method (before auth provider redirect).
/// This information is retrieved in the "callback" method (after auth provider redirect).
type VerifierMap =
DashMap<String, (PkceCodeVerifier, Nonce, RedirectUrl, i64)>;
fn csrf_verifier_tokens() -> &'static CsrfMap {
static CSRF: OnceLock<CsrfMap> = OnceLock::new();
CSRF.get_or_init(Default::default)
fn verifier_tokens() -> &'static VerifierMap {
static VERIFIERS: OnceLock<VerifierMap> = OnceLock::new();
VERIFIERS.get_or_init(Default::default)
}
pub fn router() -> Router {
@@ -61,10 +75,10 @@ pub fn router() -> Router {
async fn login(
Query(RedirectQuery { redirect }): Query<RedirectQuery>,
) -> anyhow::Result<Redirect> {
let client = oidc_client().load();
let client =
default_oidc_client().context("OIDC Client not configured")?;
client.as_ref().context("OIDC Client not configured")?;
// Generate a PKCE challenge.
let (pkce_challenge, pkce_verifier) =
PkceCodeChallenge::new_random_sha256();
@@ -75,13 +89,14 @@ async fn login(
CsrfToken::new_random,
Nonce::new_random,
)
.add_scope(Scope::new("openid".to_string()))
.add_scope(Scope::new("email".to_string()))
.set_pkce_challenge(pkce_challenge)
.add_scope(Scope::new("openid".to_string()))
.add_scope(Scope::new("profile".to_string()))
.add_scope(Scope::new("email".to_string()))
.url();
// Data inserted here will be matched on callback side for csrf protection.
csrf_verifier_tokens().insert(
verifier_tokens().insert(
csrf_token.secret().clone(),
(
pkce_verifier,
@@ -123,8 +138,9 @@ struct CallbackQuery {
async fn callback(
Query(query): Query<CallbackQuery>,
) -> anyhow::Result<Redirect> {
let client = oidc_client().load();
let client =
default_oidc_client().context("OIDC Client not configured")?;
client.as_ref().context("OIDC Client not initialized successfully. Is the provider properly configured?")?;
if let Some(e) = query.error {
return Err(anyhow!("Provider returned error: {e}"));
@@ -136,21 +152,22 @@ async fn callback(
);
let (_, (pkce_verifier, nonce, redirect, valid_until)) =
csrf_verifier_tokens()
verifier_tokens()
.remove(state.secret())
.context("CSRF Token invalid")?;
.context("CSRF token invalid")?;
if komodo_timestamp() > valid_until {
return Err(anyhow!(
"CSRF token invalid (Timed out). The token must be "
"CSRF token invalid (Timed out). The token must be used within 2 minutes."
));
}
let reqwest_client = reqwest_client();
let token_response = client
.exchange_code(AuthorizationCode::new(code))
// Set the PKCE code verifier.
.context("Failed to get Oauth token at exchange code")?
.set_pkce_verifier(pkce_verifier)
.request_async(openidconnect::reqwest::async_http_client)
.request_async(reqwest_client)
.await
.context("Failed to get Oauth token")?;
@@ -173,7 +190,7 @@ async fn callback(
let claims = id_token
.claims(&verifier, &nonce)
.context("Failed to verify token claims")?;
.context("Failed to verify token claims. This issue may be temporary (60 seconds max).")?;
// Verify the access token hash to ensure that the access token hasn't been substituted for
// another user's.
@@ -181,7 +198,8 @@ async fn callback(
{
let actual_access_token_hash = AccessTokenHash::from_token(
token_response.access_token(),
&id_token.signing_alg()?,
id_token.signing_alg()?,
id_token.signing_key(&verifier)?,
)?;
if actual_access_token_hash != *expected_access_token_hash {
return Err(anyhow!("Invalid access token"));
@@ -202,7 +220,7 @@ async fn callback(
let jwt = match user {
Some(user) => jwt_client()
.generate(user.id)
.encode(user.id)
.context("failed to generate jwt")?,
None => {
let ts = komodo_timestamp();
@@ -212,12 +230,26 @@ async fn callback(
if !no_users_exist && core_config.disable_user_registration {
return Err(anyhow!("User registration is disabled"));
}
// Fetch user info
let user_info = client
.user_info(
token_response.access_token().clone(),
claims.subject().clone().into(),
)
.context("Invalid user info request")?
.request_async::<EmptyAdditionalClaims, _, CoreGenderClaim>(
reqwest_client,
)
.await
.context("Failed to fetch user info for new user")?;
// Will use preferred_username, then email, then user_id if it isn't available.
let username = claims
let mut username = user_info
.preferred_username()
.map(|username| username.to_string())
.unwrap_or_else(|| {
let email = claims
let email = user_info
.email()
.map(|email| email.as_str())
.unwrap_or(user_id);
@@ -231,6 +263,19 @@ async fn callback(
}
.to_string()
});
// Modify username if it already exists
if db_client
.users
.find_one(doc! { "username": &username })
.await
.context("Failed to query users collection")?
.is_some()
{
username += "-";
username += &random_string(5);
};
let user = User {
id: Default::default(),
username,
@@ -248,6 +293,7 @@ async fn callback(
user_id: user_id.to_string(),
},
};
let user_id = db_client
.users
.insert_one(user)
@@ -257,8 +303,9 @@ async fn callback(
.as_object_id()
.context("inserted_id is not ObjectId")?
.to_string();
jwt_client()
.generate(user_id)
.encode(user_id)
.context("failed to generate jwt")?
}
};

View File

@@ -1,22 +1,22 @@
use std::{str::FromStr, time::Duration};
use std::time::Duration;
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use aws_config::{BehaviorVersion, Region};
use aws_sdk_ec2::{
Client,
types::{
BlockDeviceMapping, EbsBlockDevice,
InstanceNetworkInterfaceSpecification, InstanceStateChange,
InstanceStateName, InstanceStatus, InstanceType, ResourceType,
Tag, TagSpecification, VolumeType,
Tag, TagSpecification,
},
Client,
};
use base64::Engine;
use komodo_client::entities::{
alert::{Alert, AlertData, SeverityLevel},
komodo_timestamp,
server_template::aws::AwsServerTemplateConfig,
ResourceTarget,
alert::{Alert, AlertData, SeverityLevel},
builder::AwsBuilderConfig,
komodo_timestamp,
};
use crate::{alert::send_alerts, config::core_config};
@@ -29,20 +29,40 @@ pub struct Ec2Instance {
pub ip: String,
}
/// Provides credentials in the core config file to the AWS client
#[derive(Debug)]
struct CredentialsFromConfig;
impl aws_credential_types::provider::ProvideCredentials
for CredentialsFromConfig
{
fn provide_credentials<'a>(
&'a self,
) -> aws_credential_types::provider::future::ProvideCredentials<'a>
where
Self: 'a,
{
aws_credential_types::provider::future::ProvideCredentials::new(
async {
let config = core_config();
Ok(aws_credential_types::Credentials::new(
&config.aws.access_key_id,
&config.aws.secret_access_key,
None,
None,
"komodo-config",
))
},
)
}
}
#[instrument]
async fn create_ec2_client(region: String) -> Client {
// There may be a better way to pass these keys to client
std::env::set_var(
"AWS_ACCESS_KEY_ID",
&core_config().aws.access_key_id,
);
std::env::set_var(
"AWS_SECRET_ACCESS_KEY",
&core_config().aws.secret_access_key,
);
let region = Region::new(region);
let config = aws_config::defaults(BehaviorVersion::v2024_03_28())
let config = aws_config::defaults(BehaviorVersion::latest())
.region(region)
.credentials_provider(CredentialsFromConfig)
.load()
.await;
Client::new(&config)
@@ -51,12 +71,12 @@ async fn create_ec2_client(region: String) -> Client {
#[instrument]
pub async fn launch_ec2_instance(
name: &str,
config: AwsServerTemplateConfig,
config: &AwsBuilderConfig,
) -> anyhow::Result<Ec2Instance> {
let AwsServerTemplateConfig {
let AwsBuilderConfig {
region,
instance_type,
volumes,
volume_gb,
ami_id,
subnet_id,
security_group_ids,
@@ -66,19 +86,22 @@ pub async fn launch_ec2_instance(
user_data,
port: _,
use_https: _,
git_providers: _,
docker_registries: _,
secrets: _,
} = config;
let instance_type = handle_unknown_instance_type(
InstanceType::from(instance_type.as_str()),
)?;
let client = create_ec2_client(region.clone()).await;
let mut req = client
let req = client
.run_instances()
.image_id(ami_id)
.instance_type(instance_type)
.network_interfaces(
InstanceNetworkInterfaceSpecification::builder()
.subnet_id(subnet_id)
.associate_public_ip_address(assign_public_ip)
.associate_public_ip_address(*assign_public_ip)
.set_groups(security_group_ids.to_vec().into())
.device_index(0)
.build(),
@@ -90,6 +113,17 @@ pub async fn launch_ec2_instance(
.resource_type(ResourceType::Instance)
.build(),
)
.block_device_mappings(
BlockDeviceMapping::builder()
.set_device_name("/dev/sda1".to_string().into())
.set_ebs(
EbsBlockDevice::builder()
.volume_size(*volume_gb)
.build()
.into(),
)
.build(),
)
.min_count(1)
.max_count(1)
.user_data(
@@ -97,26 +131,6 @@ pub async fn launch_ec2_instance(
.encode(user_data),
);
for volume in volumes {
let ebs = EbsBlockDevice::builder()
.volume_size(volume.size_gb)
.volume_type(
VolumeType::from_str(volume.volume_type.as_ref())
.context("invalid volume type")?,
)
.set_iops((volume.iops != 0).then_some(volume.iops))
.set_throughput(
(volume.throughput != 0).then_some(volume.throughput),
)
.build();
req = req.block_device_mappings(
BlockDeviceMapping::builder()
.set_device_name(volume.device_name.into())
.set_ebs(ebs.into())
.build(),
)
}
let res = req
.send()
.await
@@ -136,7 +150,7 @@ pub async fn launch_ec2_instance(
let state_name =
get_ec2_instance_state_name(&client, &instance_id).await?;
if state_name == Some(InstanceStateName::Running) {
let ip = if use_public_ip {
let ip = if *use_public_ip {
get_ec2_instance_public_ip(&client, &instance_id).await?
} else {
instance
@@ -212,21 +226,37 @@ async fn terminate_ec2_instance_inner(
Ok(res)
}
/// Automatically retries 5 times, waiting 2 sec in between
#[instrument(level = "debug")]
async fn get_ec2_instance_status(
client: &Client,
instance_id: &str,
) -> anyhow::Result<Option<InstanceStatus>> {
let status = client
.describe_instance_status()
.instance_ids(instance_id)
.send()
let mut try_count = 1;
loop {
match async {
anyhow::Ok(
client
.describe_instance_status()
.instance_ids(instance_id)
.send()
.await
.context("failed to describe instance status from aws")?
.instance_statuses()
.first()
.cloned(),
)
}
.await
.context("failed to get instance status from aws")?
.instance_statuses()
.first()
.cloned();
Ok(status)
{
Ok(res) => return Ok(res),
Err(e) if try_count > 4 => return Err(e),
Err(_) => {
tokio::time::sleep(Duration::from_secs(2)).await;
try_count += 1;
}
}
}
}
#[instrument(level = "debug")]
@@ -248,28 +278,43 @@ async fn get_ec2_instance_state_name(
Ok(Some(state))
}
/// Automatically retries 5 times, waiting 2 sec in between
#[instrument(level = "debug")]
async fn get_ec2_instance_public_ip(
client: &Client,
instance_id: &str,
) -> anyhow::Result<String> {
let ip = client
.describe_instances()
.instance_ids(instance_id)
.send()
let mut try_count = 1;
loop {
match async {
anyhow::Ok(
client
.describe_instances()
.instance_ids(instance_id)
.send()
.await
.context("failed to describe instances from aws")?
.reservations()
.first()
.context("instance reservations is empty")?
.instances()
.first()
.context("instances is empty")?
.public_ip_address()
.context("instance has no public ip")?
.to_string(),
)
}
.await
.context("failed to get instance status from aws")?
.reservations()
.first()
.context("instance reservations is empty")?
.instances()
.first()
.context("instances is empty")?
.public_ip_address()
.context("instance has no public ip")?
.to_string();
Ok(ip)
{
Ok(res) => return Ok(res),
Err(e) if try_count > 4 => return Err(e),
Err(_) => {
tokio::time::sleep(Duration::from_secs(2)).await;
try_count += 1;
}
}
}
}
fn handle_unknown_instance_type(
@@ -1056,7 +1101,90 @@ fn handle_unknown_instance_type(
| InstanceType::Z1d6xlarge
| InstanceType::Z1dLarge
| InstanceType::Z1dMetal
| InstanceType::Z1dXlarge => Ok(instance_type),
| InstanceType::Z1dXlarge
| InstanceType::C7gdMetal
| InstanceType::C7gnMetal
| InstanceType::C7iFlex2xlarge
| InstanceType::C7iFlex4xlarge
| InstanceType::C7iFlex8xlarge
| InstanceType::C7iFlexLarge
| InstanceType::C7iFlexXlarge
| InstanceType::C8g12xlarge
| InstanceType::C8g16xlarge
| InstanceType::C8g24xlarge
| InstanceType::C8g2xlarge
| InstanceType::C8g48xlarge
| InstanceType::C8g4xlarge
| InstanceType::C8g8xlarge
| InstanceType::C8gLarge
| InstanceType::C8gMedium
| InstanceType::C8gMetal24xl
| InstanceType::C8gMetal48xl
| InstanceType::C8gXlarge
| InstanceType::G612xlarge
| InstanceType::G616xlarge
| InstanceType::G624xlarge
| InstanceType::G62xlarge
| InstanceType::G648xlarge
| InstanceType::G64xlarge
| InstanceType::G68xlarge
| InstanceType::G6Xlarge
| InstanceType::G6e12xlarge
| InstanceType::G6e16xlarge
| InstanceType::G6e24xlarge
| InstanceType::G6e2xlarge
| InstanceType::G6e48xlarge
| InstanceType::G6e4xlarge
| InstanceType::G6e8xlarge
| InstanceType::G6eXlarge
| InstanceType::Gr64xlarge
| InstanceType::Gr68xlarge
| InstanceType::M7gdMetal
| InstanceType::M8g12xlarge
| InstanceType::M8g16xlarge
| InstanceType::M8g24xlarge
| InstanceType::M8g2xlarge
| InstanceType::M8g48xlarge
| InstanceType::M8g4xlarge
| InstanceType::M8g8xlarge
| InstanceType::M8gLarge
| InstanceType::M8gMedium
| InstanceType::M8gMetal24xl
| InstanceType::M8gMetal48xl
| InstanceType::M8gXlarge
| InstanceType::Mac2M1ultraMetal
| InstanceType::R7gdMetal
| InstanceType::R7izMetal16xl
| InstanceType::R7izMetal32xl
| InstanceType::R8g12xlarge
| InstanceType::R8g16xlarge
| InstanceType::R8g24xlarge
| InstanceType::R8g2xlarge
| InstanceType::R8g48xlarge
| InstanceType::R8g4xlarge
| InstanceType::R8g8xlarge
| InstanceType::R8gLarge
| InstanceType::R8gMedium
| InstanceType::R8gMetal24xl
| InstanceType::R8gMetal48xl
| InstanceType::R8gXlarge
| InstanceType::U7i12tb224xlarge
| InstanceType::U7ib12tb224xlarge
| InstanceType::U7in16tb224xlarge
| InstanceType::U7in24tb224xlarge
| InstanceType::U7in32tb224xlarge
| InstanceType::X8g12xlarge
| InstanceType::X8g16xlarge
| InstanceType::X8g24xlarge
| InstanceType::X8g2xlarge
| InstanceType::X8g48xlarge
| InstanceType::X8g4xlarge
| InstanceType::X8g8xlarge
| InstanceType::X8gLarge
| InstanceType::X8gMedium
| InstanceType::X8gMetal24xl
| InstanceType::X8gMetal48xl
| InstanceType::X8gXlarge => Ok(instance_type),
other => Err(anyhow!("unknown InstanceType: {other:?}")),
}
}

View File

@@ -1,157 +0,0 @@
use anyhow::{anyhow, Context};
use axum::http::{HeaderName, HeaderValue};
use reqwest::{RequestBuilder, StatusCode};
use serde::{de::DeserializeOwned, Serialize};
use super::{
common::{
HetznerActionResponse, HetznerDatacenterResponse,
HetznerServerResponse, HetznerVolumeResponse,
},
create_server::{CreateServerBody, CreateServerResponse},
create_volume::{CreateVolumeBody, CreateVolumeResponse},
};
const BASE_URL: &str = "https://api.hetzner.cloud/v1";
pub struct HetznerClient(reqwest::Client);
impl HetznerClient {
pub fn new(token: &str) -> HetznerClient {
HetznerClient(
reqwest::ClientBuilder::new()
.default_headers(
[(
HeaderName::from_static("authorization"),
HeaderValue::from_str(&format!("Bearer {token}"))
.unwrap(),
)]
.into_iter()
.collect(),
)
.build()
.context("failed to build Hetzner request client")
.unwrap(),
)
}
pub async fn get_server(
&self,
id: i64,
) -> anyhow::Result<HetznerServerResponse> {
self.get(&format!("/servers/{id}")).await
}
pub async fn create_server(
&self,
body: &CreateServerBody,
) -> anyhow::Result<CreateServerResponse> {
self.post("/servers", body).await
}
#[allow(unused)]
pub async fn delete_server(
&self,
id: i64,
) -> anyhow::Result<HetznerActionResponse> {
self.delete(&format!("/servers/{id}")).await
}
pub async fn get_volume(
&self,
id: i64,
) -> anyhow::Result<HetznerVolumeResponse> {
self.get(&format!("/volumes/{id}")).await
}
pub async fn create_volume(
&self,
body: &CreateVolumeBody,
) -> anyhow::Result<CreateVolumeResponse> {
self.post("/volumes", body).await
}
#[allow(unused)]
pub async fn delete_volume(&self, id: i64) -> anyhow::Result<()> {
let res = self
.0
.delete(format!("{BASE_URL}/volumes/{id}"))
.send()
.await
.context("failed at request to delete volume")?;
let status = res.status();
if status == StatusCode::NO_CONTENT {
Ok(())
} else {
let text = res
.text()
.await
.context("failed to get response body as text")?;
Err(anyhow!("{status} | {text}"))
}
}
#[allow(unused)]
pub async fn list_datacenters(
&self,
) -> anyhow::Result<HetznerDatacenterResponse> {
self.get("/datacenters").await
}
async fn get<Res: DeserializeOwned>(
&self,
path: &str,
) -> anyhow::Result<Res> {
let req = self.0.get(format!("{BASE_URL}{path}"));
handle_req(req).await.with_context(|| {
format!("failed at GET request to Hetzner | path: {path}")
})
}
async fn post<Body: Serialize, Res: DeserializeOwned>(
&self,
path: &str,
body: &Body,
) -> anyhow::Result<Res> {
let req = self.0.post(format!("{BASE_URL}{path}")).json(&body);
handle_req(req).await.with_context(|| {
format!("failed at POST request to Hetzner | path: {path}")
})
}
#[allow(unused)]
async fn delete<Res: DeserializeOwned>(
&self,
path: &str,
) -> anyhow::Result<Res> {
let req = self.0.delete(format!("{BASE_URL}{path}"));
handle_req(req).await.with_context(|| {
format!("failed at DELETE request to Hetzner | path: {path}")
})
}
}
async fn handle_req<Res: DeserializeOwned>(
req: RequestBuilder,
) -> anyhow::Result<Res> {
let res = req.send().await?;
let status = res.status();
if status.is_success() {
res.json().await.context("failed to parse response to json")
} else {
let text = res
.text()
.await
.context("failed to get response body as text")?;
if let Ok(json_error) =
serde_json::from_str::<serde_json::Value>(&text)
{
return Err(anyhow!("{status} | {json_error:?}"));
}
Err(anyhow!("{status} | {text}"))
}
}

View File

@@ -1,280 +0,0 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerServerResponse {
pub server: HetznerServer,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerServer {
pub id: i64,
pub name: String,
pub primary_disk_size: f64,
pub image: Option<HetznerImage>,
pub private_net: Vec<HetznerPrivateNet>,
pub public_net: HetznerPublicNet,
pub server_type: HetznerServerTypeDetails,
pub status: HetznerServerStatus,
#[serde(default)]
pub volumes: Vec<i64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerServerTypeDetails {
pub architecture: String,
pub cores: i64,
pub cpu_type: String,
pub description: String,
pub disk: f64,
pub id: i64,
pub memory: f64,
pub name: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerPrivateNet {
pub alias_ips: Vec<String>,
pub ip: String,
pub mac_address: String,
pub network: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerPublicNet {
#[serde(default)]
pub firewalls: Vec<HetznerFirewall>,
pub floating_ips: Vec<i64>,
pub ipv4: Option<HetznerIpv4>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerFirewall {
pub id: i64,
pub status: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerIpv4 {
pub id: Option<i64>,
pub blocked: bool,
pub dns_ptr: String,
pub ip: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerImage {
pub id: i64,
pub description: String,
pub name: Option<String>,
pub os_flavor: String,
pub os_version: Option<String>,
pub rapid_deploy: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerActionResponse {
pub action: HetznerAction,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerAction {
pub command: String,
pub error: Option<HetznerError>,
pub finished: Option<String>,
pub id: i64,
pub progress: i32,
pub resources: Vec<HetznerResource>,
pub started: String,
pub status: HetznerActionStatus,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerError {
pub code: String,
pub message: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerResource {
pub id: i64,
#[serde(rename = "type")]
pub ty: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerVolumeResponse {
pub volume: HetznerVolume,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerVolume {
/// Name of the Resource. Must be unique per Project.
pub name: String,
/// Point in time when the Resource was created (in ISO-8601 format).
pub created: String,
/// Filesystem of the Volume if formatted on creation, null if not formatted on creation
pub format: Option<HetznerVolumeFormat>,
/// ID of the Volume.
pub id: i64,
/// User-defined labels ( key/value pairs) for the Resource
pub labels: HashMap<String, String>,
/// Device path on the file system for the Volume
pub linux_device: String,
/// Protection configuration for the Resource.
pub protection: HetznerProtection,
/// ID of the Server the Volume is attached to, null if it is not attached at all
pub server: Option<i64>,
/// Size in GB of the Volume
pub size: i64,
/// Current status of the Volume. Allowed: `creating`, `available`
pub status: HetznerVolumeStatus,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerProtection {
/// Prevent the Resource from being deleted.
pub delete: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerDatacenterResponse {
pub datacenters: Vec<HetznerDatacenterDetails>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerDatacenterDetails {
pub id: i64,
pub name: String,
pub location: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HetznerLocation {
#[serde(rename = "nbg1")]
Nuremberg1,
#[serde(rename = "hel1")]
Helsinki1,
#[serde(rename = "fsn1")]
Falkenstein1,
#[serde(rename = "ash")]
Ashburn,
#[serde(rename = "hil")]
Hillsboro,
#[serde(rename = "sin")]
Singapore,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum HetznerDatacenter {
#[serde(rename = "nbg1-dc3")]
Nuremberg1Dc3,
#[serde(rename = "hel1-dc2")]
Helsinki1Dc2,
#[serde(rename = "fsn1-dc14")]
Falkenstein1Dc14,
#[serde(rename = "ash-dc1")]
AshburnDc1,
#[serde(rename = "hil-dc1")]
HillsboroDc1,
#[serde(rename = "sin-dc1")]
SingaporeDc1,
}
impl From<HetznerDatacenter> for HetznerLocation {
fn from(value: HetznerDatacenter) -> Self {
match value {
HetznerDatacenter::Nuremberg1Dc3 => HetznerLocation::Nuremberg1,
HetznerDatacenter::Helsinki1Dc2 => HetznerLocation::Helsinki1,
HetznerDatacenter::Falkenstein1Dc14 => {
HetznerLocation::Falkenstein1
}
HetznerDatacenter::AshburnDc1 => HetznerLocation::Ashburn,
HetznerDatacenter::HillsboroDc1 => HetznerLocation::Hillsboro,
HetznerDatacenter::SingaporeDc1 => HetznerLocation::Singapore,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HetznerVolumeFormat {
Xfs,
Ext4,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HetznerVolumeStatus {
Creating,
Available,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HetznerServerStatus {
Running,
Initializing,
Starting,
Stopping,
Off,
Deleting,
Migrating,
Rebuilding,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HetznerActionStatus {
Running,
Success,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
#[allow(clippy::enum_variant_names)]
pub enum HetznerServerType {
// Shared
#[serde(rename = "cpx11")]
SharedAmd2Core2Ram40Disk,
#[serde(rename = "cax11")]
SharedArm2Core4Ram40Disk,
#[serde(rename = "cx22")]
SharedIntel2Core4Ram40Disk,
#[serde(rename = "cpx21")]
SharedAmd3Core4Ram80Disk,
#[serde(rename = "cax21")]
SharedArm4Core8Ram80Disk,
#[serde(rename = "cx32")]
SharedIntel4Core8Ram80Disk,
#[serde(rename = "cpx31")]
SharedAmd4Core8Ram160Disk,
#[serde(rename = "cax31")]
SharedArm8Core16Ram160Disk,
#[serde(rename = "cx42")]
SharedIntel8Core16Ram160Disk,
#[serde(rename = "cpx41")]
SharedAmd8Core16Ram240Disk,
#[serde(rename = "cax41")]
SharedArm16Core32Ram320Disk,
#[serde(rename = "cx52")]
SharedIntel16Core32Ram320Disk,
#[serde(rename = "cpx51")]
SharedAmd16Core32Ram360Disk,
// Dedicated
#[serde(rename = "ccx13")]
DedicatedAmd2Core8Ram80Disk,
#[serde(rename = "ccx23")]
DedicatedAmd4Core16Ram160Disk,
#[serde(rename = "ccx33")]
DedicatedAmd8Core32Ram240Disk,
#[serde(rename = "ccx43")]
DedicatedAmd16Core64Ram360Disk,
#[serde(rename = "ccx53")]
DedicatedAmd32Core128Ram600Disk,
#[serde(rename = "ccx63")]
DedicatedAmd48Core192Ram960Disk,
}

View File

@@ -1,75 +0,0 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::common::{
HetznerAction, HetznerDatacenter, HetznerLocation, HetznerServer,
HetznerServerType,
};
#[derive(Debug, Clone, Serialize)]
pub struct CreateServerBody {
/// Name of the Server to create (must be unique per Project and a valid hostname as per RFC 1123)
pub name: String,
/// Auto-mount Volumes after attach
#[serde(skip_serializing_if = "Option::is_none")]
pub automount: Option<bool>,
/// ID or name of Datacenter to create Server in (must not be used together with location)
#[serde(skip_serializing_if = "Option::is_none")]
pub datacenter: Option<HetznerDatacenter>,
/// ID or name of Location to create Server in (must not be used together with datacenter)
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<HetznerLocation>,
/// Firewalls which should be applied on the Server's public network interface at creation time
pub firewalls: Vec<Firewall>,
/// ID or name of the Image the Server is created from
pub image: String,
/// User-defined labels (key-value pairs) for the Resource
pub labels: HashMap<String, String>,
/// Network IDs which should be attached to the Server private network interface at the creation time
pub networks: Vec<i64>,
/// ID of the Placement Group the server should be in
#[serde(skip_serializing_if = "Option::is_none")]
pub placement_group: Option<i64>,
/// Public Network options
pub public_net: PublicNet,
/// ID or name of the Server type this Server should be created with
pub server_type: HetznerServerType,
/// SSH key IDs ( integer ) or names ( string ) which should be injected into the Server at creation time
pub ssh_keys: Vec<String>,
/// This automatically triggers a Power on a Server-Server Action after the creation is finished and is returned in the next_actions response object.
pub start_after_create: bool,
/// Cloud-Init user data to use during Server creation. This field is limited to 32KiB.
#[serde(skip_serializing_if = "Option::is_none")]
pub user_data: Option<String>,
/// Volume IDs which should be attached to the Server at the creation time. Volumes must be in the same Location.
pub volumes: Vec<i64>,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub struct Firewall {
/// ID of the Firewall
pub firewall: i64,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub struct PublicNet {
/// Attach an IPv4 on the public NIC. If false, no IPv4 address will be attached.
pub enable_ipv4: bool,
/// Attach an IPv6 on the public NIC. If false, no IPv6 address will be attached.
pub enable_ipv6: bool,
/// ID of the ipv4 Primary IP to use. If omitted and enable_ipv4 is true, a new ipv4 Primary IP will automatically be created.
#[serde(skip_serializing_if = "Option::is_none")]
pub ipv4: Option<i64>,
/// ID of the ipv6 Primary IP to use. If omitted and enable_ipv6 is true, a new ipv6 Primary IP will automatically be created.
#[serde(skip_serializing_if = "Option::is_none")]
pub ipv6: Option<i64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CreateServerResponse {
pub action: HetznerAction,
pub next_actions: Vec<HetznerAction>,
pub root_password: Option<String>,
pub server: HetznerServer,
}

View File

@@ -1,36 +0,0 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::common::{
HetznerAction, HetznerLocation, HetznerVolume, HetznerVolumeFormat,
};
#[derive(Debug, Clone, Serialize)]
pub struct CreateVolumeBody {
/// Name of the volume
pub name: String,
/// Auto-mount Volume after attach. server must be provided.
#[serde(skip_serializing_if = "Option::is_none")]
pub automount: Option<bool>,
/// Format Volume after creation. One of: xfs, ext4
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<HetznerVolumeFormat>,
/// User-defined labels (key-value pairs) for the Resource
pub labels: HashMap<String, String>,
/// Location to create the Volume in (can be omitted if Server is specified)
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<HetznerLocation>,
/// Server to which to attach the Volume once it's created (Volume will be created in the same Location as the server)
#[serde(skip_serializing_if = "Option::is_none")]
pub server: Option<i64>,
/// Size of the Volume in GB
pub size: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CreateVolumeResponse {
pub action: HetznerAction,
pub next_actions: Vec<HetznerAction>,
pub volume: HetznerVolume,
}

View File

@@ -1,281 +0,0 @@
use std::{
sync::{Arc, Mutex, OnceLock},
time::Duration,
};
use anyhow::{anyhow, Context};
use futures::future::join_all;
use komodo_client::entities::server_template::hetzner::{
HetznerDatacenter, HetznerServerTemplateConfig, HetznerServerType,
HetznerVolumeFormat,
};
use crate::{
cloud::hetzner::{
common::HetznerServerStatus, create_server::CreateServerBody,
create_volume::CreateVolumeBody,
},
config::core_config,
};
use self::{client::HetznerClient, common::HetznerVolumeStatus};
mod client;
mod common;
mod create_server;
mod create_volume;
fn hetzner() -> Option<&'static HetznerClient> {
static HETZNER_CLIENT: OnceLock<Option<HetznerClient>> =
OnceLock::new();
HETZNER_CLIENT
.get_or_init(|| {
let token = &core_config().hetzner.token;
(!token.is_empty()).then(|| HetznerClient::new(token))
})
.as_ref()
}
pub struct HetznerServerMinimal {
pub id: i64,
pub ip: String,
}
const POLL_RATE_SECS: u64 = 3;
const MAX_POLL_TRIES: usize = 100;
#[instrument]
pub async fn launch_hetzner_server(
name: &str,
config: HetznerServerTemplateConfig,
) -> anyhow::Result<HetznerServerMinimal> {
let hetzner =
*hetzner().as_ref().context("Hetzner token not configured")?;
let HetznerServerTemplateConfig {
image,
datacenter,
private_network_ids,
placement_group,
enable_public_ipv4,
enable_public_ipv6,
firewall_ids,
server_type,
ssh_keys,
user_data,
use_public_ip,
labels,
volumes,
port: _,
use_https: _,
} = config;
let datacenter = hetzner_datacenter(datacenter);
// Create volumes and get their ids
let mut volume_ids = Vec::new();
for volume in volumes {
let body = CreateVolumeBody {
name: volume.name,
format: Some(hetzner_format(volume.format)),
location: Some(datacenter.into()),
labels: volume.labels,
size: volume.size_gb,
automount: None,
server: None,
};
let id = hetzner
.create_volume(&body)
.await
.context("failed to create hetzner volume")?
.volume
.id;
volume_ids.push(id);
}
// Make sure volumes are available before continue
let vol_ids_poll = Arc::new(Mutex::new(volume_ids.clone()));
for _ in 0..MAX_POLL_TRIES {
if vol_ids_poll.lock().unwrap().is_empty() {
break;
}
tokio::time::sleep(Duration::from_secs(POLL_RATE_SECS)).await;
let ids = vol_ids_poll.lock().unwrap().clone();
let futures = ids.into_iter().map(|id| {
let vol_ids = vol_ids_poll.clone();
async move {
let Ok(res) = hetzner.get_volume(id).await else {
return;
};
if matches!(res.volume.status, HetznerVolumeStatus::Available)
{
vol_ids.lock().unwrap().retain(|_id| *_id != id);
}
}
});
join_all(futures).await;
}
if !vol_ids_poll.lock().unwrap().is_empty() {
return Err(anyhow!("Volumes not ready after poll"));
}
let body = CreateServerBody {
name: name.to_string(),
automount: None,
datacenter: Some(datacenter),
location: None,
firewalls: firewall_ids
.into_iter()
.map(|firewall| create_server::Firewall { firewall })
.collect(),
image,
labels,
networks: private_network_ids,
placement_group: (placement_group > 0).then_some(placement_group),
public_net: create_server::PublicNet {
enable_ipv4: enable_public_ipv4,
enable_ipv6: enable_public_ipv6,
ipv4: None,
ipv6: None,
},
server_type: hetzner_server_type(server_type),
ssh_keys,
start_after_create: true,
user_data: (!user_data.is_empty()).then_some(user_data),
volumes: volume_ids,
};
let server_id = hetzner
.create_server(&body)
.await
.context("failed to create hetnzer server")?
.server
.id;
for _ in 0..MAX_POLL_TRIES {
tokio::time::sleep(Duration::from_secs(POLL_RATE_SECS)).await;
let Ok(res) = hetzner.get_server(server_id).await else {
continue;
};
if matches!(res.server.status, HetznerServerStatus::Running) {
let ip = if use_public_ip {
res
.server
.public_net
.ipv4
.context("instance does not have public ipv4 attached")?
.ip
} else {
res
.server
.private_net
.first()
.context("no private networks attached")?
.ip
.to_string()
};
let server = HetznerServerMinimal { id: server_id, ip };
return Ok(server);
}
}
Err(anyhow!(
"failed to verify server running after polling status"
))
}
fn hetzner_format(
format: HetznerVolumeFormat,
) -> common::HetznerVolumeFormat {
match format {
HetznerVolumeFormat::Xfs => common::HetznerVolumeFormat::Xfs,
HetznerVolumeFormat::Ext4 => common::HetznerVolumeFormat::Ext4,
}
}
fn hetzner_datacenter(
datacenter: HetznerDatacenter,
) -> common::HetznerDatacenter {
match datacenter {
HetznerDatacenter::Nuremberg1Dc3 => {
common::HetznerDatacenter::Nuremberg1Dc3
}
HetznerDatacenter::Helsinki1Dc2 => {
common::HetznerDatacenter::Helsinki1Dc2
}
HetznerDatacenter::Falkenstein1Dc14 => {
common::HetznerDatacenter::Falkenstein1Dc14
}
HetznerDatacenter::AshburnDc1 => {
common::HetznerDatacenter::AshburnDc1
}
HetznerDatacenter::HillsboroDc1 => {
common::HetznerDatacenter::HillsboroDc1
}
HetznerDatacenter::SingaporeDc1 => {
common::HetznerDatacenter::SingaporeDc1
}
}
}
fn hetzner_server_type(
server_type: HetznerServerType,
) -> common::HetznerServerType {
match server_type {
HetznerServerType::SharedAmd2Core2Ram40Disk => {
common::HetznerServerType::SharedAmd2Core2Ram40Disk
}
HetznerServerType::SharedArm2Core4Ram40Disk => {
common::HetznerServerType::SharedArm2Core4Ram40Disk
}
HetznerServerType::SharedIntel2Core4Ram40Disk => {
common::HetznerServerType::SharedIntel2Core4Ram40Disk
}
HetznerServerType::SharedAmd3Core4Ram80Disk => {
common::HetznerServerType::SharedAmd3Core4Ram80Disk
}
HetznerServerType::SharedArm4Core8Ram80Disk => {
common::HetznerServerType::SharedArm4Core8Ram80Disk
}
HetznerServerType::SharedIntel4Core8Ram80Disk => {
common::HetznerServerType::SharedIntel4Core8Ram80Disk
}
HetznerServerType::SharedAmd4Core8Ram160Disk => {
common::HetznerServerType::SharedAmd4Core8Ram160Disk
}
HetznerServerType::SharedArm8Core16Ram160Disk => {
common::HetznerServerType::SharedArm8Core16Ram160Disk
}
HetznerServerType::SharedIntel8Core16Ram160Disk => {
common::HetznerServerType::SharedIntel8Core16Ram160Disk
}
HetznerServerType::SharedAmd8Core16Ram240Disk => {
common::HetznerServerType::SharedAmd8Core16Ram240Disk
}
HetznerServerType::SharedArm16Core32Ram320Disk => {
common::HetznerServerType::SharedArm16Core32Ram320Disk
}
HetznerServerType::SharedIntel16Core32Ram320Disk => {
common::HetznerServerType::SharedIntel16Core32Ram320Disk
}
HetznerServerType::SharedAmd16Core32Ram360Disk => {
common::HetznerServerType::SharedAmd16Core32Ram360Disk
}
HetznerServerType::DedicatedAmd2Core8Ram80Disk => {
common::HetznerServerType::DedicatedAmd2Core8Ram80Disk
}
HetznerServerType::DedicatedAmd4Core16Ram160Disk => {
common::HetznerServerType::DedicatedAmd4Core16Ram160Disk
}
HetznerServerType::DedicatedAmd8Core32Ram240Disk => {
common::HetznerServerType::DedicatedAmd8Core32Ram240Disk
}
HetznerServerType::DedicatedAmd16Core64Ram360Disk => {
common::HetznerServerType::DedicatedAmd16Core64Ram360Disk
}
HetznerServerType::DedicatedAmd32Core128Ram600Disk => {
common::HetznerServerType::DedicatedAmd32Core128Ram600Disk
}
HetznerServerType::DedicatedAmd48Core192Ram960Disk => {
common::HetznerServerType::DedicatedAmd48Core192Ram960Disk
}
}
}

View File

@@ -1,10 +1,9 @@
pub mod aws;
#[allow(unused)]
pub mod hetzner;
#[derive(Debug)]
pub enum BuildCleanupData {
Server { repo_name: String },
/// Nothing to clean up
Server,
/// Clean up AWS instance
Aws { instance_id: String, region: String },
}

View File

@@ -8,7 +8,7 @@ use komodo_client::entities::{
config::core::{
AwsCredentials, CoreConfig, DatabaseConfig, Env,
GithubWebhookAppConfig, GithubWebhookAppInstallationConfig,
HetznerCredentials, OauthCredentials,
OauthCredentials,
},
logger::LogConfig,
};
@@ -120,11 +120,6 @@ pub fn core_config() -> &'static CoreConfig {
.komodo_aws_secret_access_key)
.unwrap_or(config.aws.secret_access_key),
},
hetzner: HetznerCredentials {
token: maybe_read_item_from_file(env.komodo_hetzner_token_file, env
.komodo_hetzner_token)
.unwrap_or(config.hetzner.token),
},
github_webhook_app: GithubWebhookAppConfig {
app_id: maybe_read_item_from_file(env.komodo_github_webhook_app_app_id_file, env
.komodo_github_webhook_app_app_id)
@@ -139,6 +134,8 @@ pub fn core_config() -> &'static CoreConfig {
title: env.komodo_title.unwrap_or(config.title),
host: env.komodo_host.unwrap_or(config.host),
port: env.komodo_port.unwrap_or(config.port),
bind_ip: env.komodo_bind_ip.unwrap_or(config.bind_ip),
timezone: env.komodo_timezone.unwrap_or(config.timezone),
first_server: env.komodo_first_server.unwrap_or(config.first_server),
frontend_path: env.komodo_frontend_path.unwrap_or(config.frontend_path),
jwt_ttl: env
@@ -176,12 +173,16 @@ pub fn core_config() -> &'static CoreConfig {
.unwrap_or(config.ui_write_disabled),
disable_confirm_dialog: env.komodo_disable_confirm_dialog
.unwrap_or(config.disable_confirm_dialog),
disable_websocket_reconnect: env.komodo_disable_websocket_reconnect
.unwrap_or(config.disable_websocket_reconnect),
enable_new_users: env.komodo_enable_new_users
.unwrap_or(config.enable_new_users),
disable_user_registration: env.komodo_disable_user_registration
.unwrap_or(config.disable_user_registration),
disable_non_admin_create: env.komodo_disable_non_admin_create
.unwrap_or(config.disable_non_admin_create),
lock_login_credentials_for: env.komodo_lock_login_credentials_for
.unwrap_or(config.lock_login_credentials_for),
local_auth: env.komodo_local_auth
.unwrap_or(config.local_auth),
logging: LogConfig {
@@ -191,6 +192,7 @@ pub fn core_config() -> &'static CoreConfig {
stdio: env
.komodo_logging_stdio
.unwrap_or(config.logging.stdio),
pretty: env.komodo_logging_pretty.unwrap_or(config.logging.pretty),
otlp_endpoint: env
.komodo_logging_otlp_endpoint
.unwrap_or(config.logging.otlp_endpoint),
@@ -198,6 +200,7 @@ pub fn core_config() -> &'static CoreConfig {
.komodo_logging_opentelemetry_service_name
.unwrap_or(config.logging.opentelemetry_service_name),
},
pretty_startup_config: env.komodo_pretty_startup_config.unwrap_or(config.pretty_startup_config),
ssl_enabled: env.komodo_ssl_enabled.unwrap_or(config.ssl_enabled),
ssl_key_file: env.komodo_ssl_key_file.unwrap_or(config.ssl_key_file),
ssl_cert_file: env.komodo_ssl_cert_file.unwrap_or(config.ssl_cert_file),

View File

@@ -12,7 +12,6 @@ use komodo_client::entities::{
provider::{DockerRegistryAccount, GitProviderAccount},
repo::Repo,
server::Server,
server_template::ServerTemplate,
stack::Stack,
stats::SystemStatsRecord,
sync::ResourceSync,
@@ -50,7 +49,6 @@ pub struct DbClient {
pub procedures: Collection<Procedure>,
pub actions: Collection<Action>,
pub alerters: Collection<Alerter>,
pub server_templates: Collection<ServerTemplate>,
pub resource_syncs: Collection<ResourceSync>,
pub stacks: Collection<Stack>,
//
@@ -89,7 +87,9 @@ impl DbClient {
client = client.address(address);
}
_ => {
error!("config.mongo not configured correctly. must pass either config.mongo.uri, or config.mongo.address + config.mongo.username? + config.mongo.password?");
error!(
"config.mongo not configured correctly. must pass either config.mongo.uri, or config.mongo.address + config.mongo.username? + config.mongo.password?"
);
std::process::exit(1)
}
}
@@ -118,8 +118,6 @@ impl DbClient {
alerters: resource_collection(&db, "Alerter").await?,
procedures: resource_collection(&db, "Procedure").await?,
actions: resource_collection(&db, "Action").await?,
server_templates: resource_collection(&db, "ServerTemplate")
.await?,
resource_syncs: resource_collection(&db, "ResourceSync")
.await?,
stacks: resource_collection(&db, "Stack").await?,

View File

@@ -84,8 +84,8 @@ pub struct UpdateGuard<'a, States: Default + Send + 'static>(
&'a Mutex<States>,
);
impl<'a, States: Default + Send + 'static> Drop
for UpdateGuard<'a, States>
impl<States: Default + Send + 'static> Drop
for UpdateGuard<'_, States>
{
fn drop(&mut self) {
let mut lock = match self.0.lock() {

View File

@@ -0,0 +1,73 @@
use std::collections::HashMap;
use komodo_client::entities::{
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, procedure::Procedure, repo::Repo,
server::Server, stack::Stack, sync::ResourceSync,
};
#[derive(Debug, Default)]
pub struct AllResourcesById {
pub servers: HashMap<String, Server>,
pub deployments: HashMap<String, Deployment>,
pub stacks: HashMap<String, Stack>,
pub builds: HashMap<String, Build>,
pub repos: HashMap<String, Repo>,
pub procedures: HashMap<String, Procedure>,
pub actions: HashMap<String, Action>,
pub builders: HashMap<String, Builder>,
pub alerters: HashMap<String, Alerter>,
pub syncs: HashMap<String, ResourceSync>,
}
impl AllResourcesById {
/// Use `match_tags` to filter resources by tag.
pub async fn load() -> anyhow::Result<Self> {
let map = HashMap::new();
let id_to_tags = &map;
let match_tags = &[];
Ok(Self {
servers: crate::resource::get_id_to_resource_map::<Server>(
id_to_tags, match_tags,
)
.await?,
deployments: crate::resource::get_id_to_resource_map::<
Deployment,
>(id_to_tags, match_tags)
.await?,
builds: crate::resource::get_id_to_resource_map::<Build>(
id_to_tags, match_tags,
)
.await?,
repos: crate::resource::get_id_to_resource_map::<Repo>(
id_to_tags, match_tags,
)
.await?,
procedures:
crate::resource::get_id_to_resource_map::<Procedure>(
id_to_tags, match_tags,
)
.await?,
actions: crate::resource::get_id_to_resource_map::<Action>(
id_to_tags, match_tags,
)
.await?,
builders: crate::resource::get_id_to_resource_map::<Builder>(
id_to_tags, match_tags,
)
.await?,
alerters: crate::resource::get_id_to_resource_map::<Alerter>(
id_to_tags, match_tags,
)
.await?,
syncs: crate::resource::get_id_to_resource_map::<ResourceSync>(
id_to_tags, match_tags,
)
.await?,
stacks: crate::resource::get_id_to_resource_map::<Stack>(
id_to_tags, match_tags,
)
.await?,
})
}
}

View File

@@ -1,27 +1,26 @@
use std::time::Duration;
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use formatting::muted;
use komodo_client::entities::{
Version,
builder::{AwsBuilderConfig, Builder, BuilderConfig},
komodo_timestamp,
server::Server,
server_template::aws::AwsServerTemplateConfig,
update::{Log, Update},
Version,
};
use periphery_client::{
api::{self, GetVersionResponse},
PeripheryClient,
api::{self, GetVersionResponse},
};
use crate::{
cloud::{
aws::ec2::{
launch_ec2_instance, terminate_ec2_instance_with_retry,
Ec2Instance,
},
BuildCleanupData,
aws::ec2::{
Ec2Instance, launch_ec2_instance,
terminate_ec2_instance_with_retry,
},
},
config::core_config,
helpers::update::update_update,
@@ -44,7 +43,9 @@ pub async fn get_builder_periphery(
match builder.config {
BuilderConfig::Url(config) => {
if config.address.is_empty() {
return Err(anyhow!("Builder has not yet configured an address"));
return Err(anyhow!(
"Builder has not yet configured an address"
));
}
let periphery = PeripheryClient::new(
config.address,
@@ -53,17 +54,13 @@ pub async fn get_builder_periphery(
} else {
config.passkey
},
Duration::from_secs(3),
);
periphery
.health_check()
.await
.context("Url Builder failed health check")?;
Ok((
periphery,
BuildCleanupData::Server {
repo_name: resource_name,
},
))
Ok((periphery, BuildCleanupData::Server))
}
BuilderConfig::Server(config) => {
if config.server_id.is_empty() {
@@ -71,12 +68,7 @@ pub async fn get_builder_periphery(
}
let server = resource::get::<Server>(&config.server_id).await?;
let periphery = periphery_client(&server)?;
Ok((
periphery,
BuildCleanupData::Server {
repo_name: resource_name,
},
))
Ok((periphery, BuildCleanupData::Server))
}
BuilderConfig::Aws(config) => {
get_aws_builder(&resource_name, version, config, update).await
@@ -95,11 +87,8 @@ async fn get_aws_builder(
let version = version.map(|v| format!("-v{v}")).unwrap_or_default();
let instance_name = format!("BUILDER-{resource_name}{version}");
let Ec2Instance { instance_id, ip } = launch_ec2_instance(
&instance_name,
AwsServerTemplateConfig::from_builder_config(&config),
)
.await?;
let Ec2Instance { instance_id, ip } =
launch_ec2_instance(&instance_name, &config).await?;
info!("ec2 instance launched");
@@ -119,8 +108,11 @@ async fn get_aws_builder(
let protocol = if config.use_https { "https" } else { "http" };
let periphery_address =
format!("{protocol}://{ip}:{}", config.port);
let periphery =
PeripheryClient::new(&periphery_address, &core_config().passkey);
let periphery = PeripheryClient::new(
&periphery_address,
&core_config().passkey,
Duration::from_secs(3),
);
let start_connect_ts = komodo_timestamp();
let mut res = Ok(GetVersionResponse {
@@ -173,17 +165,14 @@ async fn get_aws_builder(
)
}
#[instrument(skip(periphery, update))]
#[instrument(skip(update))]
pub async fn cleanup_builder_instance(
periphery: PeripheryClient,
cleanup_data: BuildCleanupData,
update: &mut Update,
) {
match cleanup_data {
BuildCleanupData::Server { repo_name } => {
let _ = periphery
.request(api::git::DeleteRepo { name: repo_name })
.await;
BuildCleanupData::Server => {
// Nothing to clean up
}
BuildCleanupData::Aws {
instance_id,

View File

@@ -9,9 +9,9 @@ pub struct Cache<K: PartialEq + Eq + Hash, T: Clone + Default> {
}
impl<
K: PartialEq + Eq + Hash + std::fmt::Debug + Clone,
T: Clone + Default,
> Cache<K, T>
K: PartialEq + Eq + Hash + std::fmt::Debug + Clone,
T: Clone + Default,
> Cache<K, T>
{
#[instrument(level = "debug", skip(self))]
pub async fn get(&self, key: &K) -> Option<T> {
@@ -70,9 +70,9 @@ impl<
}
impl<
K: PartialEq + Eq + Hash + std::fmt::Debug + Clone,
T: Clone + Default + Busy,
> Cache<K, T>
K: PartialEq + Eq + Hash + std::fmt::Debug + Clone,
T: Clone + Default + Busy,
> Cache<K, T>
{
#[instrument(level = "debug", skip(self))]
pub async fn busy(&self, id: &K) -> bool {

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